Java併發專題(一)認識執行緒

GrimMjx發表於2018-09-09

1.1 認識執行緒

  執行緒是輕量級程式,也是程式執行的一個路徑,每一個執行緒都有自己的區域性變數表、程式計數器(指向正在執行的指令指標)以及各自的生命週期,現代作業系統中一般不止一個執行緒在執行。比如說,當我們啟動了一個JVM的時候,作業系統建立一個新的程式(JVM程式),JVM程式中將會建立很多執行緒。總而言之,執行緒是一個時間段的描述,是CPU工作時間段的描述。你可以想象成一個生命體,從生到死。

 

1.2 執行緒的生命週期

  上文說過我們可以抽象的理解一個執行緒是一個CPU工作時間段的生命體,那麼他的生命週期是如何的呢?請看圖1-1。

  由圖可知,執行緒的生命週期大致分為以下5個主要階段

  • NEW
  • RUNNABLE
  • RUNNING
  • BLOCKED
  • TERMINATED

1.2.1 執行緒的NEW狀態

  用java關鍵字new建立一個Thread物件的時候,該執行緒的狀態為NEW狀態,準確的說,和你用關鍵字new建立一個普通的java物件沒有什麼區別,NEW狀態通過start()方法進入RUNNABLE狀態。有些軟文上說建立一個執行緒的方式有2種,繼承Thread,實現Runnable介面。這種說法是不準確的,也可以說是錯誤的。在JDK中代表執行緒的只有Thread這個類,所以準確的說建立執行緒只有一種方式,那就是構造Thread類。而實現執行緒要執行的業務邏輯是重寫Thread的run()或者實現Runnable介面的run()。最終將Runnable例項用作構造Thread的引數。無論是哪一種,都是想將執行緒的控制本身和業務邏輯的執行分離開來。

1.2.2 執行緒的RUNNABLE狀態

  執行緒物件呼叫start()方法,才真正的在JVM程式中建立了一個執行緒,執行緒執行與否和程式一樣聽令與CPU排程,所以這裡只是成為RUNNABLE狀態,具備執行的資格,但是並沒有真正的執行起來而是在等待CPU排程。

  嚴格來說,RUNNABLE的執行緒只能意外終止或者進入RUNNING狀態。

1.2.3 執行緒的RUNNING狀態

  一旦CPU通過輪詢選中了執行緒,那麼它才真正開始執行自己的邏輯程式碼,正在RUNNING狀態的執行緒也是RUNNABLE的,但是反之不成立。

  該狀態可能發生如下轉換

  • 直接進入TERMINATED狀態,比如呼叫stop()方法(JDK已經不推薦使用)
  • 進入BLOCKED狀態,比如呼叫了sleep()或者wait()方法。
  • 進入某個阻塞的IO操作
  • 獲取某個鎖資源
  • CPU排程使該執行緒放棄執行,進入RUNNABLE狀態
  • 執行緒主動呼叫yield()方法,放棄執行權, 進入RUNNABLE狀態

1.2.4 執行緒的BLOCKED狀態

  該狀態可能切換至以下狀態

  • 直接進入TERMINATED,比如呼叫stop()方法(JDK已經不推薦使用)
  • 執行緒阻塞操作完成,進入RUNNABLE狀態,比如讀取到了資料位元組
  • 執行緒完成了指定時間的休眠,進入RUNNABLE狀態
  • wait中的執行緒被其他執行緒notify/notifyAll喚醒,進入RUNNABLE狀態
  • 執行緒獲取到了鎖資源,進入RUNNABLE狀態
  • 執行緒在阻塞過程中被打斷,其他執行緒呼叫了interrupt()方法,進入RUNNABLE狀態

1.2.5 執行緒的TERMINATED狀態

  TERMINAED狀態是一個最終狀態,相當於一個生命體的死亡,不會切換到其他狀態了,意味著整個生命週期的結束。下面這些情況將會導致執行緒進入TERMINATED狀態。

  • 執行緒執行正常結束
  • 執行緒執行出錯意外結束
  • JVM crash,導致這個程式裡的所有執行緒都結束。

 

1.3 Thread API介紹

1.3.1 sleep()方法介紹

public static native void sleep(long millis) throws InterruptedException;

public static void sleep(long millis, int nanos) throws InterruptedException;

  sleep()方法會使當前執行緒進入指定毫秒數的休眠,休眠有一個非常重要的特性,它不會放棄monitor鎖的所有權。可以這麼記,抱著鎖(你)睡覺。

1.3.2 yield()方法介紹

public static native void yield();

  yield()會提醒排程器我願意放棄當前的CPU資源,如果CPU的資源不緊張,則會忽略這種提示。

  呼叫yield()方法會使當前執行緒從RUNNING狀態切換到RUNNABLE狀態。yield()只是一個提示,CPU排程器並不會擔保每次都能滿足yield提示。

1.3.3 interrupt()方法介紹

  執行緒interrupt()方法是一個非常重要的API,有些方法(wait(),sleep(),join(),io操作等方法)的呼叫會使得當前執行緒進入阻塞狀態,而呼叫當前執行緒的interrupt方法,就可以打斷阻塞。因此這些方法有時會被稱為可中斷方法,打斷一個執行緒並不等於該執行緒的生命週期結束,僅僅是打斷了當前執行緒的阻塞狀態。接下來我們看一個例子。

/**
 * 打斷阻塞狀態測試類
 * 
 * @author GrimMjx
 */
public class InterruptTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            try {
                TimeUnit.MINUTES.sleep(1);
            } catch (InterruptedException e) {
                System.out.println("I am interrupted.");
            }
        });

        thread1.start();

        TimeUnit.SECONDS.sleep(2);
        thread1.interrupt();
    }
}

  上面的程式碼先建立出一個執行緒,休眠1分鐘,但是main執行緒在休眠2秒鐘之後呼叫interrupt()方法打斷thread1。

  interrupt()到底做了什麼事情呢?在一個執行緒內部存在一個名為interrupt flag的標識,如果一個執行緒被interrupt,那麼它的flag會被設定,但是如果當前執行緒正在執行可中斷方法被阻塞時,呼叫interrupt方法將其中斷,反而會導致flag被清除。

1.3.4 join()方法介紹

public final void join() throws InterruptedException;

  join某個執行緒A,會使當前執行緒進入等待狀態,直到執行緒A結束生命週期,或者到達給定的時間,那麼在此期間,當前執行緒都是出於BLOCKED狀態的,而不是執行緒A。接下來我們看一個例子。

/**
 * Join方法測試類
 *
 * @author GrimMjx
 */
public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        Thread thread = create("thread1");
        thread.start();

        //執行join方法
        thread.join();

        //主執行緒開始工作
        System.out.println(Thread.currentThread().getName() + " is processing");
        TimeUnit.SECONDS.sleep(1);
    }

    private static Thread create(String name) {
        return new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " is processing");
        }, name);
    }
}

  join()會使當前執行緒永遠等待下去,直到期間被另外的執行緒中斷,或者join的執行緒執行結束。concurrent包裡的CountDownLatch和CyclicBarrier都可以實現和join()方法一樣的功能,當某些執行緒達到一個點做一件事情。這個有興趣的同學可以自己去學習使用。並掌握其中的不同點和共同點。

 

相關文章