java多线程详解
文章目錄
- 多線程基礎(chǔ)
- 進程
- 進程 vs 線程
- 多線程
- 創(chuàng)建新線程
- 線程的優(yōu)先級
- 練習(xí)
- 小結(jié)
- 線程的狀態(tài)
- 小結(jié)
- 中斷線程
- 小結(jié)
- 守護線程
- 練習(xí)
- 小結(jié)
- 線程同步
- 不需要synchronized的操作
- 小結(jié)
- 同步方法
- 小結(jié)
- 死鎖
- 死鎖
- 練習(xí)
- 小結(jié)
轉(zhuǎn)載于:https://www.liaoxuefeng.com/wiki/1252599548343744/1266265175882464
多線程基礎(chǔ)
現(xiàn)代操作系統(tǒng)(Windows,macOS,Linux)都可以執(zhí)行多任務(wù)。多任務(wù)就是同時運行多個任務(wù)。
CPU執(zhí)行代碼都是一條一條順序執(zhí)行的,但是,即使是單核cpu,也可以同時運行多個任務(wù)。因為操作系統(tǒng)執(zhí)行多任務(wù)實際上就是讓CPU對多個任務(wù)輪流交替執(zhí)行。
例如,假設(shè)我們有語文、數(shù)學(xué)、英語3門作業(yè)要做,每個作業(yè)需要30分鐘。我們把這3門作業(yè)看成是3個任務(wù),可以做1分鐘語文作業(yè),再做1分鐘數(shù)學(xué)作業(yè),再做1分鐘英語作業(yè):
這樣輪流做下去,在某些人眼里看來,做作業(yè)的速度就非常快,看上去就像同時在做3門作業(yè)一樣
類似的,操作系統(tǒng)輪流讓多個任務(wù)交替執(zhí)行,例如,讓瀏覽器執(zhí)行0.001秒,讓QQ執(zhí)行0.001秒,再讓音樂播放器執(zhí)行0.001秒,在人看來,CPU就是在同時執(zhí)行多個任務(wù)。
即使是多核CPU,因為通常任務(wù)的數(shù)量遠遠多于CPU的核數(shù),所以任務(wù)也是交替執(zhí)行的。
進程
在計算機中,我們把一個任務(wù)稱為一個進程,瀏覽器就是一個進程,視頻播放器是另一個進程,類似的,音樂播放器和Word都是進程。
某些進程內(nèi)部還需要同時執(zhí)行多個子任務(wù)。例如,我們在使用Word時,Word可以讓我們一邊打字,一邊進行拼寫檢查,同時還可以在后臺進行打印,我們把子任務(wù)稱為線程。
進程和線程的關(guān)系就是:一個進程可以包含一個或多個線程,但至少會有一個線程。
┌──────────┐│Process ││┌────────┐│┌──────────┐││ Thread ││┌──────────┐│Process ││└────────┘││Process ││┌────────┐││┌────────┐││┌────────┐│ ┌──────────┐││ Thread ││││ Thread ││││ Thread ││ │Process ││└────────┘││└────────┘││└────────┘│ │┌────────┐││┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘││└────────┘│ └──────────┘└──────────┘└──────────┘└──────────┘ ┌──────────────────────────────────────────────┐ │ Operating System │ └──────────────────────────────────────────────┘操作系統(tǒng)調(diào)度的最小任務(wù)單位其實不是進程,而是線程。常用的Windows、Linux等操作系統(tǒng)都采用搶占式多任務(wù),如何調(diào)度線程完全由操作系統(tǒng)決定,程序自己不能決定什么時候執(zhí)行,以及執(zhí)行多長時間。
因為同一個應(yīng)用程序,既可以有多個進程,也可以有多個線程,因此,實現(xiàn)多任務(wù)的方法,有以下幾種:
多進程模式(每個進程只有一個線程):
┌──────────┐ ┌──────────┐ ┌──────────┐ │Process │ │Process │ │Process │ │┌────────┐│ │┌────────┐│ │┌────────┐│ ││ Thread ││ ││ Thread ││ ││ Thread ││ │└────────┘│ │└────────┘│ │└────────┘│ └──────────┘ └──────────┘ └──────────┘多線程模式(一個進程有多個線程):
┌────────────────────┐ │Process │ │┌────────┐┌────────┐│ ││ Thread ││ Thread ││ │└────────┘└────────┘│ │┌────────┐┌────────┐│ ││ Thread ││ Thread ││ │└────────┘└────────┘│ └────────────────────┘多進程+多線程模式(復(fù)雜度最高):
┌──────────┐┌──────────┐┌──────────┐ │Process ││Process ││Process │ │┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘│ │┌────────┐││┌────────┐││┌────────┐│ ││ Thread ││││ Thread ││││ Thread ││ │└────────┘││└────────┘││└────────┘│ └──────────┘└──────────┘└──────────┘進程 vs 線程
進程和線程是包含關(guān)系,但是多任務(wù)既可以由多進程實現(xiàn),也可以由單進程內(nèi)的多線程實現(xiàn),還可以混合多進程+多線程。
具體采用哪種方式,要考慮到進程和線程的特點。
和多線程相比,多進程的缺點在于:
- 創(chuàng)建進程比創(chuàng)建線程開銷大,尤其是在Windows系統(tǒng)上;
- 進程間通信比線程間通信要慢,因為線程間通信就是讀寫同一個變量,速度很快。
而多進程的優(yōu)點在于:
多進程穩(wěn)定性比多線程高,因為在多進程的情況下,一個進程崩潰不會影響其他進程,而在多線程的情況下,任何一個線程崩潰會直接導(dǎo)致整個進程崩潰。
多線程
Java語言內(nèi)置了多線程支持:一個Java程序?qū)嶋H上是一個JVM進程,JVM進程用一個主線程來執(zhí)行main()方法,在main()方法內(nèi)部,我們又可以啟動多個線程。此外,JVM還有負責(zé)垃圾回收的其他工作線程等。
因此,對于大多數(shù)Java程序來說,我們說多任務(wù),實際上是說如何使用多線程實現(xiàn)多任務(wù)。
和單線程相比,多線程編程的特點在于:多線程經(jīng)常需要讀寫共享數(shù)據(jù),并且需要同步。例如,播放電影時,就必須由一個線程播放視頻,另一個線程播放音頻,兩個線程需要協(xié)調(diào)運行,否則畫面和聲音就不同步。因此,多線程編程的復(fù)雜度高,調(diào)試更困難。
Java多線程編程的特點又在于:
- 多線程模型是Java程序最基本的并發(fā)模型;
- 后續(xù)讀寫網(wǎng)絡(luò)、數(shù)據(jù)庫、Web開發(fā)等都依賴Java多線程模型。
因此,必須掌握Java多線程編程才能繼續(xù)深入學(xué)習(xí)其他內(nèi)容。
創(chuàng)建新線程
Java語言內(nèi)置了多線程支持。當Java程序啟動的時候,實際上是啟動了一個JVM進程,然后,JVM啟動主線程來執(zhí)行main()方法。在main()方法中,我們又可以啟動其他線程。
要創(chuàng)建一個新線程非常容易,我們需要實例化一個Thread實例,然后調(diào)用它的start()方法:
// 多線程 Run
public class Main {public static void main(String[] args) {Thread t = new Thread();t.start(); // 啟動新線程} }但是這個線程啟動后實際上什么也不做就立刻結(jié)束了。我們希望新線程能執(zhí)行指定的代碼,有以下幾種方法:
方法一:從Thread派生一個自定義類,然后覆寫run()方法:
// 多線程 Run
public class Main {public static void main(String[] args) {Thread t = new MyThread();t.start(); // 啟動新線程} }class MyThread extends Thread {@Overridepublic void run() {System.out.println("start new thread!");} }執(zhí)行上述代碼,注意到start()方法會在內(nèi)部自動調(diào)用實例的run()方法。
方法二:創(chuàng)建Thread實例時,傳入一個Runnable實例:
// 多線程 Run
public class Main {public static void main(String[] args) {Thread t = new Thread(new MyRunnable());t.start(); // 啟動新線程} }class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("start new thread!");} }或者用Java8引入的lambda語法進一步簡寫為:
// 多線程 Run
public class Main {public static void main(String[] args) {Thread t = new Thread(() -> {System.out.println("start new thread!");});t.start(); // 啟動新線程} }有童鞋會問,使用線程執(zhí)行的打印語句,和直接在main()方法執(zhí)行有區(qū)別嗎?
區(qū)別大了去了。我們看以下代碼:
public class Main {public static void main(String[] args) {System.out.println("main start...");Thread t = new Thread() {public void run() {System.out.println("thread run...");System.out.println("thread end.");}};t.start();System.out.println("main end...");} }我們用藍色表示主線程,也就是main線程,main線程執(zhí)行的代碼有4行,首先打印main start,然后創(chuàng)建Thread對象,緊接著調(diào)用start()啟動新線程。當start()方法被調(diào)用時,JVM就創(chuàng)建了一個新線程,我們通過實例變量t來表示這個新線程對象,并開始執(zhí)行。
接著,main線程繼續(xù)執(zhí)行打印main end語句,而t線程在main線程執(zhí)行的同時會并發(fā)執(zhí)行,打印thread run和thread end語句。
當run()方法結(jié)束時,新線程就結(jié)束了。而main()方法結(jié)束時,主線程也結(jié)束了。
我們再來看線程的執(zhí)行順序:
但是,除了可以肯定,main start會先打印外,main end打印在thread run之前、thread end之后或者之間,都無法確定。因為從t線程開始運行以后,兩個線程就開始同時運行了,并且由操作系統(tǒng)調(diào)度,程序本身無法確定線程的調(diào)度順序。
要模擬并發(fā)執(zhí)行的效果,我們可以在線程中調(diào)用Thread.sleep(),強迫當前線程暫停一段時間:
// 多線程 Run
public class Main {public static void main(String[] args) {System.out.println("main start...");Thread t = new Thread() {public void run() {System.out.println("thread run...");try {Thread.sleep(10);} catch (InterruptedException e) {}System.out.println("thread end.");}};t.start();try {Thread.sleep(20);} catch (InterruptedException e) {}System.out.println("main end...");} }sleep()傳入的參數(shù)是毫秒。調(diào)整暫停時間的大小,我們可以看到main線程和t線程執(zhí)行的先后順序。
要特別注意:直接調(diào)用Thread實例的run()方法是無效的:
public class Main {public static void main(String[] args) {Thread t = new MyThread();t.run();} }class MyThread extends Thread {public void run() {System.out.println("hello");} }直接調(diào)用run()方法,相當于調(diào)用了一個普通的Java方法,當前線程并沒有任何改變,也不會啟動新線程。上述代碼實際上是在main()方法內(nèi)部又調(diào)用了run()方法,打印hello語句是在main線程中執(zhí)行的,沒有任何新線程被創(chuàng)建。
必須調(diào)用Thread實例的start()方法才能啟動新線程,如果我們查看Thread類的源代碼,會看到start()方法內(nèi)部調(diào)用了一個private native void start0()方法,native修飾符表示這個方法是由JVM虛擬機內(nèi)部的C代碼實現(xiàn)的,不是由Java代碼實現(xiàn)的。
線程的優(yōu)先級
可以對線程設(shè)定優(yōu)先級,設(shè)定優(yōu)先級的方法是:
Thread.setPriority(int n) // 1~10, 默認值5優(yōu)先級高的線程被操作系統(tǒng)調(diào)度的優(yōu)先級較高,操作系統(tǒng)對高優(yōu)先級線程可能調(diào)度更頻繁,但我們決不能通過設(shè)置優(yōu)先級來確保高優(yōu)先級的線程一定會先執(zhí)行。
練習(xí)
從[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-LiunirUz-1640516280422)(data:image/png;base64,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)]下載練習(xí):創(chuàng)建新線程 (推薦使用IDE練習(xí)插件快速下載)
小結(jié)
Java用Thread對象表示一個線程,通過調(diào)用start()啟動一個新線程;
一個線程對象只能調(diào)用一次start()方法;
線程的執(zhí)行代碼寫在run()方法中;
線程調(diào)度由操作系統(tǒng)決定,程序本身無法決定調(diào)度順序;
Thread.sleep()可以把當前線程暫停一段時間。
線程的狀態(tài)
在Java程序中,一個線程對象只能調(diào)用一次start()方法啟動新線程,并在新線程中執(zhí)行run()方法。一旦run()方法執(zhí)行完畢,線程就結(jié)束了。因此,Java線程的狀態(tài)有以下幾種:
- New:新創(chuàng)建的線程,尚未執(zhí)行;
- Runnable:運行中的線程,正在執(zhí)行run()方法的Java代碼;
- Blocked:運行中的線程,因為某些操作被阻塞而掛起;
- Waiting:運行中的線程,因為某些操作在等待中;
- Timed Waiting:運行中的線程,因為執(zhí)行sleep()方法正在計時等待;
- Terminated:線程已終止,因為run()方法執(zhí)行完畢。
用一個狀態(tài)轉(zhuǎn)移圖表示如下:
┌─────────────┐│ New │└─────────────┘│▼ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐┌─────────────┐ ┌─────────────┐ ││ Runnable │ │ Blocked ││└─────────────┘ └─────────────┘ │┌─────────────┐ ┌─────────────┐││ Waiting │ │Timed Waiting│ │└─────────────┘ └─────────────┘│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│▼┌─────────────┐│ Terminated │└─────────────┘當線程啟動后,它可以在Runnable、Blocked、Waiting和Timed Waiting這幾個狀態(tài)之間切換,直到最后變成Terminated狀態(tài),線程終止。
線程終止的原因有:
- 線程正常終止:run()方法執(zhí)行到return語句返回;
- 線程意外終止:run()方法因為未捕獲的異常導(dǎo)致線程終止;
- 對某個線程的Thread實例調(diào)用stop()方法強制終止(強烈不推薦使用)。
一個線程還可以等待另一個線程直到其運行結(jié)束。例如,main線程在啟動t線程后,可以通過t.join()等待t線程結(jié)束后再繼續(xù)運行:
// 多線程 Run
public class Main {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {System.out.println("hello");});System.out.println("start");t.start();t.join();System.out.println("end");} }當main線程對線程對象t調(diào)用join()方法時,主線程將等待變量t表示的線程運行結(jié)束,即join就是指等待該線程結(jié)束,然后才繼續(xù)往下執(zhí)行自身線程。所以,上述代碼打印順序可以肯定是main線程先打印start,t線程再打印hello,main線程最后再打印end。
如果t線程已經(jīng)結(jié)束,對實例t調(diào)用join()會立刻返回。此外,join(long)的重載方法也可以指定一個等待時間,超過等待時間后就不再繼續(xù)等待。
小結(jié)
Java線程對象Thread的狀態(tài)包括:New、Runnable、Blocked、Waiting、Timed Waiting和Terminated;
通過對另一個線程對象調(diào)用join()方法可以等待其執(zhí)行結(jié)束;
可以指定等待時間,超過等待時間線程仍然沒有結(jié)束就不再等待;
對已經(jīng)運行結(jié)束的線程調(diào)用join()方法會立刻返回。
中斷線程
如果線程需要執(zhí)行一個長時間任務(wù),就可能需要能中斷線程。中斷線程就是其他線程給該線程發(fā)一個信號,該線程收到信號后結(jié)束執(zhí)行run()方法,使得自身線程能立刻結(jié)束運行。
我們舉個栗子:假設(shè)從網(wǎng)絡(luò)下載一個100M的文件,如果網(wǎng)速很慢,用戶等得不耐煩,就可能在下載過程中點“取消”,這時,程序就需要中斷下載線程的執(zhí)行。
中斷一個線程非常簡單,只需要在其他線程中對目標線程調(diào)用interrupt()方法,目標線程需要反復(fù)檢測自身狀態(tài)是否是interrupted狀態(tài),如果是,就立刻結(jié)束運行。
我們還是看示例代碼:
// 中斷線程 Run
public class Main {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread();t.start();Thread.sleep(1); // 暫停1毫秒t.interrupt(); // 中斷t線程t.join(); // 等待t線程結(jié)束System.out.println("end");} }class MyThread extends Thread {public void run() {int n = 0;while (! isInterrupted()) {n ++;System.out.println(n + " hello!");}} }仔細看上述代碼,main線程通過調(diào)用t.interrupt()方法中斷t線程,但是要注意,interrupt()方法僅僅向t線程發(fā)出了“中斷請求”,至于t線程是否能立刻響應(yīng),要看具體代碼。而t線程的while循環(huán)會檢測isInterrupted(),所以上述代碼能正確響應(yīng)interrupt()請求,使得自身立刻結(jié)束運行run()方法。
如果線程處于等待狀態(tài),例如,t.join()會讓main線程進入等待狀態(tài),此時,如果對main線程調(diào)用interrupt(),join()方法會立刻拋出InterruptedException,因此,目標線程只要捕獲到j(luò)oin()方法拋出的InterruptedException,就說明有其他線程對其調(diào)用了interrupt()方法,通常情況下該線程應(yīng)該立刻結(jié)束運行。
我們來看下面的示例代碼:
// 中斷線程 Run
public class Main {public static void main(String[] args) throws InterruptedException {Thread t = new MyThread();t.start();Thread.sleep(1000);t.interrupt(); // 中斷t線程t.join(); // 等待t線程結(jié)束System.out.println("end");} }class MyThread extends Thread {public void run() {Thread hello = new HelloThread();hello.start(); // 啟動hello線程try {hello.join(); // 等待hello線程結(jié)束} catch (InterruptedException e) {System.out.println("interrupted!");}hello.interrupt();} }class HelloThread extends Thread {public void run() {int n = 0;while (!isInterrupted()) {n++;System.out.println(n + " hello!");try {Thread.sleep(100);} catch (InterruptedException e) {break;}}} }main線程通過調(diào)用t.interrupt()從而通知t線程中斷,而此時t線程正位于hello.join()的等待中,此方法會立刻結(jié)束等待并拋出InterruptedException。由于我們在t線程中捕獲了InterruptedException,因此,就可以準備結(jié)束該線程。在t線程結(jié)束前,對hello線程也進行了interrupt()調(diào)用通知其中斷。如果去掉這一行代碼,可以發(fā)現(xiàn)hello線程仍然會繼續(xù)運行,且JVM不會退出。
另一個常用的中斷線程的方法是設(shè)置標志位。我們通常會用一個running標志位來標識線程是否應(yīng)該繼續(xù)運行,在外部線程中,通過把HelloThread.running置為false,就可以讓線程結(jié)束:
// 中斷線程 Run
public class Main {public static void main(String[] args) throws InterruptedException {HelloThread t = new HelloThread();t.start();Thread.sleep(1);t.running = false; // 標志位置為false} }class HelloThread extends Thread {public volatile boolean running = true;public void run() {int n = 0;while (running) {n ++;System.out.println(n + " hello!");}System.out.println("end!");} }注意到HelloThread的標志位boolean running是一個線程間共享的變量。線程間共享變量需要使用volatile關(guān)鍵字標記,確保每個線程都能讀取到更新后的變量值。
為什么要對線程間共享的變量用關(guān)鍵字volatile聲明?這涉及到Java的內(nèi)存模型。在Java虛擬機中,變量的值保存在主內(nèi)存中,但是,當線程訪問變量時,它會先獲取一個副本,并保存在自己的工作內(nèi)存中。如果線程修改了變量的值,虛擬機會在某個時刻把修改后的值回寫到主內(nèi)存,但是,這個時間是不確定的!
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐Main Memory │ │┌───────┐┌───────┐┌───────┐ │ │ var A ││ var B ││ var C │ │└───────┘└───────┘└───────┘ │ │ ▲ │ ▲ │─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─│ │ │ │ ┌ ─ ─ ┼ ┼ ─ ─ ┐ ┌ ─ ─ ┼ ┼ ─ ─ ┐▼ │ ▼ │ │ ┌───────┐ │ │ ┌───────┐ ││ var A │ │ var C │ │ └───────┘ │ │ └───────┘ │Thread 1 Thread 2 └ ─ ─ ─ ─ ─ ─ ┘ └ ─ ─ ─ ─ ─ ─ ┘這會導(dǎo)致如果一個線程更新了某個變量,另一個線程讀取的值可能還是更新前的。例如,主內(nèi)存的變量a = true,線程1執(zhí)行a = false時,它在此刻僅僅是把變量a的副本變成了false,主內(nèi)存的變量a還是true,在JVM把修改后的a回寫到主內(nèi)存之前,其他線程讀取到的a的值仍然是true,這就造成了多線程之間共享的變量不一致。
因此,volatile關(guān)鍵字的目的是告訴虛擬機:
- 每次訪問變量時,總是獲取主內(nèi)存的最新值;
- 每次修改變量后,立刻回寫到主內(nèi)存。
volatile關(guān)鍵字解決的是可見性問題:當一個線程修改了某個共享變量的值,其他線程能夠立刻看到修改后的值。
如果我們?nèi)サ魐olatile關(guān)鍵字,運行上述程序,發(fā)現(xiàn)效果和帶volatile差不多,這是因為在x86的架構(gòu)下,JVM回寫主內(nèi)存的速度非常快,但是,換成ARM的架構(gòu),就會有顯著的延遲。
小結(jié)
對目標線程調(diào)用interrupt()方法可以請求中斷一個線程,目標線程通過檢測isInterrupted()標志獲取自身是否已中斷。如果目標線程處于等待狀態(tài),該線程會捕獲到InterruptedException;
目標線程檢測到isInterrupted()為true或者捕獲了InterruptedException都應(yīng)該立刻結(jié)束自身線程;
通過標志位判斷需要正確使用volatile關(guān)鍵字;
volatile關(guān)鍵字解決了共享變量在線程間的可見性問題。
守護線程
Java程序入口就是由JVM啟動main線程,main線程又可以啟動其他線程。當所有線程都運行結(jié)束時,JVM退出,進程結(jié)束。
如果有一個線程沒有退出,JVM進程就不會退出。所以,必須保證所有線程都能及時結(jié)束。
但是有一種線程的目的就是無限循環(huán),例如,一個定時觸發(fā)任務(wù)的線程:
class TimerThread extends Thread {@Overridepublic void run() {while (true) {System.out.println(LocalTime.now());try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}} }如果這個線程不結(jié)束,JVM進程就無法結(jié)束。問題是,由誰負責(zé)結(jié)束這個線程?
然而這類線程經(jīng)常沒有負責(zé)人來負責(zé)結(jié)束它們。但是,當其他線程結(jié)束時,JVM進程又必須要結(jié)束,怎么辦?
答案是使用守護線程(Daemon Thread)。
守護線程是指為其他線程服務(wù)的線程。在JVM中,所有非守護線程都執(zhí)行完畢后,無論有沒有守護線程,虛擬機都會自動退出。
因此,JVM退出時,不必關(guān)心守護線程是否已結(jié)束。
如何創(chuàng)建守護線程呢?方法和普通線程一樣,只是在調(diào)用start()方法前,調(diào)用setDaemon(true)把該線程標記為守護線程:
Thread t = new MyThread(); t.setDaemon(true); t.start();在守護線程中,編寫代碼要注意:守護線程不能持有任何需要關(guān)閉的資源,例如打開文件等,因為虛擬機退出時,守護線程沒有任何機會來關(guān)閉文件,這會導(dǎo)致數(shù)據(jù)丟失。
練習(xí)
從[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-b30HBfgd-1640516280422)(data:image/png;base64,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)]下載練習(xí):使用守護線程 (推薦使用IDE練習(xí)插件快速下載)
小結(jié)
守護線程是為其他線程服務(wù)的線程;
所有非守護線程都執(zhí)行完畢后,虛擬機退出;
守護線程不能持有需要關(guān)閉的資源(如打開文件等)。
線程同步
當多個線程同時運行時,線程的調(diào)度由操作系統(tǒng)決定,程序本身無法決定。因此,任何一個線程都有可能在任何指令處被操作系統(tǒng)暫停,然后在某個時間段后繼續(xù)執(zhí)行。
這個時候,有個單線程模型下不存在的問題就來了:如果多個線程同時讀寫共享變量,會出現(xiàn)數(shù)據(jù)不一致的問題。
我們來看一個例子:
// 多線程 Run
public class Main {public static void main(String[] args) throws Exception {var add = new AddThread();var dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);} }class Counter {public static int count = 0; }class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count += 1; }} }class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) { Counter.count -= 1; }} }上面的代碼很簡單,兩個線程同時對一個int變量進行操作,一個加10000次,一個減10000次,最后結(jié)果應(yīng)該是0,但是,每次運行,結(jié)果實際上都是不一樣的。
這是因為對變量進行讀取和寫入時,結(jié)果要正確,必須保證是原子操作。原子操作是指不能被中斷的一個或一系列操作。
例如,對于語句:
n = n + 1;看上去是一行語句,實際上對應(yīng)了3條指令:
ILOAD IADD ISTORE我們假設(shè)n的值是100,如果兩個線程同時執(zhí)行n = n + 1,得到的結(jié)果很可能不是102,而是101,原因在于:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘│ ││ILOAD (100) ││ │ILOAD (100)│ │IADD│ │ISTORE (101)│IADD ││ISTORE (101)│▼ ▼如果線程1在執(zhí)行ILOAD后被操作系統(tǒng)中斷,此刻如果線程2被調(diào)度執(zhí)行,它執(zhí)行ILOAD后獲取的值仍然是100,最終結(jié)果被兩個線程的ISTORE寫入后變成了101,而不是期待的102。
這說明多線程模型下,要保證邏輯正確,對共享變量進行讀寫時,必須保證一組指令以原子方式執(zhí)行:即某一個線程執(zhí)行時,其他線程必須等待:
┌───────┐ ┌───────┐ │Thread1│ │Thread2│ └───┬───┘ └───┬───┘│ ││-- lock -- ││ILOAD (100) ││IADD ││ISTORE (101) ││-- unlock -- ││ │-- lock --│ │ILOAD (101)│ │IADD│ │ISTORE (102)│ │-- unlock --▼ ▼通過加鎖和解鎖的操作,就能保證3條指令總是在一個線程執(zhí)行期間,不會有其他線程會進入此指令區(qū)間。即使在執(zhí)行期線程被操作系統(tǒng)中斷執(zhí)行,其他線程也會因為無法獲得鎖導(dǎo)致無法進入此指令區(qū)間。只有執(zhí)行線程將鎖釋放后,其他線程才有機會獲得鎖并執(zhí)行。這種加鎖和解鎖之間的代碼塊我們稱之為臨界區(qū)(Critical Section),任何時候臨界區(qū)最多只有一個線程能執(zhí)行。
可見,保證一段代碼的原子性就是通過加鎖和解鎖實現(xiàn)的。Java程序使用synchronized關(guān)鍵字對一個對象進行加鎖:
synchronized(lock) {n = n + 1; }synchronized保證了代碼塊在任意時刻最多只有一個線程能執(zhí)行。我們把上面的代碼用synchronized改寫如下:
// 多線程 Run
public class Main {public static void main(String[] args) throws Exception {var add = new AddThread();var dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);} }class Counter {public static final Object lock = new Object();public static int count = 0; }class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count += 1;}}} }class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.count -= 1;}}} }注意到代碼:
synchronized(Counter.lock) { // 獲取鎖... } // 釋放鎖它表示用Counter.lock實例作為鎖,兩個線程在執(zhí)行各自的synchronized(Counter.lock) { ... }代碼塊時,必須先獲得鎖,才能進入代碼塊進行。執(zhí)行結(jié)束后,在synchronized語句塊結(jié)束會自動釋放鎖。這樣一來,對Counter.count變量進行讀寫就不可能同時進行。上述代碼無論運行多少次,最終結(jié)果都是0。
使用synchronized解決了多線程同步訪問共享變量的正確性問題。但是,它的缺點是帶來了性能下降。因為synchronized代碼塊無法并發(fā)執(zhí)行。此外,加鎖和解鎖需要消耗一定的時間,所以,synchronized會降低程序的執(zhí)行效率。
我們來概括一下如何使用synchronized:
在使用synchronized的時候,不必擔(dān)心拋出異常。因為無論是否有異常,都會在synchronized結(jié)束處正確釋放鎖:
public void add(int m) {synchronized (obj) {if (m < 0) {throw new RuntimeException();}this.value += m;} // 無論有無異常,都會在此釋放鎖 }我們再來看一個錯誤使用synchronized的例子:
// 多線程 Run
public class Main {public static void main(String[] args) throws Exception {var add = new AddThread();var dec = new DecThread();add.start();dec.start();add.join();dec.join();System.out.println(Counter.count);} }class Counter {public static final Object lock1 = new Object();public static final Object lock2 = new Object();public static int count = 0; }class AddThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock1) {Counter.count += 1;}}} }class DecThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock2) {Counter.count -= 1;}}} }結(jié)果并不是0,這是因為兩個線程各自的synchronized鎖住的不是同一個對象!這使得兩個線程各自都可以同時獲得鎖:因為JVM只保證同一個鎖在任意時刻只能被一個線程獲取,但兩個不同的鎖在同一時刻可以被兩個線程分別獲取。
因此,使用synchronized的時候,獲取到的是哪個鎖非常重要。鎖對象如果不對,代碼邏輯就不對。
我們再看一個例子:
// 多線程 Run
public class Main {public static void main(String[] args) throws Exception {var ts = new Thread[] { new AddStudentThread(), new DecStudentThread(), new AddTeacherThread(), new DecTeacherThread() };for (var t : ts) {t.start();}for (var t : ts) {t.join();}System.out.println(Counter.studentCount);System.out.println(Counter.teacherCount);} }class Counter {public static final Object lock = new Object();public static int studentCount = 0;public static int teacherCount = 0; }class AddStudentThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.studentCount += 1;}}} }class DecStudentThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.studentCount -= 1;}}} }class AddTeacherThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.teacherCount += 1;}}} }class DecTeacherThread extends Thread {public void run() {for (int i=0; i<10000; i++) {synchronized(Counter.lock) {Counter.teacherCount -= 1;}}} }上述代碼的4個線程對兩個共享變量分別進行讀寫操作,但是使用的鎖都是Counter.lock這一個對象,這就造成了原本可以并發(fā)執(zhí)行的Counter.studentCount += 1和Counter.teacherCount += 1,現(xiàn)在無法并發(fā)執(zhí)行了,執(zhí)行效率大大降低。實際上,需要同步的線程可以分成兩組:AddStudentThread和DecStudentThread,AddTeacherThread和DecTeacherThread,組之間不存在競爭,因此,應(yīng)該使用兩個不同的鎖,即:
AddStudentThread和DecStudentThread使用lockStudent鎖:
synchronized(Counter.lockStudent) {... }AddTeacherThread和DecTeacherThread使用lockTeacher鎖:
synchronized(Counter.lockTeacher) {... }這樣才能最大化地提高執(zhí)行效率。
不需要synchronized的操作
JVM規(guī)范定義了幾種原子操作:
- 基本類型(long和double除外)賦值,例如:int n = m;
- 引用類型賦值,例如:List<String> list = anotherList。
long和double是64位數(shù)據(jù),JVM沒有明確規(guī)定64位賦值操作是不是一個原子操作,不過在x64平臺的JVM是把long和double的賦值作為原子操作實現(xiàn)的。
單條原子操作的語句不需要同步。例如:
public void set(int m) {synchronized(lock) {this.value = m;} }就不需要同步。
對引用也是類似。例如:
public void set(String s) {this.value = s; }上述賦值語句并不需要同步。
但是,如果是多行賦值語句,就必須保證是同步操作,例如:
class Pair {int first;int last;public void set(int first, int last) {synchronized(this) {this.first = first;this.last = last;}} }有些時候,通過一些巧妙的轉(zhuǎn)換,可以把非原子操作變?yōu)樵硬僮鳌@?#xff0c;上述代碼如果改造成:
class Pair {int[] pair;public void set(int first, int last) {int[] ps = new int[] { first, last };this.pair = ps;} }就不再需要同步,因為this.pair = ps是引用賦值的原子操作。而語句:
int[] ps = new int[] { first, last };這里的ps是方法內(nèi)部定義的局部變量,每個線程都會有各自的局部變量,互不影響,并且互不可見,并不需要同步。
小結(jié)
多線程同時讀寫共享變量時,會造成邏輯錯誤,因此需要通過synchronized同步;
同步的本質(zhì)就是給指定對象加鎖,加鎖后才能繼續(xù)執(zhí)行后續(xù)代碼;
注意加鎖對象必須是同一個實例;
對JVM定義的單個原子操作不需要同步。
同步方法
我們知道Java程序依靠synchronized對線程進行同步,使用synchronized的時候,鎖住的是哪個對象非常重要。
讓線程自己選擇鎖對象往往會使得代碼邏輯混亂,也不利于封裝。更好的方法是把synchronized邏輯封裝起來。例如,我們編寫一個計數(shù)器如下:
public class Counter {private int count = 0;public void add(int n) {synchronized(this) {count += n;}}public void dec(int n) {synchronized(this) {count -= n;}}public int get() {return count;} }這樣一來,線程調(diào)用add()、dec()方法時,它不必關(guān)心同步邏輯,因為synchronized代碼塊在add()、dec()方法內(nèi)部。并且,我們注意到,synchronized鎖住的對象是this,即當前實例,這又使得創(chuàng)建多個Counter實例的時候,它們之間互不影響,可以并發(fā)執(zhí)行:
var c1 = Counter(); var c2 = Counter();// 對c1進行操作的線程: new Thread(() -> {c1.add(); }).start(); new Thread(() -> {c1.dec(); }).start();// 對c2進行操作的線程: new Thread(() -> {c2.add(); }).start(); new Thread(() -> {c2.dec(); }).start();現(xiàn)在,對于Counter類,多線程可以正確調(diào)用。
如果一個類被設(shè)計為允許多線程正確訪問,我們就說這個類就是“線程安全”的(thread-safe),上面的Counter類就是線程安全的。Java標準庫的java.lang.StringBuffer也是線程安全的。
還有一些不變類,例如String,Integer,LocalDate,它們的所有成員變量都是final,多線程同時訪問時只能讀不能寫,這些不變類也是線程安全的。
最后,類似Math這些只提供靜態(tài)方法,沒有成員變量的類,也是線程安全的。
除了上述幾種少數(shù)情況,大部分類,例如ArrayList,都是非線程安全的類,我們不能在多線程中修改它們。但是,如果所有線程都只讀取,不寫入,那么ArrayList是可以安全地在線程間共享的。
沒有特殊說明時,一個類默認是非線程安全的。
我們再觀察Counter的代碼:
public class Counter {public void add(int n) {synchronized(this) {count += n;}}... }當我們鎖住的是this實例時,實際上可以用synchronized修飾這個方法。下面兩種寫法是等價的:
public void add(int n) {synchronized(this) { // 鎖住thiscount += n;} // 解鎖 } public synchronized void add(int n) { // 鎖住thiscount += n; } // 解鎖因此,用synchronized修飾的方法就是同步方法,它表示整個方法都必須用this實例加鎖。
我們再思考一下,如果對一個靜態(tài)方法添加synchronized修飾符,它鎖住的是哪個對象?
public synchronized static void test(int n) {... }對于static方法,是沒有this實例的,因為static方法是針對類而不是實例。但是我們注意到任何一個類都有一個由JVM自動創(chuàng)建的Class實例,因此,對static方法添加synchronized,鎖住的是該類的Class實例。上述synchronized static方法實際上相當于:
public class Counter {public static void test(int n) {synchronized(Counter.class) {...}} }我們再考察Counter的get()方法:
public class Counter {private int count;public int get() {return count;}... }它沒有同步,因為讀一個int變量不需要同步。
然而,如果我們把代碼稍微改一下,返回一個包含兩個int的對象:
public class Counter {private int first;private int last;public Pair get() {Pair p = new Pair();p.first = first;p.last = last;return p;}... }就必須要同步了。
小結(jié)
用synchronized修飾方法可以把整個方法變?yōu)橥酱a塊,synchronized方法加鎖對象是this;
通過合理的設(shè)計和數(shù)據(jù)封裝可以讓一個類變?yōu)椤熬€程安全”;
一個類沒有特殊說明,默認不是thread-safe;
多線程能否安全訪問某個非線程安全的實例,需要具體問題具體分析。
死鎖
Java的線程鎖是可重入的鎖。
什么是可重入的鎖?我們還是來看例子:
public class Counter {private int count = 0;public synchronized void add(int n) {if (n < 0) {dec(-n);} else {count += n;}}public synchronized void dec(int n) {count += n;} }觀察synchronized修飾的add()方法,一旦線程執(zhí)行到add()方法內(nèi)部,說明它已經(jīng)獲取了當前實例的this鎖。如果傳入的n < 0,將在add()方法內(nèi)部調(diào)用dec()方法。由于dec()方法也需要獲取this鎖,現(xiàn)在問題來了:
對同一個線程,能否在獲取到鎖以后繼續(xù)獲取同一個鎖?
答案是肯定的。JVM允許同一個線程重復(fù)獲取同一個鎖,這種能被同一個線程反復(fù)獲取的鎖,就叫做可重入鎖。
由于Java的線程鎖是可重入鎖,所以,獲取鎖的時候,不但要判斷是否是第一次獲取,還要記錄這是第幾次獲取。每獲取一次鎖,記錄+1,每退出synchronized塊,記錄-1,減到0的時候,才會真正釋放鎖。
死鎖
一個線程可以獲取一個鎖后,再繼續(xù)獲取另一個鎖。例如:
public void add(int m) {synchronized(lockA) { // 獲得lockA的鎖this.value += m;synchronized(lockB) { // 獲得lockB的鎖this.another += m;} // 釋放lockB的鎖} // 釋放lockA的鎖 }public void dec(int m) {synchronized(lockB) { // 獲得lockB的鎖this.another -= m;synchronized(lockA) { // 獲得lockA的鎖this.value -= m;} // 釋放lockA的鎖} // 釋放lockB的鎖 }在獲取多個鎖的時候,不同線程獲取多個不同對象的鎖可能導(dǎo)致死鎖。對于上述代碼,線程1和線程2如果分別執(zhí)行add()和dec()方法時:
- 線程1:進入add(),獲得lockA;
- 線程2:進入dec(),獲得lockB。
隨后:
- 線程1:準備獲得lockB,失敗,等待中;
- 線程2:準備獲得lockA,失敗,等待中。
此時,兩個線程各自持有不同的鎖,然后各自試圖獲取對方手里的鎖,造成了雙方無限等待下去,這就是死鎖。
死鎖發(fā)生后,沒有任何機制能解除死鎖,只能強制結(jié)束JVM進程。
因此,在編寫多線程應(yīng)用時,要特別注意防止死鎖。因為死鎖一旦形成,就只能強制結(jié)束進程。
那么我們應(yīng)該如何避免死鎖呢?答案是:線程獲取鎖的順序要一致。即嚴格按照先獲取lockA,再獲取lockB的順序,改寫dec()方法如下:
public void dec(int m) {synchronized(lockA) { // 獲得lockA的鎖this.value -= m;synchronized(lockB) { // 獲得lockB的鎖this.another -= m;} // 釋放lockB的鎖} // 釋放lockA的鎖 }練習(xí)
請觀察死鎖的代碼輸出,然后修復(fù)。
從[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-fJuyXAwN-1640516280423)(data:image/png;base64,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)]下載練習(xí):死鎖 (推薦使用IDE練習(xí)插件快速下載)
小結(jié)
Java的synchronized鎖是可重入鎖;
死鎖產(chǎn)生的條件是多線程各自持有不同的鎖,并互相試圖獲取對方已持有的鎖,導(dǎo)致無限等待;
避免死鎖的方法是多線程獲取鎖的順序要一致。
總結(jié)
- 上一篇: leetcode -39组合总数
- 下一篇: MATLAB统计与回归