建立執行緒的4種方法 and 執行緒的生命週期

BLLBL發表於2021-10-16

執行緒的啟動和執行

方法一:使用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高併發核心程式設計》

相關文章