Java 多執行緒知識的簡單總結

語言: CN / TW / HK

theme: devui-blue highlight: vs2015


持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第1天,點選檢視活動詳情

Java 提供了多執行緒程式設計的內建支援,讓我們可以輕鬆開發多執行緒應用。

Java 中我們最為熟悉的執行緒就是 main 執行緒——主執行緒。

一個程序可以併發多個執行緒,每條執行緒並行執行不同的任務。執行緒是程序的基本單位,是一個單一順序的控制流,一個程序一直執行,直到所有的“非守護執行緒”都結束執行後才能結束。Java 中常見的守護執行緒有:垃圾回收執行緒、

這裡簡要述說以下併發和並行的區別。

併發:同一時間段內有多個任務在執行

並行:同一時間點上有多個任務同時在執行

多執行緒可以幫助我們高效地執行任務,合理利用 CPU 資源,充分地發揮多核 CPU 的效能。但是多執行緒也並不總是能夠讓程式高效執行的,多執行緒切換帶來的開銷、執行緒死鎖、執行緒異常等等問題,都會使得多執行緒開發較單執行緒開發更麻煩。因此,有必要學習 Java 多執行緒的相關知識,從而提高開發效率。

1 建立多執行緒

根據官方文件 Thread (Java Platform SE 8 ) (oracle.com)java.lang.Thread 的說明,可以看到執行緒的建立方式主要有兩種:

There are two ways to create a new thread of execution. One is to declare a class to be a subclass of Thread. This subclass should override the run method of class Thread. An instance of the subclass can then be allocated and started.

The other way to create a thread is to declare a class that implements the Runnable interface. That class then implements the run method. An instance of the class can then be allocated, passed as an argument when creating Thread, and started.

可以看到,有兩種建立執行緒的方式:

  • 宣告一個類繼承 Thread 類,這個子類需要重寫 run 方法,隨後建立這個子類的例項,這個例項就可以建立並啟動一個執行緒執行任務;
  • 宣告一個類實現介面 Runnable 並實現 run 方法。這個類的例項作為引數分配給一個 Thread 例項,隨後使用 Thread 例項建立並啟動執行緒即可

除此之外的建立執行緒的方法,諸如使用 CallableFutureTask、執行緒池等等,無非是在此基礎上的擴充套件,檢視原始碼可以看到 FutureTask 也實現了 Runnable 介面。

使用繼承 Thread 類的方法建立執行緒的程式碼:

```java /* * 使用繼承 Thread 類的方法建立執行緒 / public class CreateOne { public static void main(String[] args) { Thread t = new MySubThread(); t.start(); } }

class MySubThread extends Thread { @Override public void run() { // currentThread() 是 Thread 的靜態方法,可以獲取正在執行當前程式碼的執行緒例項 System.out.println(Thread.currentThread().getName() + "執行任務"); } }

// ================================== 執行結果 Thread-0執行任務 ```

使用實現 Runnable 介面的方法建立執行緒的程式碼:

```java /* * 使用實現 Runnable 介面的方法建立執行緒 / public class CreateTwo { public static void main(String[] args) { RunnableImpl r = new RunnableImpl(); Thread t = new Thread(r); t.start(); } }

class RunnableImpl implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + "執行任務"); } }

// ================================== 執行結果 Thread-0執行任務 ```

1.1 孰優孰劣

建立執行緒雖然有兩種方法,但是在實際開發中,使用實現介面 Runnable 的方法更好,原因如下:

  1. 檢視 Threadrun 方法,可以看到:

```java // Thread 例項的成員變數 target 是一個 Runnable 例項,可以通過 Thread 的構造方法傳入 private Runnable target;

// 如果有傳入 Runnable 例項,那麼就執行它的 run 方法 // 如果重寫,就完全執行我們自己的邏輯 public void run() { if (target != null) { target.run(); } } ```

  1. 檢視上面的原始碼,我們可以知道,Thread 類並不是定義執行任務的主體,而是 Runnable 定義執行任務內容,Thread 呼叫執行,從而實現執行緒與任務的解耦
  2. 由於執行緒與任務解耦,我們可以複用執行緒,而不是當需要執行任務就去建立執行緒、執行完畢就銷燬執行緒,這樣帶來的系統開銷太大。這也是執行緒池的基本思想。
  3. 此外,Java 只只支援單繼承,如果繼承 Thread 使用多執行緒,那麼後續需要通過繼承的方式擴充套件功能,那會相當麻煩。

2 start 和 run 方法

從上面可以得知,有兩種建立執行緒的方式,我們通過 Thread 類或 Runnable 介面的 run 方法定義任務,通過 Threadstart 方法建立並啟動執行緒。

❗❗ 我們不能通過 run 方法啟動並建立一個執行緒,它只是一個普通方法,如果直接呼叫這個方法,其實只是呼叫這個方法的執行緒在執行任務罷了。

```java // 將上面的程式碼修改一下,檢視執行結果 public class CreateOne { public static void main(String[] args) { Thread t = new MySubThread(); t.run(); //t.start(); } }

// ===================== 執行結果 main執行任務 ```

檢視 start 方法的原始碼:

```java // 執行緒狀態,為 0 表示還未啟動 private volatile int threadStatus = 0;

// ❗❗ 同步方法,確保建立、啟動執行緒是執行緒安全的 public synchronized void start() { // 如果執行緒狀態不為 0,那麼丟擲異常——💡即執行緒已經建立了 if (threadStatus != 0) throw new IllegalThreadStateException(); // 將當前執行緒新增到執行緒組 group.add(this);

boolean started = false;
try {
    // 這是一個本地方法
    start0();
    started = true;
} finally {
    try {
        if (!started) {
            group.threadStartFailed(this);
        }
    } catch (Throwable ignore) {
    }
}

}

// 由本地方法實現,只需要知道,該方法呼叫後會建立一個執行緒,並且會執行 run 方法 private native void start0(); ```

由上面的原始碼可以得知:

  1. 建立並啟動一個執行緒是執行緒安全的
  2. start() 方法不能反覆呼叫,否則會丟擲異常

3 怎麼停止執行緒

執行緒並不是無休止地執行下去的,通常情況下,執行緒停止的條件有:

  1. run 方法執行結束
  2. 執行緒發生異常,但是沒有捕獲處理

除此之外,我們還需要自定義某些情況下需要通知執行緒停止,例如:

  1. 使用者主動取消任務
  2. 任務執行時間超時、出錯
  3. 出現故障,服務需要快速停止
  4. ...

💡 為什麼不能直接簡單粗暴的停止執行緒呢?通過通知執行緒停止任務,我們可以更優雅地停止執行緒,讓執行緒儲存問題現場、記錄日誌、傳送警報、友好提示等等,令執行緒在合適的程式碼位置停止執行緒,從而避免一些資料丟失等情況。

令執行緒停止的方法是讓執行緒捕獲中斷異常或檢測中斷標誌位,從而優雅地停止執行緒,這是推薦的做法。而不推薦的做法有,使用被標記為過時的方法:stopresumesuspend,這些方法可能會造成死鎖、執行緒不安全等情況,由於已經過時了,所以不做過多介紹。

3.1 通知執行緒中斷

我們要使用通知的方式停止目標執行緒,通過以下方法,希望能夠幫助你掌握中斷執行緒的方法:

```java /* * 中斷執行緒 / public class InterruptThread { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { long i = 0; // isInterrupted() 檢測當前執行緒是否處於中斷狀態 while (i < Long.MAX_VALUE && !Thread.currentThread().isInterrupted()) { i++; } System.out.println(i); });

    t.start();
    // 主執行緒睡眠 1 秒,通知執行緒中斷
    Thread.sleep(1000);
    t.interrupt();
}

} // 執行結果 1436125519 ```

這是中斷執行緒的方法之一,還有其他方法,當執行緒處於阻塞狀態時,執行緒並不能執行到檢測執行緒狀態的程式碼位置,然後正確響應中斷,這個時候,我們需要通過捕獲異常的方式停止執行緒:

java /** * 通過捕獲中斷異常停止執行緒 */ public class InterruptThreadByException { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ long i = 0; while (i < Long.MAX_VALUE) { i++; try { // 執行緒大部分時間處於阻塞狀態,sleep 方法會丟擲中斷異常 InterruptedException Thread.sleep(100); } catch (InterruptedException e) { // 捕獲到中斷異常,代表執行緒被通知中斷,做出相應處理再停止執行緒 System.out.println("執行緒收到中斷通知 " + i); // 如果 try-catch 在 while 程式碼塊之外,可以不用 return 也可以結束程式碼 // 在 while 程式碼塊之內,如果沒有 return / break,那麼還是會進入下一次迴圈,並不能正確停止 return; } } }); t.start(); Thread.sleep(1000); t.interrupt(); } } // 執行結果 執行緒收到中斷通知 10

以上,就是停止執行緒的正確做法,此外,捕獲中斷異常後,會清除執行緒的中斷狀態,在實際開發中需要特別注意。例如,修改上面的程式碼:

java public class InterruptThreadByException { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(()->{ long i = 0; while (i < Long.MAX_VALUE) { i++; try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("執行緒收到中斷通知 " + i); // ❗❗ 新增這行程式碼,捕獲到中斷異常後,檢測中斷狀態,中斷狀態為 false System.out.println(Thread.currentThread().isInterrupted()); return; } } }); t.start(); Thread.sleep(1000); t.interrupt(); } }

所以,線上程中,如果呼叫了其他方法,如果該方法有異常發生,那麼:

  1. 將異常丟擲,而不是在子方法內部捕獲處理,由 run 方法統一處理異常
  2. 捕獲異常,並重新通知當前執行緒中斷,Thread.currentThread().interrupt()

例如:

```java public class SubMethodException { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new ExceptionRunnableA()); Thread t2 = new Thread(new ExceptionRunnableB()); t1.start(); t2.start(); Thread.sleep(1000); t1.interrupt(); t2.interrupt(); } }

class ExceptionRunnableA implements Runnable { @Override public void run() { try { while (true) { method(); } } catch (InterruptedException e) { System.out.println("run 方法內部捕獲中斷異常"); } }

public void method() throws InterruptedException {
    Thread.sleep(100000L);
}

}

class ExceptionRunnableB implements Runnable { @Override public void run() { while (!Thread.currentThread().isInterrupted()) { method(); } }

public void method()  {
    try {
        Thread.sleep(100000L);
    } catch (InterruptedException e) {
        System.out.println("子方法內部捕獲中斷異常");
        // 如果不重新設定中斷,執行緒將不能正確響應中斷
        Thread.currentThread().interrupt();
    }
}

} ```

綜上,總結出令執行緒正確停止的方法為:

  1. 使用 interrupt() 方法通知目標執行緒停止,標記目標執行緒的中斷狀態為 true
  2. 目標執行緒通過 isInterrupted() 不時地檢測執行緒的中斷狀態,根據情況決定是否停止執行緒
  3. 如果執行緒使用了阻塞方法例如 sleep(),那麼需要捕獲中斷異常並處理中斷通知,捕獲了中斷異常會重置中斷標記位
  4. 如果 run() 方法呼叫了其他子方法,那麼子方法:
  5. 將異常丟擲,傳遞到頂層 run 方法,由 run 方法統一處理
  6. 將異常捕獲,同時重新通知當前執行緒中斷

下面再說說關於中斷的幾個相關方法和一些會丟擲中斷異常的方法,使用的時候需要特別注意。

3.2 執行緒中斷的相關方法

  1. interrupt() 例項方法,通知目標執行緒中斷。
  2. static interrupted() 靜態方法,獲取當前執行緒是否處於中斷狀態,會重置中斷狀態,即如果中斷狀態為 true,那麼呼叫後中斷狀態為 false。方法內部通過 Thread.currentThread() 獲取執行執行緒例項。
  3. isInterrupted() 例項方法,獲取執行緒的中斷狀態,不會清除中斷狀態。

3.3 阻塞並能響應中斷的方法

  • Object.wait()
  • Thread.sleep()
  • Thread.join()

  • BlockingQueue.take() / put()

  • Lock.lockInterruptibly()
  • CountDownLatch.await()
  • CyclicBarrier.await()
  • Exchanger.exchange()

4 執行緒的生命週期

執行緒的生命週期狀態由六部分組成:

| 狀態 | 說明 | | ------------- | ------------------------------------------------------------ | | NEW | 執行緒剛建立,還沒有呼叫 start 方法,執行緒尚未啟動 | | RUNNABLE | 執行緒已經呼叫了 start 方法,已經準備好執行,正在等待 CPU 分配資源;或者正在執行 | | BLOCKED | 進入 synchronized 程式碼塊,但是沒有拿到物件鎖,進入阻塞狀態 | | WAITING | synchronized 程式碼塊中,同步鎖物件呼叫了 wait 方法,或者執行緒被呼叫了 join 方法等,進入等待狀態,需要被喚醒 | | TIMED WAITING | 計時等待狀態,等待一段時間自動甦醒,或者等待過程中被喚醒 | | TERMINATED | 執行緒執行結束,正常結束或者發生未捕獲的異常 |

可以用一張圖總結執行緒的生命週期,以及各個過程之間是如何轉換的:

未命名檔案 (6).png

5 Thread 和 Object 中的執行緒方法

現在,我們已經知道了執行緒的建立、啟動、停止以及執行緒的生命週期了,那麼,再來看看執行緒相關的方法有哪些。

首先,看看 Thread 中的一些方法:

| 方法 | 說明 | | ----------------------------- | ------------------------------------------------- | | sleep() | 讓執行緒等待一段時間,不會釋放鎖 | | join() | 當前執行緒等待目標執行緒執行結束 | | yield() | 放棄已經獲得的 CPU 資源,執行緒依舊是 RUNNABLE 狀態 | | currentThread() | 獲取當前的執行緒例項 | | start() | 啟動執行緒 | | run() | 執行緒任務主體 | | interrupt() | 通知執行緒中斷,設定中斷標誌為 true | | isInterrupted() | 檢查執行緒是否處於中斷狀態 | | interrupted() | 返回執行緒中斷狀態,會重置執行緒中斷狀態 | | ~~stop()/suspend()/resume()~~ | 過時的停止執行緒方法 |

再看看 Object 中的相關方法:

| 方法 | 說明 | | ----------- | ------------------------------------------------------------ | | wait() | 執行緒獲取物件鎖,進入等待狀態,必須配合同步程式碼塊使用;
要麼被喚醒,要麼計時等待時間結束 | | notify() | 隨機喚醒一個執行緒,被喚醒執行緒嘗試獲取同步鎖 | | notifyAll() | 喚醒所有執行緒,所有執行緒都會嘗試獲取同步鎖 |

執行以下程式碼,檢視 wait()sleep() 是否會釋放同步鎖

```java /* * 證明 sleep 不會釋放鎖,wait 會釋放鎖 / public class SleepAndWait {

private static Object lock = new Object();

public static void main(String[] args) {
    Thread t1 = new Thread(()->{
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "獲得同步鎖,呼叫 wait() 方法");
            try {
                lock.wait(2000);
                System.out.println(Thread.currentThread().getName() + "重新獲得同步鎖");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    Thread t2 = new Thread(()->{
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "獲得同步鎖,喚醒另一個執行緒,呼叫 sleep()");
            lock.notify();
            try {
                // 如果 sleep() 會釋放鎖,那麼在此期間,上面的執行緒將會繼續執行,即 sleep 不會釋放同步鎖
                Thread.sleep(2000);
                // 如果執行 wait 方法,那麼上面的執行緒將會繼續執行,證明 wait 方法會釋放鎖
                //lock.wait(2000);
                System.out.println(Thread.currentThread().getName() + "sleep 結束");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t1.start();
    t2.start();
}

} ```

上面的程式碼已經證明了 sleep() 不會釋放同步鎖,此外,sleep() 也不會釋放 Lock 的鎖,執行以下程式碼檢視結果:

```java /* * sleep 不會釋放 Lock 鎖 / public class SleepDontReleaseLock implements Runnable { private static Lock lock = new ReentrantLock();

@Override
public void run() {
    // 呼叫 lock 方法,執行緒會嘗試持有該鎖物件,如果已經被其他執行緒鎖住,那麼當前執行緒會進入阻塞狀態
    lock.lock();
    try {
        System.out.println(Thread.currentThread().getName() + "獲得 lock 鎖");
        // 如果 sleep 會釋放 Lock 鎖,那麼另一個執行緒會馬上列印上面的語句
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "釋放 lock 鎖");
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 當前執行緒釋放鎖,讓其他執行緒可以佔有鎖
        lock.unlock();
    }
}

public static void main(String[] args) {
    SleepDontReleaseLock task = new SleepDontReleaseLock();
    new Thread(task).start();
    new Thread(task).start();
}

} ```

5.1 wait 和 sleep 的異同

接下來總結 Object.wait()Thread.sleep() 方法的異同點。

相同點:

  1. 都會使執行緒進入阻塞狀態
  2. 都可以響應中斷

不同點:

  1. wait()Object 的例項方法,sleep()Thread 的靜態方法
  2. sleep() 需要指定時間
  3. wait() 會釋放鎖,sleep() 不會釋放鎖,包括同步鎖和 Lock
  4. wait() 必須配合 synchronized 使用

6 執行緒的相關屬性

現在我們已經對 Java 中的多執行緒有一定的瞭解了,我們再看看 Java 中執行緒 Thread 的一些相關屬性,即它的成員變數。

| 屬性 | 說明 | | --------------------- | ------------------------------------------------------------ | | 執行緒 ID | 唯一標識執行緒,無法修改,從 1 遞增,主執行緒 main ID 為 1
使用者建立的執行緒 ID 並不是從 2 開始,虛擬機器程序啟動後還會建立其他執行緒,例如垃圾回收執行緒 | | 名稱 name | 預設為 Thread-自增ID
可以通過 Thread.setName() 自定義執行緒名,方便區分執行緒、排查問題 | | 是否是守護執行緒 daemon | false 代表不為守護執行緒,一般會繼承父執行緒的型別
為目標執行緒提供服務,非守護執行緒執行結束後,會隨著虛擬機器一起停止
通常不使用守護執行緒,使用者執行緒一旦結束,守護執行緒也會結束 | | 優先順序 priority | 優先順序從小到大為 0-10, 預設為 5
通常不改變優先順序,因為:
1. 不同作業系統的優先順序定義不同
2. 優先順序會被作業系統修改
3. 低優先順序的執行緒可能一致無法獲取資源而無法執行 |

執行以下程式碼,瞭解執行緒的相關屬性

java public class ThreadFields { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { // 自定義執行緒的 ID 並不是從 2 開始 System.out.println("執行緒 " + Thread.currentThread().getName() + " 的執行緒 ID " + Thread.currentThread().getId()); while (true) { // 守護執行緒一直執行,但是 使用者執行緒即這裡的主執行緒結束後,也會隨著虛擬機器一起停止 } }); // 自定義執行緒名字 t.setName("自定義執行緒"); // 將其設定為守護執行緒 t.setDaemon(true); // 設定優先順序 Thread.MIN_PRIORITY = 1 Thread.MAX_PRIORITY = 10 t.setPriority(Thread.MIN_PRIORITY); t.start(); // 主執行緒的 ID 為 1 System.out.println("執行緒 " + Thread.currentThread().getName() + " 的執行緒 ID " + Thread.currentThread().getId()); Thread.sleep(3000); } }

7 全域性異常處理

在子執行緒中,如果發生了異常我們能夠及時捕獲並處理,那麼對程式執行並不會有什麼惡劣影響。

但是,如果發生了一些未捕獲的異常,在多執行緒情況下,這些異常打印出來的堆疊資訊,很容易淹沒在龐大的日誌中,我們可能很難察覺到,並且不好排查問題。

如果對這些異常都做捕獲處理,那麼就會造成程式碼的冗餘,編寫起來也不方便。

因此,我們可以編寫一個全域性異常處理器來處理子執行緒中丟擲的異常,統一地處理,解耦程式碼。

7.1 原始碼檢視

在講解如何處理子執行緒的異常問題前,我們先看看 JVM 預設情況下,是如何處理未捕獲的異常的。

檢視 Thread 的原始碼:

```java public class Thread implements Runnable { 【1】當發生未捕獲的異常時,JVM 會呼叫該方法,並傳遞異常資訊給異常處理器 可以在這裡打下斷點,線上程中丟擲異常不捕獲,IDEA 會跳轉到這裡 // 向處理程式傳送未捕獲的異常。此方法僅由JVM呼叫。 private void dispatchUncaughtException(Throwable e) { 【2】檢視第 9 行程式碼,可以看到如果沒有指定異常處理器,預設是執行緒組作為異常處理器 【3】呼叫這個異常處理器的處理方法,處理異常,檢視第 15 行 getUncaughtExceptionHandler().uncaughtException(this, e); }

public UncaughtExceptionHandler getUncaughtExceptionHandler() {
    return uncaughtExceptionHandler != null ?
        uncaughtExceptionHandler : group;
}

【4】UncaughtExceptionHandler 是 Thread 的內部介面,執行緒組也是該介面的實現,
    只有一個方法處理異常,接下來檢視第 25 行,看看 Group 是如何實現的
@FunctionalInterface
public interface UncaughtExceptionHandler {
    void uncaughtException(Thread t, Throwable e);
}

}

public class ThreadGroup implements Thread.UncaughtExceptionHandler { 【5】預設異常處理器的實現 public void uncaughtException(Thread t, Throwable e) { // 如果有父執行緒組,交給它處理 if (parent != null) { parent.uncaughtException(t, e); } else { // 獲取預設的異常處理器,如果沒有指定,那麼為 null Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } // 沒有指定異常處理器,列印堆疊資訊 else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } } } ```

7.2 自定義全域性異常處理器

通過上面的原始碼講解,已經可以知道 JVM 是如何處理未捕獲的異常的了,即只打印堆疊資訊。那麼,要如何自定義異常處理器呢?

具體方法為:

  1. 實現介面 Thread.UncaughtExceptionHandler 並實現方法 uncaughtException()
  2. 為建立的執行緒指定異常處理器

示例程式碼:

```java public class MyExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("發生了未捕獲的異常,進行日誌處理、報警處理、友好提示、資料備份等等......"); e.printStackTrace(); }

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        throw new RuntimeException();
    });
    t.setUncaughtExceptionHandler(new MyExceptionHandler());
    t.start();
}

} ```

8 多執行緒帶來的問題

合理地利用多執行緒能夠帶來效能上的提升,但是如果因為一些疏漏,多執行緒反而會成為程式設計師的噩夢。

例如,多執行緒開發,我們需要考慮執行緒安全問題、效能問題。

首先,講講執行緒安全問題。

什麼是執行緒安全?所謂執行緒安全,即

在多執行緒情況下,如果訪問某個物件,不需要額外處理,例如加鎖、令執行緒阻塞、額外的執行緒排程等,呼叫這個物件都能獲得正確的結果,那麼這個物件就是執行緒安全的

因此,在編寫多執行緒程式時,就需要考慮某個資料是否是執行緒安全的,如果這個物件滿足:

  1. 被多個執行緒共享
  2. 操作具有時序要求,先讀後寫
  3. 這個物件的類有他人編寫,並且沒有宣告是執行緒安全的

那麼我們就需要考慮使用同步鎖、Lock、併發工具類(java.util.concurrent)來保證這個物件是在多執行緒下是安全的。

再看看多執行緒帶來的效能問題。

多個執行緒的排程需要上下文切換,這需要耗費 CPU 資源。

所謂上下文,即處理器中暫存器、程式計數器內的資訊。

上下文切換,即 CPU 掛起一個執行緒,將其上下文儲存到記憶體中,從記憶體中獲取另一個執行執行緒的上下文,恢復到暫存器中,根據程式計數器中的指令恢復執行緒執行。

一個執行緒被掛起,另一個執行緒恢復執行,這個時候,被掛起的執行緒的資料快取對於執行執行緒來說是無效的,減緩了執行緒的執行速度,新的執行緒需要重新快取資料提升執行速度。

通常情況下,密集的 IO 操作、搶鎖操作都會帶來密集的上下文切換。

以上,是上下文切換帶來的效能問題,Java 的記憶體模型也會帶來效能問題,為了保證資料的可見性,JVM 會強制令資料快取失效,保證資料是實時最新的,這也犧牲了快取帶來的效能提升。

9 總結

這裡總結下上面的內容。

  1. 建立執行緒有兩種方式,繼承 Thread 和實現 Runnable
  2. start 方法才能正確建立和啟動執行緒,run 方法只是一個普通方法
  3. start 方法不能反覆呼叫,反覆呼叫會丟擲異常
  4. 正確停止執行緒的方法是通過 interrupt() 通知執行緒
  5. 執行緒不時地檢查中斷狀態並判斷是否停止執行緒,使用方法 isInterrupt()
  6. 如果執行緒阻塞,捕獲中斷異常,判斷是否停止執行緒
  7. 執行緒呼叫的子方法最好將異常丟擲,由 run 方法統一捕獲處理
  8. 執行緒呼叫的子方法如果捕獲異常,需要重新通知執行緒中斷
  9. 執行緒的生命週期為
  10. NEW
  11. RUNNABLE
  12. BLOCKED
  13. WAITING
  14. TIMED WAITING
  15. TERMINATED
  16. wait()/notify()/notifyAll() 必須配合同步鎖使用
  17. wait() 會釋放鎖,sleep() 不會釋放鎖,包括同步鎖和 Lock 鎖
  18. 執行緒的一些屬性
  19. 執行緒ID,無法修改
  20. 執行緒名 name,可以自定義
  21. 守護執行緒 daemon,執行緒型別會繼承自父執行緒,通常不指定執行緒為守護執行緒
  22. 優先順序 priority,通常使用預設優先順序,不改變優先順序
  23. 可以自定義全域性異常處理器,處理非主執行緒中的未捕獲的異常,如備份資料、日誌處理、報警等等
  24. 多執行緒開發會帶來執行緒安全問題、效能問題,開發過程需要特別注意