創建線程的4種方法 and 線程的生命周期


線程的啟動和運行

方法一:使用start()方法:用來啟動一個線程,當調用start方法后,JVM會開啟一個新線程執行用戶定義的線程代碼邏輯。

方法二:使用run()方法:作為線程代碼邏輯的入口方法。run方法不是由用戶程序來調用的,當調用start方法啟動一個線程之后,只要線程獲得了CPU執行時間,便進入run方法去執行具體的用戶線程代碼。

start方法用於啟動線程,run方法是用戶邏輯代碼執行入口。

1.創建一個空線程

main {
    Thread thread = new Thread();
    thread.start();
}

程序調用start方法啟動新線程的執行。新線程的執行會調用Thread的run方法,該方法是業務代碼的入口。查看一下Thread類的源碼,run方法的具體代碼如下:

public void run() {
    if(this.target != null) {
        this.target.run();
    }
}

這里的target屬性是Thread類的一個實例屬性,該屬性非常重要,后面會講到。在Thread類中,target屬性默認為空。在這個例子中,thread屬性默認為null。所以在thread線程執行時,其run方法其實什么也沒做,線程就執行完了。

2.繼承Thread類創建線程

new Thread(() -> {
            System.out.println(1);
        }).start();

3.實現Runnable接口創建線程

class TestMain implements Runnable {
    main {
        new Thread(TestMain::new).start();
    }
     
    @Override
    public void run() {
        System.out.println(12);
    }
}

4.使用Callable和FutureTask創建線程

前面的Thread和Runnable都不能獲取異步執行的結果。為了解決這個問題,Java在1.5之后提供了一種新的多線程創建方法:Callable接口和FutureTask類相結合創建線程。

1.Callable接口

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

Callable接口是一個泛型接口,也是函數式接口。其唯一的抽象方法call有返回值。

Callable能否和Runnable一樣,作為Thread實例的target使用呢?

答案是不可以。因為Thread的target屬性類型為Runnable,所以一個在Callable與Thread之間搭橋接線的重要接口即將登場。

2.RunnableFuture接口

這個重要的接口就是RunnableFuture接口,他實現了兩個目標,一是可以作為Thread實例的target實例,二是可以獲取異步執行的結果。

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

通過源碼可以看出:RunnableFuture是通過繼承Runnable和Future來實現上面2個目標的。

3.Future接口

Future接口至少提供了三大功能:(1)取消執行中的任務(2)判斷任務是否完成(3)獲取任務的執行結果

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    V get() throws InterruptedException, ExecutionException;

    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • V get():獲取異步任務執行的結果,這個方法的調用是阻塞性的。如果異步任務沒有執行完成,會一直被阻塞直到任務執行完成。

總體來說,Future是一個對異步任務進行交互、操作的接口。但是Future只是一個接口,它沒有辦法直接完成對異步任務的操作,JDK提供了一個默認的實現類-----FutureTask。

4.FutureTask類

FutureTask類是Future接口的實現類,提供了對異步任務的操作的具體實現。但是FutureTask類不僅實現了Future接口,還實現了Runnable接口,更精准的說FutureTask類實現了RunnableFuture接口。

前面提到RunnableFuture接口很關鍵,既可以作為Thread線程實例的target目標,又可以獲取並發任務執行的結果,是Thread與Callable之間一個非常重要的搭橋角色。

關系圖如下:

image

可以看出,FutureTask既能作為一個Runnable類型的target,又能作為Future異步任務來獲取Callable的計算結果。

FutureTask是如何完成多線程的並發執行、任務結果的異步獲取呢?他的內部有一個Callable類型的成員:

private Callable<V> callable;

Callable實例屬性用來保存並發執行的Callable類型的任務,並且Callable實例屬性需要在FutureTask實例構造時初始化。FutureTask實現了Runnable接口,在run方法的實現中會執行Callable的call方法。

FutureTask內部有一個outcome實例屬性用於保存執行結果。

public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

使用Callable和FutureTask創建線程的具體步驟

(1)創建Callable接口的實現類,並實現call方法,編寫好具體邏輯。

(2)使用Callable實現類的實例構造一個FutureTask實例。

(3)使用FutureTask實例作為Thread構造器的target入參,構造新的Thread線程實例

(4)調用Thread實例的start方法啟動新線程,內部執行過程:啟動Thread實例的run方法並發執行后,會執行FutureTask實例的run方法,最終會並發執行Callable實現類的call方法。

(5)調用FutureTask對象的get方法阻塞的獲得結果。

public static void main(String[] args) {
        new Thread(new FutureTask<>(() -> {
            int i = 1;
            return i;
        })).start();
    }

5.線程池創建線程

1.線程池的創建與執行目標提交

創建一個固定3個線程的線程池。

private static ExecutorService pool = Executors.newFixedThreadPool(3);

向ExecutorService線程池提交異步執行target目標任務的常用方法有:

// 方法一:無返回
void execute(Runnable command);
// 返回一個Future實例
<T> Future<T> submit(Callable<T> task);
// 也是返回Future實例
Future<?> submit(Runnable task);

2.線程池使用實戰

public class TestMain {

    public static ExecutorService pool = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        pool.execute(() -> {
            int i = 1;
        });

        final Future<?> submit = pool.submit(() -> {
            int i = 1;
            return i;
        });
        Integer o = (Integer) submit.get();
        System.out.println("o: " + o);


        AtomicInteger ans = new AtomicInteger();
        final Future<AtomicInteger> submit1 = pool.submit(new FutureTask<>(() -> {
            int i = 1;
            ans.set(i);
            return ans;
        }), ans);
        System.out.println("ans: " + submit1.get());
        pool.shutdown();
    }

Lambda中不允許使用局部變量:因為使用的是局部變量的副本,對局部變量本身不起作用,所以不能使用。但是可以使用引用類型的變量,比如原子類的ans變量,這才會對原值產生影響。

image

execute與submit區別:

(1)接收參數不一樣。

(2)submit有返回值,execute沒有返回值。

說明:實際生產環境禁止使用Executors創建線程池。

線程的核心原理

1.線程的生命周期

public static enum State {
    NEW, 新建
    RUNNABLE, 可執行
    BLOCKED, 阻塞
    WATTING, 等待
    TIMED_WAITING, 超時等待
    TERMINATED; 終止
}

在定義的6種狀態中,有4種比較常見的狀態,他們是:NEW、RUNNABLE、TERMINATED、TIMED_WAITING。

  • NEW狀態:創建成功但是沒有調用start方法啟動的線程實例都處於NEW狀態。

當然,並不是線程實例的start方法一調用,其狀態就從NEW到RUNNABLE,此時並不意味着線程立即獲取CPU時間片並且立即執行。

  • RUNNABLE狀態:前面說到,當調用了線程實例的start方法后,下一步如果線程獲取CPU時間片開始執行,JVM將異步調用線程的run方法執行其業務代碼。那么在run方法被調用之前,JVM在做什么呢?

JVM的幕后工作和操作系統的線程調度有關。當Java線程示例的start方法被調用后,操作系統中對應線程並不是運行狀態,而是就緒狀態,而Java線程沒有就緒態。就緒態的意思就是該線程已經滿足執行條件,處於等待系統調度的狀態,一旦被選中就會獲得CPU時間片,這時就變成運行態了。

在操作系統中,處於運行狀態的線程在CPU時間片用完后,又回到就緒態,等待CPU的下一次調度。就這樣,操作系統線程在就緒態和運行態之間被反復調度,知道線程的代碼邏輯完成或者異常終止為止。這時線程進入TERMINATED狀態。

image

就緒態和運行態都是操作系統的線程狀態。在Java中,沒有細分這兩種狀態,而是將他們二合一,都叫作RUNNABLE狀態。這時Java線程狀態和操作系統不一樣的的地方。

總結:NEW狀態的線程實例調用了start方法后,線程的狀態變為RUNNABLE。但是線程的run方法不一定會馬上執行,需要線程獲取了CPU時間片之后才會執行。

  • TERMINATED狀態:run方法執行完成之后就變成終止狀態了,異常也會。
  • TIME_WAITING狀態:處於等待狀態,例如Thread.sleep,Objects.wait,Thread.join等。(主動)
  • BLOCKED狀態:處於此狀態的線程不會占用CPU資源,例如線程等待獲取鎖,IO阻塞。(被動)

2.線程狀態實例

讓五個線程處於TIME-WAITING狀態,使用Jstack查看。

public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            for (int j = 0; j < 500; j++) {

                try {
                    Thread.sleep(500);
                    System.out.println(j);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }).start();
}
"Thread-0" #14 prio=5 os_prio=0 cpu=0.00ms elapsed=15.27s tid=0x0000018624761800 nid=0x2fcc waiting on condition  [0x0000003944bff000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(java.base@11.0.11/Native Method)
        at com.test.TestMain.lambda$main$0(TestMain.java:33)
        at com.test.TestMain$$Lambda$14/0x0000000800066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

"Thread-1" #15 prio=5 os_prio=0 cpu=0.00ms elapsed=15.27s tid=0x0000018624762000 nid=0x1d74 waiting on condition  [0x0000003944cfe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(java.base@11.0.11/Native Method)
        at com.test.TestMain.lambda$main$0(TestMain.java:33)
        at com.test.TestMain$$Lambda$14/0x0000000800066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

"Thread-2" #16 prio=5 os_prio=0 cpu=0.00ms elapsed=15.27s tid=0x0000018624763800 nid=0x3f08 waiting on condition  [0x0000003944dfe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(java.base@11.0.11/Native Method)
        at com.test.TestMain.lambda$main$0(TestMain.java:33)
        at com.test.TestMain$$Lambda$14/0x0000000800066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

"Thread-3" #17 prio=5 os_prio=0 cpu=0.00ms elapsed=15.27s tid=0x0000018624765000 nid=0x2b5c waiting on condition  [0x0000003944eff000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(java.base@11.0.11/Native Method)
        at com.test.TestMain.lambda$main$0(TestMain.java:33)
        at com.test.TestMain$$Lambda$14/0x0000000800066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

"Thread-4" #18 prio=5 os_prio=0 cpu=0.00ms elapsed=15.27s tid=0x0000018624768000 nid=0x52f0 waiting on condition  [0x0000003944ffe000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
        at java.lang.Thread.sleep(java.base@11.0.11/Native Method)
        at com.test.TestMain.lambda$main$0(TestMain.java:33)
        at com.test.TestMain$$Lambda$14/0x0000000800066840.run(Unknown Source)
        at java.lang.Thread.run(java.base@11.0.11/Thread.java:834)

Re

《Java高並發核心編程》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM