程式設計師必備介面測試偵錯工具:
推薦學習:《》
Java 提供了多執行緒程式設計的內建支援,讓我們可以輕鬆開發多執行緒應用。
Java 中我們最為熟悉的執行緒就是 main 執行緒——主執行緒。
一個程序可以並行多個執行緒,每條執行緒並行執行不同的任務。執行緒是程序的基本單位,是一個單一順序的控制流,一個程序一直執行,直到所有的「非守護執行緒」都結束執行後才能結束。Java 中常見的守護執行緒有:垃圾回收執行緒、
這裡簡要述說以下並行和並行的區別。
並行:同一時間段內有多個任務在執行
並行:同一時間點上有多個任務同時在執行
多執行緒可以幫助我們高效地執行任務,合理利用 CPU 資源,充分地發揮多核 CPU 的效能。但是多執行緒也並不總是能夠讓程式高效執行的,多執行緒切換帶來的開銷、執行緒死鎖、執行緒異常等等問題,都會使得多執行緒開發較單執行緒開發更麻煩。因此,有必要學習 Java 多執行緒的相關知識,從而提高開發效率。
根據官方檔案 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 範例建立並啟動執行緒即可
除此之外的建立執行緒的方法,諸如使用 Callable 和 FutureTask、執行緒池等等,無非是在此基礎上的擴充套件,檢視原始碼可以看到 FutureTask 也實現了 Runnable 介面。
使用繼承 Thread 類的方法建立執行緒的程式碼:
/**
* 使用繼承 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 介面的方法建立執行緒的程式碼:
/**
* 使用實現 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 的方法更好,原因如下:
檢視 Thread 的 run 方法,可以看到:
// Thread 範例的成員變數 target 是一個 Runnable 範例,可以通過 Thread 的構造方法傳入
private Runnable target;
// 如果有傳入 Runnable 範例,那麼就執行它的 run 方法
// 如果重寫,就完全執行我們自己的邏輯
public void run() {
if (target != null) {
target.run();
}
}
登入後複製
檢視上面的原始碼,我們可以知道,Thread 類並不是定義執行任務的主體,而是 Runnable 定義執行任務內容,Thread 呼叫執行,從而實現執行緒與任務的解耦。
由於執行緒與任務解耦,我們可以複用執行緒,而不是當需要執行任務就去建立執行緒、執行完畢就銷燬執行緒,這樣帶來的系統開銷太大。這也是執行緒池的基本思想。
此外,Java 只只支援單繼承,如果繼承 Thread 使用多執行緒,那麼後續需要通過繼承的方式擴充套件功能,那會相當麻煩。
2 start 和 run 方法
從上面可以得知,有兩種建立執行緒的方式,我們通過 Thread 類或 Runnable 介面的 run 方法定義任務,通過 Thread 的 start 方法建立並啟動執行緒。
我們不能通過 run 方法啟動並建立一個執行緒,它只是一個普通方法,如果直接呼叫這個方法,其實只是呼叫這個方法的執行緒在執行任務罷了。
// 將上面的程式碼修改一下,檢視執行結果
public class CreateOne {
public static void main(String[] args) {
Thread t = new MySubThread();
t.run();
//t.start();
}
}
// ===================== 執行結果
main執行任務
登入後複製
檢視 start 方法的原始碼:
// 執行緒狀態,為 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();
登入後複製
由上面的原始碼可以得知:
建立並啟動一個執行緒是執行緒安全的
start() 方法不能反覆呼叫,否則會丟擲異常
執行緒並不是無休止地執行下去的,通常情況下,執行緒停止的條件有:
run 方法執行結束
執行緒發生異常,但是沒有捕獲處理
除此之外,我們還需要自定義某些情況下需要通知執行緒停止,例如:
使用者主動取消任務
任務執行時間超時、出錯
出現故障,服務需要快速停止
...
為什麼不能直接簡單粗暴的停止執行緒呢?通過通知執行緒停止任務,我們可以更優雅地停止執行緒,讓執行緒儲存問題現場、記錄紀錄檔、傳送警報、友好提示等等,令執行緒在合適的程式碼位置停止執行緒,從而避免一些資料丟失等情況。
令執行緒停止的方法是讓執行緒捕獲中斷異常或檢測中斷標誌位,從而優雅地停止執行緒,這是推薦的做法。而不推薦的做法有,使用被標記為過時的方法:stop,resume,suspend,這些方法可能會造成死鎖、執行緒不安全等情況,由於已經過時了,所以不做過多介紹。
3.1 通知執行緒中斷
我們要使用通知的方式停止目標執行緒,通過以下方法,希望能夠幫助你掌握中斷執行緒的方法:
/**
* 中斷執行緒
*/
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
登入後複製
這是中斷執行緒的方法之一,還有其他方法,當執行緒處於阻塞狀態時,執行緒並不能執行到檢測執行緒狀態的程式碼位置,然後正確響應中斷,這個時候,我們需要通過捕獲異常的方式停止執行緒:
/**
* 通過捕獲中斷異常停止執行緒
*/
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
登入後複製
以上,就是停止執行緒的正確做法,此外,捕獲中斷異常後,會清除執行緒的中斷狀態,在實際開發中需要特別注意。例如,修改上面的程式碼:
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();
}
}
登入後複製
所以,線上程中,如果呼叫了其他方法,如果該方法有異常發生,那麼:
將異常丟擲,而不是在子方法內部捕獲處理,由 run 方法統一處理異常
捕獲異常,並重新通知當前執行緒中斷,Thread.currentThread().interrupt()
例如:
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();
}
}
}
登入後複製
綜上,總結出令執行緒正確停止的方法為:
使用 interrupt() 方法通知目標執行緒停止,標記目標執行緒的中斷狀態為 true
目標執行緒通過 isInterrupted() 不時地檢測執行緒的中斷狀態,根據情況決定是否停止執行緒
如果執行緒使用了阻塞方法例如 sleep(),那麼需要捕獲中斷異常並處理中斷通知,捕獲了中斷異常會重置中斷標記位
如果 run() 方法呼叫了其他子方法,那麼子方法:
將異常丟擲,傳遞到頂層 run 方法,由 run 方法統一處理
將異常捕獲,同時重新通知當前執行緒中斷
下面再說說關於中斷的幾個相關方法和一些會丟擲中斷異常的方法,使用的時候需要特別注意。
3.2 執行緒中斷的相關方法
interrupt() 實體方法,通知目標執行緒中斷。
static interrupted() 靜態方法,獲取當前執行緒是否處於中斷狀態,會重置中斷狀態,即如果中斷狀態為 true,那麼呼叫後中斷狀態為 false。方法內部通過 Thread.currentThread() 獲取執行執行緒範例。
isInterrupted() 實體方法,獲取執行緒的中斷狀態,不會清除中斷狀態。
3.3 阻塞並能響應中斷的方法
Object.wait()
Thread.sleep()
Thread.join()
BlockingQueue.take() / put()
Lock.lockInterruptibly()
CountDownLatch.await()
CyclicBarrier.await()
Exchanger.exchange()
4 執行緒的生命週期
執行緒的生命週期狀態由六部分組成:
可以用一張圖總結執行緒的生命週期,以及各個過程之間是如何轉換的:
現在,我們已經知道了執行緒的建立、啟動、停止以及執行緒的生命週期了,那麼,再來看看執行緒相關的方法有哪些。
首先,看看 Thread 中的一些方法:
再看看 Object 中的相關方法:
執行以下程式碼,檢視 wait() 和 sleep() 是否會釋放同步鎖
/**
* 證明 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 的鎖,執行以下程式碼檢視結果:
/**
* 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() 方法的異同點。
相同點:
都會使執行緒進入阻塞狀態
都可以響應中斷
不同點:
wait() 是 Object 的實體方法,sleep() 是 Thread 的靜態方法
sleep() 需要指定時間
wait() 會釋放鎖,sleep() 不會釋放鎖,包括同步鎖和 Lock 鎖
wait() 必須配合 synchronized 使用
現在我們已經對 Java 中的多執行緒有一定的瞭解了,我們再看看 Java 中執行緒 Thread 的一些相關屬性,即它的成員變數。
執行以下程式碼,瞭解執行緒的相關屬性
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.1 原始碼檢視
在講解如何處理子執行緒的異常問題前,我們先看看 JVM 預設情況下,是如何處理未捕獲的異常的。
檢視 Thread 的原始碼:
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 是如何處理未捕獲的異常的了,即只列印堆疊資訊。那麼,要如何自定義例外處理器呢?
具體方法為:
實現介面 Thread.UncaughtExceptionHandler 並實現方法 uncaughtException()
為建立的執行緒指定例外處理器
範例程式碼:
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();
}
}
登入後複製
合理地利用多執行緒能夠帶來效能上的提升,但是如果因為一些疏漏,多執行緒反而會成為程式設計師的噩夢。
例如,多執行緒開發,我們需要考慮執行緒安全問題、效能問題。
首先,講講執行緒安全問題。
什麼是執行緒安全?所謂執行緒安全,即
在多執行緒情況下,如果存取某個物件,不需要額外處理,例如加鎖、令執行緒阻塞、額外的執行緒排程等,呼叫這個物件都能獲得正確的結果,那麼這個物件就是執行緒安全的
因此,在編寫多執行緒程式時,就需要考慮某個資料是否是執行緒安全的,如果這個物件滿足:
被多個執行緒共用
操作具有時序要求,先讀後寫
這個物件的類有他人編寫,並且沒有宣告是執行緒安全的
那麼我們就需要考慮使用同步鎖、Lock、並行工具類(java.util.concurrent)來保證這個物件是在多執行緒下是安全的。
再看看多執行緒帶來的效能問題。
多個執行緒的排程需要上下文切換,這需要耗費 CPU 資源。
所謂上下文,即處理器中暫存器、程式計數器內的資訊。
上下文切換,即 CPU 掛起一個執行緒,將其上下文儲存到記憶體中,從記憶體中獲取另一個執行執行緒的上下文,恢復到暫存器中,根據程式計數器中的指令恢復執行緒執行。
一個執行緒被掛起,另一個執行緒恢復執行,這個時候,被掛起的執行緒的資料快取對於執行執行緒來說是無效的,減緩了執行緒的執行速度,新的執行緒需要重新快取資料提升執行速度。
通常情況下,密集的 IO 操作、搶鎖操作都會帶來密集的上下文切換。
以上,是上下文切換帶來的效能問題,Java 的記憶體模型也會帶來效能問題,為了保證資料的可見性,JVM 會強制令資料快取失效,保證資料是實時最新的,這也犧牲了快取帶來的效能提升。
這裡總結下上面的內容。
建立執行緒有兩種方式,繼承 Thread 和實現 Runnable
start 方法才能正確建立和啟動執行緒,run 方法只是一個普通方法
start 方法不能反覆呼叫,反覆呼叫會丟擲異常
正確停止執行緒的方法是通過 interrupt() 通知執行緒
執行緒不時地檢查中斷狀態並判斷是否停止執行緒,使用方法 isInterrupt()
如果執行緒阻塞,捕獲中斷異常,判斷是否停止執行緒
執行緒呼叫的子方法最好將異常丟擲,由 run 方法統一捕獲處理
執行緒呼叫的子方法如果捕獲異常,需要重新通知執行緒中斷
執行緒的生命週期為
NEW
RUNNABLE
BLOCKED
WAITING
TIMED WAITING
TERMINATED
wait()/notify()/notifyAll() 必須配合同步鎖使用
wait() 會釋放鎖,sleep() 不會釋放鎖,包括同步鎖和 Lock 鎖
執行緒的一些屬性
執行緒ID,無法修改
執行緒名 name,可以自定義
守護執行緒 daemon,執行緒型別會繼承自父執行緒,通常不指定執行緒為守護執行緒
優先順序 priority,通常使用預設優先順序,不改變優先順序
可以自定義全域性例外處理器,處理非主執行緒中的未捕獲的異常,如備份資料、紀錄檔處理、報警等等
多執行緒開發會帶來執行緒安全問題、效能問題,開發過程需要特別注意
推薦學習:《》
以上就是簡單總結Java多執行緒知識點的詳細內容,更多請關注TW511.COM其它相關文章!