Java的基本使用之多線(xiàn)程
1、多線(xiàn)程的基本介紹
現(xiàn)代操作系統(tǒng)(Windows,macOS,Linux)都可以執(zhí)行多任務(wù),多任務(wù)就是同時(shí)運(yùn)行多個(gè)任務(wù)。
現(xiàn)在,多核CPU已經(jīng)非常普及了,但是,即使過(guò)去的單核CPU,也可以執(zhí)行多任務(wù)。由于CPU執(zhí)行代碼都是順序執(zhí)行的,操作系統(tǒng)輪流讓各個(gè)任務(wù)交替執(zhí)行,任務(wù)1執(zhí)行0.01秒,切換到任務(wù)2,任務(wù)2執(zhí)行0.01秒,再切換到任務(wù)3,執(zhí)行0.01秒……這樣反復(fù)執(zhí)行下去。表面上看,每個(gè)任務(wù)都是交替執(zhí)行的,但是,由于CPU的執(zhí)行速度實(shí)在是太快了,我們感覺(jué)就像所有任務(wù)都在同時(shí)執(zhí)行一樣。
真正的并行執(zhí)行多任務(wù)只能在多核CPU上實(shí)現(xiàn),但是,由于任務(wù)數(shù)量遠(yuǎn)遠(yuǎn)多于CPU的核心數(shù)量,所以,操作系統(tǒng)也會(huì)自動(dòng)把很多任務(wù)輪流調(diào)度到每個(gè)核心上執(zhí)行。
1.1、進(jìn)程和線(xiàn)程的概念
在計(jì)算機(jī)中,我們把一個(gè)任務(wù)稱(chēng)為一個(gè)進(jìn)程,瀏覽器就是一個(gè)進(jìn)程,視頻播放器是另一個(gè)進(jìn)程,類(lèi)似的,音樂(lè)播放器和Word都是進(jìn)程。某些進(jìn)程內(nèi)部還需要同時(shí)執(zhí)行多個(gè)子任務(wù),我們把子任務(wù)稱(chēng)為線(xiàn)程。由于每個(gè)進(jìn)程至少要干一件事,所以,一個(gè)進(jìn)程至少有一個(gè)線(xiàn)程。
進(jìn)程和線(xiàn)程的關(guān)系就是:一個(gè)進(jìn)程可以包含一個(gè)或多個(gè)線(xiàn)程,但至少會(huì)有一個(gè)線(xiàn)程。 操作系統(tǒng)調(diào)度的最小任務(wù)單位其實(shí)不是進(jìn)程,而是線(xiàn)程。常用的Windows、Linux等操作系統(tǒng)都采用搶占式多任務(wù),如何調(diào)度線(xiàn)程完全由操作系統(tǒng)決定,程序自己不能決定什么時(shí)候執(zhí)行,以及執(zhí)行多長(zhǎng)時(shí)間。

1.2、實(shí)現(xiàn)多任務(wù)的方式(多進(jìn)程、多線(xiàn)程、多進(jìn)程+多線(xiàn)程)
因?yàn)橥粋€(gè)應(yīng)用程序,既可以有多個(gè)進(jìn)程,也可以有多個(gè)線(xiàn)程,因此,實(shí)現(xiàn)多任務(wù)的方法,有以下幾種:
1)多進(jìn)程模式(每個(gè)進(jìn)程只有一個(gè)線(xiàn)程):

2) 多線(xiàn)程模式(一個(gè)進(jìn)程有多個(gè)線(xiàn)程):

3)多進(jìn)程+多線(xiàn)程模式(復(fù)雜度最高):

1.2.1、多進(jìn)程和多線(xiàn)程的對(duì)比
進(jìn)程和線(xiàn)程是包含關(guān)系,但是多任務(wù)既可以由多進(jìn)程實(shí)現(xiàn),也可以由單進(jìn)程內(nèi)的多線(xiàn)程實(shí)現(xiàn),還可以混合多進(jìn)程+多線(xiàn)程。
具體采用哪種方式,要考慮到進(jìn)程和線(xiàn)程的特點(diǎn)。
和多線(xiàn)程相比,多進(jìn)程的缺點(diǎn)在于:
- 創(chuàng)建進(jìn)程比創(chuàng)建線(xiàn)程開(kāi)銷(xiāo)大,尤其是在Windows系統(tǒng)上;
- 進(jìn)程間通信比線(xiàn)程間通信要慢,因?yàn)榫€(xiàn)程間通信就是讀寫(xiě)同一個(gè)變量,速度很快。
而多進(jìn)程的優(yōu)點(diǎn)在于:
多進(jìn)程穩(wěn)定性比多線(xiàn)程高,因?yàn)樵诙噙M(jìn)程的情況下,一個(gè)進(jìn)程崩潰不會(huì)影響其他進(jìn)程,而在多線(xiàn)程的情況下,任何一個(gè)線(xiàn)程崩潰會(huì)直接導(dǎo)致整個(gè)進(jìn)程崩潰。
1.3、Java程序中的多線(xiàn)程
Java語(yǔ)言?xún)?nèi)置了多線(xiàn)程支持:一個(gè)Java程序?qū)嶋H上是一個(gè)JVM進(jìn)程,JVM進(jìn)程用一個(gè)主線(xiàn)程來(lái)執(zhí)行main()方法,在main()方法內(nèi)部,我們又可以啟動(dòng)多個(gè)線(xiàn)程。此外,JVM還有負(fù)責(zé)垃圾回收的其他工作線(xiàn)程等。
因此,對(duì)于大多數(shù)Java程序來(lái)說(shuō),我們說(shuō)多任務(wù),實(shí)際上是說(shuō)如何使用多線(xiàn)程實(shí)現(xiàn)多任務(wù)。
和單線(xiàn)程相比,多線(xiàn)程編程的特點(diǎn)在于:多線(xiàn)程經(jīng)常需要讀寫(xiě)共享數(shù)據(jù),并且需要同步。例如,播放電影時(shí),就必須由一個(gè)線(xiàn)程播放視頻,另一個(gè)線(xiàn)程播放音頻,兩個(gè)線(xiàn)程需要協(xié)調(diào)運(yùn)行,否則畫(huà)面和聲音就不同步。因此,多線(xiàn)程編程的復(fù)雜度高,調(diào)試更困難。
Java多線(xiàn)程編程的特點(diǎn)又在于:
- 多線(xiàn)程模型是Java程序最基本的并發(fā)模型;
- 后續(xù)讀寫(xiě)網(wǎng)絡(luò)、數(shù)據(jù)庫(kù)、Web開(kāi)發(fā)等都依賴(lài)Java多線(xiàn)程模型。
掌握J(rèn)ava多線(xiàn)程編程是非常必要的
2、創(chuàng)建新線(xiàn)程
Java語(yǔ)言?xún)?nèi)置了多線(xiàn)程支持。當(dāng)Java程序啟動(dòng)的時(shí)候,實(shí)際上是啟動(dòng)了一個(gè)JVM進(jìn)程,然后,JVM啟動(dòng)主線(xiàn)程來(lái)執(zhí)行main()方法。在main()方法中,我們又可以啟動(dòng)其他線(xiàn)程。
要?jiǎng)?chuàng)建一個(gè)新線(xiàn)程非常容易,我們需要先實(shí)例化一個(gè)Thread實(shí)例,然后調(diào)用它的start()方法,一個(gè)線(xiàn)程對(duì)象只能調(diào)用一次start()方法。
public class Main { public static void main(String[] args) { Thread t = new Thread(); t.start(); // 啟動(dòng)新線(xiàn)程 } }
上面那個(gè)線(xiàn)程啟動(dòng)后實(shí)際上什么也不做就立刻結(jié)束了。如果我們希望新線(xiàn)程能執(zhí)行指定的代碼,有以下幾種方法:
一、從Thread派生一個(gè)自定義類(lèi),然后覆寫(xiě)run()方法:
public class Main { public static void main(String[] args) { Thread t = new MyThread(); t.start(); // 啟動(dòng)新線(xiàn)程,調(diào)用實(shí)例的run()方法 } } class MyThread extends Thread { @Override public void run() { System.out.println("start new thread!"); } }
也可以像下面一樣創(chuàng)建一個(gè)Thread的匿名子類(lèi):
Thread thread = new Thread(){ public void run(){ System.out.println("Thread Running"); } }; thread.start();
二、創(chuàng)建Thread實(shí)例時(shí),傳入一個(gè)Runnable實(shí)例:
public class Main { public static void main(String[] args) { Thread t = new Thread(new MyRunnable()); t.start(); // 啟動(dòng)新線(xiàn)程 Thread t2 = new Thread(new MyRunnable(), "t2Name"); //在創(chuàng)建線(xiàn)程時(shí),可以給線(xiàn)程起名 t2.start(); // 啟動(dòng)新線(xiàn)程 } } class MyRunnable implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName()); //輸出線(xiàn)程名 System.out.println("start new thread!"); } } //用Java8引入的lambda語(yǔ)法可以簡(jiǎn)寫(xiě)為: public class Main { public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("start new thread!"); }); t.start(); // 啟動(dòng)新線(xiàn)程 } }
一般我們使用傳入Runnable實(shí)例的方式來(lái)創(chuàng)建線(xiàn)程。這種方式可以避免Java單繼承的局限性,因?yàn)榈谝环N方式繼承了Thread類(lèi)就無(wú)法繼承其他類(lèi)。并且多個(gè)線(xiàn)程可以共享同一個(gè)接口實(shí)現(xiàn)類(lèi)的對(duì)象,非常適合多個(gè)相同線(xiàn)程來(lái)處理同一份資源。比如下面,t 和 t2 線(xiàn)程將會(huì)共享count變量,count變量將會(huì)因?yàn)榱硪痪€(xiàn)程的操作而發(fā)生改變。
public class Main { public static void main(String[] args) { MyRunnable run = new MyRunnable(); Thread t = new Thread(run); //傳入同一個(gè)Runnable實(shí)例,多個(gè)線(xiàn)程共享同一份資源 Thread t2 = new Thread(run, "t2Name"); t.start(); t2.start(); } } class MyRunnable implements Runnable { int count = 0; @Override public void run() { for(int i=0; i<5; i++){ count++; } } }
要特別注意:直接調(diào)用Thread實(shí)例的run()方法是無(wú)效的。直接調(diào)用run()方法,只是相當(dāng)于 main() 方法里調(diào)用了一個(gè)普通的Java方法,不會(huì)啟動(dòng)新線(xiàn)程,當(dāng)前線(xiàn)程也不會(huì)發(fā)生任何改變。必須調(diào)用Thread實(shí)例的start()方法才能啟動(dòng)新線(xiàn)程,如果我們查看Thread類(lèi)的源代碼,會(huì)看到start()方法內(nèi)部調(diào)用了一個(gè)private native void start0()方法,native修飾符表示這個(gè)方法是由JVM虛擬機(jī)內(nèi)部的C代碼實(shí)現(xiàn)的,不是由Java代碼實(shí)現(xiàn)的。
2.1、線(xiàn)程的執(zhí)行順序
如下代碼:

我們用藍(lán)色表示主線(xiàn)程,也就是main線(xiàn)程,,紅色代表新線(xiàn)程。main線(xiàn)程執(zhí)行的代碼有4行,首先打印main start,然后創(chuàng)建Thread對(duì)象,緊接著調(diào)用start()啟動(dòng)新線(xiàn)程。當(dāng)start()方法被調(diào)用時(shí),JVM就創(chuàng)建了一個(gè)新線(xiàn)程,我們通過(guò)實(shí)例變量t來(lái)表示這個(gè)新線(xiàn)程對(duì)象,并開(kāi)始執(zhí)行。接著,main線(xiàn)程繼續(xù)執(zhí)行打印main end語(yǔ)句,而t線(xiàn)程在main線(xiàn)程執(zhí)行的同時(shí)會(huì)并發(fā)執(zhí)行,打印thread run和thread end語(yǔ)句。
當(dāng)run()方法結(jié)束時(shí),新線(xiàn)程就結(jié)束了。而main()方法結(jié)束時(shí),主線(xiàn)程也結(jié)束了。
我們?cè)賮?lái)看線(xiàn)程的執(zhí)行順序:
main線(xiàn)程肯定是先打印main start,再打印main end;t線(xiàn)程肯定是先打印thread run,再打印thread end。
但是,除了可以肯定,main start會(huì)先打印外,main end打印在thread run之前、thread end之后或者之間,都無(wú)法確定。因?yàn)閺?code>t線(xiàn)程開(kāi)始運(yùn)行以后,兩個(gè)線(xiàn)程就開(kāi)始同時(shí)運(yùn)行了,并且由操作系統(tǒng)調(diào)度,程序本身無(wú)法確定線(xiàn)程的調(diào)度順序。
線(xiàn)程調(diào)度由操作系統(tǒng)決定,程序本身無(wú)法決定調(diào)度順序。
2.2、設(shè)置線(xiàn)程的優(yōu)先級(jí)
可以對(duì)線(xiàn)程設(shè)定優(yōu)先級(jí),優(yōu)先級(jí)高的線(xiàn)程被操作系統(tǒng)調(diào)度的優(yōu)先級(jí)較高,操作系統(tǒng)對(duì)高優(yōu)先級(jí)線(xiàn)程可能調(diào)度更頻繁,但我們決不能通過(guò)設(shè)置優(yōu)先級(jí)來(lái)保證高優(yōu)先級(jí)的線(xiàn)程一定會(huì)先執(zhí)行。
設(shè)定優(yōu)先級(jí)的方法:
threadInstance.setPriority(int n) // 1~10, 默認(rèn)值5
可以通過(guò) threadObj.getPriority() 方法來(lái)獲取某個(gè)線(xiàn)程的優(yōu)先級(jí)
threadInstance.getPriority() //返回一個(gè)整型數(shù)據(jù)
3、線(xiàn)程的6種狀態(tài)及切換(New、Runnable、Blocked、Waiting、Timed Waiting、Terminated)
在Java程序中,一個(gè)線(xiàn)程對(duì)象只能調(diào)用一次start()方法啟動(dòng)新線(xiàn)程,并在新線(xiàn)程中執(zhí)行run()方法。一旦run()方法執(zhí)行完畢,線(xiàn)程就結(jié)束了。因此,Java線(xiàn)程的狀態(tài)有以下幾種:
- New(初始):新創(chuàng)建了一個(gè)線(xiàn)程對(duì)象,但還沒(méi)有調(diào)用start()方法。
- Runnable(運(yùn)行):Java線(xiàn)程中將就緒(ready)和運(yùn)行中(running)兩種狀態(tài)籠統(tǒng)的稱(chēng)為“運(yùn)行”。線(xiàn)程對(duì)象創(chuàng)建后,其他線(xiàn)程(比如main線(xiàn)程)調(diào)用了該對(duì)象的start()方法。該狀態(tài)的線(xiàn)程位于可運(yùn)行線(xiàn)程池中,具備了運(yùn)行的條件,等待被線(xiàn)程調(diào)度選中,獲取CPU的使用權(quán),此時(shí)處于就緒狀態(tài)(ready)。就緒狀態(tài)的線(xiàn)程在獲得CPU時(shí)間片后變?yōu)檫\(yùn)行中狀態(tài)(running)。
- Blocked(阻塞):表示線(xiàn)程阻塞于鎖。
- Waiting(等待):進(jìn)入該狀態(tài)的線(xiàn)程需要等待其他線(xiàn)程做出一些特定動(dòng)作(通知或中斷)。
- Timed Waiting(超時(shí)等待):該狀態(tài)不同于WAITING,它可以在指定的時(shí)間后自行返回。
- Terminated(終止):表示該線(xiàn)程已經(jīng)執(zhí)行完畢。
當(dāng)線(xiàn)程啟動(dòng)后,它可以在Runnable、Blocked、Waiting和Timed Waiting這幾個(gè)狀態(tài)之間切換,直到最后變成Terminated狀態(tài),線(xiàn)程終止。
線(xiàn)程終止的原因有:
- 線(xiàn)程正常終止:
run()方法執(zhí)行到return語(yǔ)句返回; - 線(xiàn)程意外終止:
run()方法因?yàn)槲床东@的異常導(dǎo)致線(xiàn)程終止,或者進(jìn)程被殺死,因?yàn)榫€(xiàn)程依賴(lài)于進(jìn)程運(yùn)行 - 對(duì)某個(gè)線(xiàn)程的
Thread實(shí)例調(diào)用stop()方法強(qiáng)制終止(強(qiáng)烈不推薦使用)。
用一個(gè)圖來(lái)表示線(xiàn)程狀態(tài)的轉(zhuǎn)移如下:

1)初始狀態(tài)(New)
實(shí)現(xiàn)Runnable接口和繼承Thread可以得到一個(gè)線(xiàn)程類(lèi),new一個(gè)實(shí)例出來(lái),線(xiàn)程就進(jìn)入了初始狀態(tài)。
2)就緒狀態(tài)(ready)
- 就緒狀態(tài)只是說(shuō)你有資格運(yùn)行,調(diào)度程序沒(méi)有挑選到你,你就永遠(yuǎn)是就緒狀態(tài)。
- 調(diào)用線(xiàn)程的start()方法,此線(xiàn)程進(jìn)入就緒狀態(tài)。
- 當(dāng)前線(xiàn)程sleep()方法結(jié)束,其他線(xiàn)程join()結(jié)束,等待用戶(hù)輸入完畢,某個(gè)線(xiàn)程拿到對(duì)象鎖,這些線(xiàn)程也將進(jìn)入就緒狀態(tài)。
- 當(dāng)前線(xiàn)程時(shí)間片用完了,調(diào)用當(dāng)前線(xiàn)程的yield()方法,當(dāng)前線(xiàn)程進(jìn)入就緒狀態(tài)。
- 鎖池里的線(xiàn)程拿到對(duì)象鎖后,進(jìn)入就緒狀態(tài)。
2.2)運(yùn)行中狀態(tài)(running)
線(xiàn)程調(diào)度程序從可運(yùn)行池中選擇一個(gè)線(xiàn)程作為當(dāng)前線(xiàn)程時(shí)線(xiàn)程所處的狀態(tài)。這也是線(xiàn)程進(jìn)入運(yùn)行狀態(tài)的唯一一種方式。
3)阻塞狀態(tài)(Blocked)
阻塞狀態(tài)是線(xiàn)程阻塞在進(jìn)入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時(shí)的狀態(tài)。
4)等待(Waiting)
處于這種狀態(tài)的線(xiàn)程不會(huì)被分配CPU執(zhí)行時(shí)間,它們要等待被顯式地喚醒,否則會(huì)處于無(wú)限期等待的狀態(tài)。
5)超時(shí)等待(Timed Waiting)
處于這種狀態(tài)的線(xiàn)程不會(huì)被分配CPU執(zhí)行時(shí)間,不過(guò)無(wú)須無(wú)限期等待被其他線(xiàn)程顯示地喚醒,在達(dá)到一定時(shí)間后它們會(huì)自動(dòng)喚醒。
6)終止?fàn)顟B(tài)(Terminated)
- 當(dāng)線(xiàn)程的run()方法完成時(shí),或者主線(xiàn)程的main()方法完成時(shí),我們就認(rèn)為它終止了。這個(gè)線(xiàn)程對(duì)象也許是活的,但是,它已經(jīng)不是一個(gè)單獨(dú)執(zhí)行的線(xiàn)程。線(xiàn)程一旦終止了,就不能復(fù)生。
- 在一個(gè)終止的線(xiàn)程上調(diào)用start()方法,會(huì)拋出java.lang.IllegalThreadStateException異常。
3.1、線(xiàn)程跟狀態(tài)相關(guān)的幾種方法(sleep()、yield()、join()、wait()、notify())
線(xiàn)程跟狀態(tài)相關(guān)的幾種方法如下:
1)Thread.sleep(long millis):當(dāng)前線(xiàn)程調(diào)用此方法,進(jìn)入TIMED_WAITING狀態(tài),但不釋放對(duì)象鎖,millis毫秒過(guò)后線(xiàn)程自動(dòng)蘇醒進(jìn)入就緒狀態(tài)。作用:給其它線(xiàn)程執(zhí)行機(jī)會(huì)的最佳方式。
2)Thread.yield():當(dāng)前線(xiàn)程調(diào)用此方法,放棄獲取的CPU時(shí)間片,但不釋放鎖資源,由運(yùn)行狀態(tài)變?yōu)榫途w狀態(tài),讓OS再次選擇線(xiàn)程。作用:讓相同優(yōu)先級(jí)的線(xiàn)程輪流執(zhí)行,但并不保證一定會(huì)輪流執(zhí)行。實(shí)際中無(wú)法保證yield()達(dá)到讓步目的,因?yàn)樽尣降木€(xiàn)程還有可能被線(xiàn)程調(diào)度程序再次選中。Thread.yield()不會(huì)導(dǎo)致阻塞。該方法與sleep()類(lèi)似,只是不能由用戶(hù)指定暫停多長(zhǎng)時(shí)間。
3)t.join()/t.join(long millis):當(dāng)前線(xiàn)程里調(diào)用其它線(xiàn)程t的join方法,當(dāng)前線(xiàn)程進(jìn)入WAITING/TIMED_WAITING狀態(tài),當(dāng)前線(xiàn)程不會(huì)釋放已經(jīng)持有的對(duì)象鎖。線(xiàn)程t執(zhí)行完畢或者millis時(shí)間到,當(dāng)前線(xiàn)程進(jìn)入就緒狀態(tài)。
4)obj.wait():當(dāng)前線(xiàn)程調(diào)用對(duì)象的wait()方法,當(dāng)前線(xiàn)程釋放對(duì)象鎖,進(jìn)入等待隊(duì)列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時(shí)間到自動(dòng)喚醒。
5)obj.notify():喚醒在此對(duì)象監(jiān)視器上等待的單個(gè)線(xiàn)程,選擇是任意性的。notifyAll()喚醒在此對(duì)象監(jiān)視器上等待的所有線(xiàn)程。
6)obj.stop():強(qiáng)制結(jié)束某一線(xiàn)程的生命周期,不管它有沒(méi)有執(zhí)行完畢
7)obj.isAlive():判斷某一線(xiàn)程是否仍然存活,true表示仍然存活,false表示已經(jīng)終止。線(xiàn)程一旦終止,就不能復(fù)生。
3.2、線(xiàn)程的 join() 方法
一個(gè)線(xiàn)程還可以等待另一個(gè)線(xiàn)程,直到該線(xiàn)程運(yùn)行結(jié)束后再繼續(xù)執(zhí)行本線(xiàn)程的程序。例如,main線(xiàn)程在啟動(dòng)t線(xiàn)程后,可以通過(guò)t.join()等待t線(xiàn)程結(jié)束后再繼續(xù)運(yùn)行:
//下面代碼將依次輸出:start hello end 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(); //這里調(diào)用t.join()方法,main線(xiàn)程將會(huì)等待t線(xiàn)程運(yùn)行結(jié)束后才會(huì)繼續(xù)執(zhí)行下面的代碼 System.out.println("end"); } }
通過(guò)對(duì)另一個(gè)線(xiàn)程對(duì)象調(diào)用join()方法可以等待其執(zhí)行結(jié)束。上面當(dāng)main線(xiàn)程對(duì)線(xiàn)程對(duì)象t調(diào)用join()方法時(shí),主線(xiàn)程將等待變量t表示的線(xiàn)程運(yùn)行結(jié)束。
對(duì)一個(gè)已經(jīng)運(yùn)行結(jié)束的線(xiàn)程調(diào)用join()方法會(huì)立刻返回。此外,join(long)的重載方法也可以指定一個(gè)等待時(shí)間,超過(guò)等待時(shí)間后就不再繼續(xù)等待。
3.3、線(xiàn)程讓步(yield()方法)
調(diào)用 Thread.yield() 方法線(xiàn)程會(huì)由運(yùn)行態(tài)到就緒態(tài),停止一下后再由就緒態(tài)到運(yùn)行態(tài)。調(diào)用 yield 方法會(huì)讓當(dāng)前線(xiàn)程交出CPU權(quán)限,讓CPU去執(zhí)行其他的線(xiàn)程,但是它只會(huì)把執(zhí)行機(jī)會(huì)讓給優(yōu)先級(jí)相同或者更高的線(xiàn)程。注意調(diào)用 yield 方法并不會(huì)讓線(xiàn)程進(jìn)入阻塞狀態(tài),而是讓線(xiàn)程重回就緒狀態(tài),它只需要等待重新獲取 CPU 執(zhí)行時(shí)間,這一點(diǎn)是和sleep方法不一樣的。
public class TestThread16 { public static void main(String[] args) { MyThreadTest mt = new MyThreadTest(); new Thread(mt,"1").start(); new Thread(mt,"2").start(); new Thread(mt,"3").start(); } } class MyThreadTest implements Runnable { @Override public void run() { for (int i = 0; i < 3; i++) { Thread.yield(); System.out.println("當(dāng)前線(xiàn)程:" + Thread.currentThread().getName() + ",i =" + i); } } }
4、中斷線(xiàn)程
如果線(xiàn)程需要執(zhí)行一個(gè)長(zhǎng)時(shí)間任務(wù),就可能需要能中斷線(xiàn)程。中斷線(xiàn)程就是其他線(xiàn)程給該線(xiàn)程發(fā)一個(gè)信號(hào),該線(xiàn)程收到信號(hào)后結(jié)束執(zhí)行run()方法,使得自身線(xiàn)程能立刻結(jié)束運(yùn)行。
我們舉個(gè)栗子:假設(shè)從網(wǎng)絡(luò)下載一個(gè)100M的文件,如果網(wǎng)速很慢,用戶(hù)等得不耐煩,就可能在下載過(guò)程中點(diǎn)“取消”,這時(shí),程序就需要中斷下載線(xiàn)程的執(zhí)行。
中斷一個(gè)線(xiàn)程非常簡(jiǎn)單,只需要在其他線(xiàn)程中對(duì)目標(biāo)線(xiàn)程調(diào)用interrupt()方法,目標(biāo)線(xiàn)程需要反復(fù)檢測(cè)自身狀態(tài)是否是interrupted狀態(tài),如果是,就立刻結(jié)束運(yùn)行。
示例代碼:
public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1); // 暫停1毫秒 t.interrupt(); // 中斷t線(xiàn)程 t.join(); // 等待t線(xiàn)程結(jié)束 System.out.println("end"); } } class MyThread extends Thread { public void run() { int n = 0; while (! isInterrupted()) { //必須得一直監(jiān)聽(tīng)是否是interrupted狀態(tài)才能中斷線(xiàn)程 n ++; System.out.println(n + " hello!"); } } }
通過(guò)調(diào)用t.interrupt()方法中斷線(xiàn)程只是向 t 線(xiàn)程發(fā)出了“中斷請(qǐng)求”,至于 t 線(xiàn)程是否能立刻響應(yīng),還要看具體代碼。
如果某一線(xiàn)程比如 A 線(xiàn)程處于等待狀態(tài)(比如 A 線(xiàn)程調(diào)用了 join() 方法在等待其他線(xiàn)程的執(zhí)行完畢,此時(shí) A 線(xiàn)程就是處于等待狀態(tài)),此時(shí)其他線(xiàn)程對(duì) A 調(diào)用 interrupt() 方法企圖中斷 A 線(xiàn)程,那么 A 線(xiàn)程中的 join() 方法就會(huì)立刻拋出InterruptedException異常。因此,如果某一線(xiàn)程比如 A 線(xiàn)程只要捕獲到本身線(xiàn)程內(nèi)的 join() 方法拋出了InterruptedException異常,就說(shuō)明有其他線(xiàn)程對(duì)它調(diào)用了interrupt()方法,通常情況下 A 線(xiàn)程應(yīng)該立刻結(jié)束運(yùn)行,并且最好也中斷其調(diào)線(xiàn)程內(nèi)開(kāi)啟的其他線(xiàn)程,否則其他線(xiàn)程仍然會(huì)繼續(xù)執(zhí)行,且 JVM 不會(huì)退出。
//下面代碼中,main線(xiàn)程通過(guò)調(diào)用t.interrupt()從而通知t線(xiàn)程中斷,而此時(shí)t線(xiàn)程正位于hello.join()的等待中,此方法會(huì)立刻結(jié)束等待并拋出InterruptedException。由于我們?cè)趖線(xiàn)程中捕獲了InterruptedException,因此,就可以準(zhǔn)備結(jié)束該線(xiàn)程。在t線(xiàn)程結(jié)束前,對(duì)hello線(xiàn)程也進(jìn)行了interrupt()調(diào)用通知其中斷。如果去掉這一行代碼,可以發(fā)現(xiàn)hello線(xiàn)程仍然會(huì)繼續(xù)運(yùn)行,且JVM不會(huì)退出。 public class Main { public static void main(String[] args) throws InterruptedException { Thread t = new MyThread(); t.start(); Thread.sleep(1000); t.interrupt(); // 中斷t線(xiàn)程 t.join(); // 等待t線(xiàn)程結(jié)束 System.out.println("end"); } } class MyThread extends Thread { public void run() { Thread hello = new HelloThread(); hello.start(); // 啟動(dòng)hello線(xiàn)程 try { hello.join(); // 等待hello線(xiàn)程結(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; } } } }
另一個(gè)常用的中斷線(xiàn)程的方法是設(shè)置標(biāo)志位。我們通常會(huì)用一個(gè)running標(biāo)志位來(lái)標(biāo)識(shí)線(xiàn)程是否應(yīng)該繼續(xù)運(yùn)行,在外部線(xiàn)程中,通過(guò)把HelloThread.running置為false,就可以讓線(xiàn)程結(jié)束:
public class Main { public static void main(String[] args) throws InterruptedException { HelloThread t = new HelloThread(); t.start(); Thread.sleep(1); t.running = false; // 標(biāo)志位置為false } } class HelloThread extends Thread { public volatile boolean running = true; //HelloThread的標(biāo)志位boolean running是一個(gè)線(xiàn)程間共享的變量。線(xiàn)程間共享變量需要使用volatile關(guān)鍵字標(biāo)記,確保每個(gè)線(xiàn)程都能讀取到更新后的變量值。 public void run() { int n = 0; while (running) { n ++; System.out.println(n + " hello!"); } System.out.println("end!"); } }
4.1、volatile關(guān)鍵字
為什么要對(duì)線(xiàn)程間共享的變量用關(guān)鍵字volatile聲明?這涉及到Java的內(nèi)存模型。在Java虛擬機(jī)中,變量的值保存在主內(nèi)存中,但是,當(dāng)線(xiàn)程訪(fǎng)問(wèn)變量時(shí),它會(huì)先獲取一個(gè)副本,并保存在自己的工作內(nèi)存中。如果線(xiàn)程修改了變量的值,虛擬機(jī)會(huì)在某個(gè)時(shí)刻把修改后的值回寫(xiě)到主內(nèi)存,但是,這個(gè)時(shí)間是不確定的!

這會(huì)導(dǎo)致如果一個(gè)線(xiàn)程更新了某個(gè)變量,另一個(gè)線(xiàn)程讀取的值可能還是更新前的。例如,主內(nèi)存的變量a = true,線(xiàn)程1執(zhí)行a = false時(shí),它在此刻僅僅是把變量a的副本變成了false,主內(nèi)存的變量a還是true,在JVM把修改后的a回寫(xiě)到主內(nèi)存之前,其他線(xiàn)程讀取到的a的值仍然是true,這就造成了多線(xiàn)程之間共享的變量不一致。
因此,volatile關(guān)鍵字的目的是告訴虛擬機(jī):
- 每次訪(fǎng)問(wèn)變量時(shí),總是獲取主內(nèi)存的最新值;
- 每次修改變量后,立刻回寫(xiě)到主內(nèi)存。
volatile關(guān)鍵字解決了共享變量在線(xiàn)程間的可見(jiàn)性問(wèn)題:當(dāng)一個(gè)線(xiàn)程修改了某個(gè)共享變量的值,其他線(xiàn)程能夠立刻看到修改后的值。
如果我們?nèi)サ?code>volatile關(guān)鍵字,運(yùn)行上述程序,發(fā)現(xiàn)效果和帶volatile差不多,這是因?yàn)樵趚86的架構(gòu)下,JVM回寫(xiě)主內(nèi)存的速度非???,但是,換成ARM的架構(gòu),就會(huì)有顯著的延遲。
5、守護(hù)線(xiàn)程
Java程序入口就是由JVM啟動(dòng)main線(xiàn)程,main線(xiàn)程又可以啟動(dòng)其他線(xiàn)程。當(dāng)所有線(xiàn)程都運(yùn)行結(jié)束時(shí),JVM退出,進(jìn)程結(jié)束。
如果有一個(gè)線(xiàn)程沒(méi)有退出,JVM進(jìn)程就不會(huì)退出。所以,必須保證所有線(xiàn)程都能及時(shí)結(jié)束。
但是有一種線(xiàn)程的目的就是無(wú)限循環(huán),例如,一個(gè)定時(shí)觸發(fā)任務(wù)的線(xiàn)程:
class TimerThread extends Thread { @Override public void run() { while (true) { System.out.println(LocalTime.now()); try { Thread.sleep(1000); } catch (InterruptedException e) { break; } } } }
這類(lèi)線(xiàn)程經(jīng)常沒(méi)有負(fù)責(zé)人來(lái)負(fù)責(zé)結(jié)束它們。但是,當(dāng)其他線(xiàn)程結(jié)束時(shí),JVM進(jìn)程又必須要結(jié)束,此時(shí)可以使用守護(hù)線(xiàn)程。守護(hù)線(xiàn)程是指為其他線(xiàn)程服務(wù)的線(xiàn)程。
在 Java 中,JVM 虛擬機(jī)在所有非守護(hù)線(xiàn)程都執(zhí)行完畢后會(huì)自動(dòng)退出,不會(huì)關(guān)心守護(hù)線(xiàn)程是否已結(jié)束。但如果非守護(hù)線(xiàn)程一直沒(méi)有執(zhí)行完畢,那么 JVM 進(jìn)程也會(huì)一直在運(yùn)行不會(huì)退出。
5.1、創(chuàng)建守護(hù)線(xiàn)程
創(chuàng)建守護(hù)線(xiàn)程的方法和普通線(xiàn)程一樣,只是在調(diào)用start()方法前,調(diào)用setDaemon(true)把該線(xiàn)程標(biāo)記為守護(hù)線(xiàn)程:
Thread t = new MyThread(); t.setDaemon(true); t.start();
在守護(hù)線(xiàn)程中,編寫(xiě)代碼要注意:守護(hù)線(xiàn)程不能持有任何需要關(guān)閉的資源,例如打開(kāi)文件等,因?yàn)樘摂M機(jī)退出時(shí),守護(hù)線(xiàn)程沒(méi)有任何機(jī)會(huì)來(lái)關(guān)閉文件,這會(huì)導(dǎo)致數(shù)據(jù)丟失。
6、線(xiàn)程同步
當(dāng)多個(gè)線(xiàn)程同時(shí)運(yùn)行時(shí),線(xiàn)程的調(diào)度由操作系統(tǒng)決定,程序本身無(wú)法決定。因此,任何一個(gè)線(xiàn)程都有可能在任何指令處被操作系統(tǒng)暫停,然后在某個(gè)時(shí)間段后繼續(xù)執(zhí)行。
這個(gè)時(shí)候,有個(gè)單線(xiàn)程模型下不存在的問(wèn)題就來(lái)了:如果多個(gè)線(xiàn)程同時(shí)讀寫(xiě)共享變量,會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題。
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; } } }
上面代碼理論上最后輸出 Counter.count 應(yīng)該為 0,因?yàn)椴还軋?zhí)行順序先后,最后都是對(duì) Counter.count 進(jìn)行了加減 10000。但實(shí)際上可以看到代碼執(zhí)行每次輸出的結(jié)果都不一定。
原因解析:
對(duì)變量進(jìn)行讀取和寫(xiě)入時(shí),結(jié)果要正確,必須保證對(duì)變量的操作是原子操作(原子操作是指不能被中斷的一個(gè)或一系列操作),即不能被中斷,并且在對(duì)變量操作過(guò)程中其他線(xiàn)程不能同時(shí)對(duì)變量進(jìn)行操作。
例如,對(duì)于語(yǔ)句:n=n+1; 看上去是一行語(yǔ)句,實(shí)際上對(duì)應(yīng)了3條指令:ILOAD IADD ISTORE
我們假設(shè)n的值是100,如果兩個(gè)線(xiàn)程同時(shí)執(zhí)行n = n + 1,得到的結(jié)果很可能不是102,而是101,原因在于:

如果線(xiàn)程1在執(zhí)行ILOAD后被操作系統(tǒng)中斷,此刻如果線(xiàn)程2被調(diào)度執(zhí)行,它執(zhí)行ILOAD后獲取的值仍然是100,最終結(jié)果被兩個(gè)線(xiàn)程的ISTORE寫(xiě)入后變成了101,而不是期待的102。
這說(shuō)明多線(xiàn)程模型下,要保證邏輯正確,對(duì)共享變量進(jìn)行讀寫(xiě)時(shí),必須保證一組指令以原子方式執(zhí)行:即某一個(gè)線(xiàn)程執(zhí)行時(shí),其他線(xiàn)程必須等待:

通過(guò)加鎖和解鎖的操作,就能保證3條指令總是在一個(gè)線(xiàn)程執(zhí)行期間,不會(huì)有其他線(xiàn)程會(huì)進(jìn)入此指令區(qū)間。即使在執(zhí)行期線(xiàn)程被操作系統(tǒng)中斷執(zhí)行,其他線(xiàn)程也會(huì)因?yàn)闊o(wú)法獲得鎖導(dǎo)致無(wú)法進(jìn)入此指令區(qū)間。只有執(zhí)行線(xiàn)程將鎖釋放后,其他線(xiàn)程才有機(jī)會(huì)獲得鎖并執(zhí)行。這種加鎖和解鎖之間的代碼塊我們稱(chēng)之為臨界區(qū)(Critical Section),任何時(shí)候臨界區(qū)最多只有一個(gè)線(xiàn)程能執(zhí)行。
6.1、通過(guò)synchronized關(guān)鍵字實(shí)現(xiàn)加解鎖
可見(jiàn),保證一段代碼的原子性就是通過(guò)加鎖和解鎖實(shí)現(xiàn)的。Java程序使用synchronized關(guān)鍵字對(duì)一個(gè)對(duì)象進(jìn)行加鎖:
synchronized(lockObj) { //獲取鎖 n = n + 1; } //釋放鎖
synchronized保證了代碼塊在任意時(shí)刻最多只有一個(gè)線(xiàn)程能執(zhí)行,使用synchronized的步驟:
- 找出修改共享變量的線(xiàn)程代碼塊;
- 選擇一個(gè)共享實(shí)例作為鎖;
- 使用
synchronized(lockObject) { ... }。
同步的本質(zhì)就是給指定對(duì)象加鎖,加鎖后才能繼續(xù)執(zhí)行后續(xù)代碼。
我們把之前的代碼用synchronized改寫(xiě)如下:
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.lock共享實(shí)例作為鎖 Counter.count += 1; } //釋放鎖 } } } class DecThread extends Thread { public void run() { for (int i=0; i<10000; i++) { synchronized(Counter.lock) { Counter.count -= 1; } } } }
上面代碼用Counter.lock實(shí)例作為鎖,兩個(gè)線(xiàn)程在執(zhí)行各自的synchronized(Counter.lock) { ... }代碼塊時(shí),必須先獲得鎖,才能進(jìn)入代碼塊進(jìn)行。執(zhí)行結(jié)束后,在synchronized語(yǔ)句塊結(jié)束會(huì)自動(dòng)釋放鎖。這樣一來(lái),對(duì)Counter.count變量進(jìn)行讀寫(xiě)就不可能同時(shí)進(jìn)行。上述代碼無(wú)論運(yùn)行多少次,最終結(jié)果都是0。
使用synchronized解決了多線(xiàn)程同步訪(fǎng)問(wèn)共享變量的正確性問(wèn)題。但是,它的缺點(diǎn)是帶來(lái)了性能下降。因?yàn)?code>synchronized代碼塊無(wú)法并發(fā)執(zhí)行。此外,加鎖和解鎖需要消耗一定的時(shí)間,所以,synchronized會(huì)降低程序的執(zhí)行效率。
請(qǐng)注意:線(xiàn)程的原子操作要想不被其他線(xiàn)程中斷,那么各自線(xiàn)程 synchronized 鎖住的 lock 對(duì)象必須得是同一個(gè)實(shí)例對(duì)象。如果兩個(gè)線(xiàn)程各自的synchronized鎖住的不是同一個(gè)對(duì)象,那么這兩個(gè)線(xiàn)程各自都可以同時(shí)獲得鎖,那么線(xiàn)程之間的原子操作仍然可以并發(fā)進(jìn)行,導(dǎo)致數(shù)據(jù)同時(shí)發(fā)生修改而出錯(cuò)。
JVM只保證同一個(gè)鎖在任意時(shí)刻只能被一個(gè)線(xiàn)程獲取,但兩個(gè)不同的鎖在同一時(shí)刻可以被兩個(gè)線(xiàn)程分別獲取。
如果線(xiàn)程之間的原子操作可以并發(fā)執(zhí)行,那么就可以不使用同一個(gè)鎖對(duì)象。比如A和B線(xiàn)程需要同步,C和D線(xiàn)程需要同步,那么A和B需要用同一個(gè)lock對(duì)象,C和D需要用同一個(gè)lock對(duì)象,但這兩組的lock對(duì)象不必是同一個(gè),不然會(huì)大大降低執(zhí)行效率。
在使用synchronized的時(shí)候,不必?fù)?dān)心拋出異常。因?yàn)闊o(wú)論是否有異常,都會(huì)在synchronized結(jié)束處正確釋放鎖:
public void add(int m) { synchronized (obj) { if (m < 0) { throw new RuntimeException(); } this.value += m; } // 無(wú)論有無(wú)異常,都會(huì)在此釋放鎖 }
6.2、JVM規(guī)定的幾種原子操作
JVM規(guī)范定義了幾種原子操作:
- 基本類(lèi)型(
long和double除外)賦值,例如:int n = m。(long和double是64位數(shù)據(jù),JVM沒(méi)有明確規(guī)定64位賦值操作是不是一個(gè)原子操作,不過(guò)在x64平臺(tái)的JVM是把long和double的賦值作為原子操作實(shí)現(xiàn)的) - 引用類(lèi)型賦值,例如:
List<String> list = anotherList。
單條原子操作的語(yǔ)句不需要同步。例如:
public void set(String s) { this.value = s; }
但是,如果是多行賦值語(yǔ)句,就必須保證是同步操作,例如:
class Pair { int first; int last; public void set(int first, int last) { synchronized(this) { this.first = first; this.last = last; } } }
當(dāng)然,我們也可以將多條賦值語(yǔ)句寫(xiě)成一條賦值語(yǔ)句,這樣就可以不必要寫(xiě)同步操作了。
每個(gè)線(xiàn)程都會(huì)有各自的局部變量,互不影響,并且互不可見(jiàn),并不需要同步。
7、同步方法(synchronized關(guān)鍵字)
7.1、線(xiàn)程安全基本介紹
線(xiàn)程安全是指在多線(xiàn)程環(huán)境下,一段代碼或一個(gè)對(duì)象能夠正確地工作,而不會(huì)因?yàn)槎鄠€(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)和修改共享資源而產(chǎn)生數(shù)據(jù)不一致或錯(cuò)誤的結(jié)果。
7.2、synchronized關(guān)鍵字用法
在 Java 里,synchronized關(guān)鍵字主要用于實(shí)現(xiàn)線(xiàn)程同步,確保同一時(shí)刻只有一個(gè)線(xiàn)程能夠訪(fǎng)問(wèn)被其修飾的代碼塊或者方法,進(jìn)而避免多線(xiàn)程訪(fǎng)問(wèn)共享資源時(shí)出現(xiàn)的數(shù)據(jù)不一致問(wèn)題。
7.2.1、修飾實(shí)例方法
public class SynchronizedInstanceMethodExample { private int count = 0; // 同步實(shí)例方法 public synchronized void increment() { count++; } public int getCount() { return count; } }
在上述代碼中,increment方法被synchronized修飾,所以在多線(xiàn)程環(huán)境下,同一時(shí)刻只有一個(gè)線(xiàn)程能夠調(diào)用該方法來(lái)修改count變量,避免了多個(gè)線(xiàn)程同時(shí)操作導(dǎo)致的數(shù)據(jù)不一致問(wèn)題。
調(diào)用示例如下:當(dāng)多個(gè)線(xiàn)程同時(shí)調(diào)用synchronized修飾的實(shí)例方法時(shí),同一時(shí)刻只會(huì)有一個(gè)線(xiàn)程獲得該實(shí)例的鎖并執(zhí)行方法,其他線(xiàn)程需要等待鎖被釋放。
class Counter { private int count = 0; // 使用synchronized修飾的實(shí)例方法 public synchronized void increment() { try { // 模擬耗時(shí)操作 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } count++; System.out.println(Thread.currentThread().getName() + " 當(dāng)前計(jì)數(shù): " + count); } } public class MultiThreadExample { public static void main(String[] args) { // 創(chuàng)建Counter類(lèi)的實(shí)例 Counter counter = new Counter(); // 創(chuàng)建并啟動(dòng)多個(gè)線(xiàn)程 Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { counter.increment(); } }, "線(xiàn)程1"); Thread thread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { counter.increment(); } }, "線(xiàn)程2"); thread1.start(); thread2.start(); try { // 等待兩個(gè)線(xiàn)程執(zhí)行完畢 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
7.2.2、修飾靜態(tài)方法
public class SynchronizedStaticMethodExample { private static int count = 0; // 同步靜態(tài)方法 public static synchronized void increment() { count++; } public static int getCount() { return count; } }
在這個(gè)例子中,increment是靜態(tài)方法,被synchronized修飾后,同一時(shí)刻僅允許一個(gè)線(xiàn)程執(zhí)行該方法,對(duì)靜態(tài)變量count進(jìn)行修改。
調(diào)用示例如下:當(dāng)多個(gè)線(xiàn)程同時(shí)調(diào)用synchronized修飾的靜態(tài)方法時(shí),由于synchronized的作用,同一時(shí)刻只會(huì)有一個(gè)線(xiàn)程能夠獲得類(lèi)鎖并執(zhí)行該方法,其他線(xiàn)程需要等待鎖被釋放。
public class SynchronizedStaticMethodExample { // 被synchronized修飾的靜態(tài)方法 public static synchronized void incrementAndPrint() { // 模擬對(duì)靜態(tài)變量的操作 staticVariable++; System.out.println(Thread.currentThread().getName() + " - 靜態(tài)變量的值為: " + staticVariable); } private static int staticVariable = 0; public static void main(String[] args) { // 創(chuàng)建多個(gè)線(xiàn)程并啟動(dòng) Thread thread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { SynchronizedStaticMethodExample.incrementAndPrint(); } }, "線(xiàn)程1"); Thread thread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { SynchronizedStaticMethodExample.incrementAndPrint(); } }, "線(xiàn)程2"); thread1.start(); thread2.start(); try { // 等待兩個(gè)線(xiàn)程執(zhí)行完畢 thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
7.2.3、修飾代碼塊
synchronized還可以用于修飾代碼塊,這種方式能更靈活地控制鎖的范圍。可以指定鎖定的對(duì)象,既可以是當(dāng)前對(duì)象實(shí)例(this),也可以是其他對(duì)象。
public class SynchronizedBlockExample { private int count = 0; private final Object lock = new Object(); public void increment() { // 同步代碼塊,鎖定lock對(duì)象 synchronized (lock) { count++; } } public int getCount() { return count; } }
在上述代碼中,synchronized修飾的代碼塊使用lock對(duì)象作為鎖。這樣在多線(xiàn)程環(huán)境下,同一時(shí)刻只有一個(gè)線(xiàn)程能夠進(jìn)入這個(gè)代碼塊來(lái)修改count變量。使用這種方式可以將鎖的范圍控制得更精確,只對(duì)需要同步的代碼部分加鎖,提高程序的性能。
8、死鎖
8.1、可重入鎖
Java的線(xiàn)程鎖是可重入的鎖。
下面的代碼中,synchronized 修飾 add 方法,一旦線(xiàn)程執(zhí)行到 add 方法內(nèi)部,說(shuō)明它已經(jīng)獲得了當(dāng)前實(shí)例的 this 鎖,如果傳入的n < 0,將在add()方法內(nèi)部調(diào)用dec()方法,dec()方法也需要獲取this鎖,并且 dec() 方法也能獲取到 this 鎖。在 Java 中,同一個(gè)線(xiàn)程在獲取到鎖仍可以繼續(xù)獲取同一個(gè)鎖。
JVM允許同一個(gè)線(xiàn)程重復(fù)獲取同一個(gè)鎖,這種能被同一個(gè)線(xiàn)程反復(fù)獲取的鎖,就叫做可重入鎖。
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; } }
由于Java的線(xiàn)程鎖是可重入鎖,所以,JVM 在獲取鎖的時(shí)候,不但要判斷是否是第一次獲取,還要記錄這是第幾次獲取。每獲取一次鎖,記錄+1,每退出synchronized塊,記錄-1,減到0的時(shí)候,才會(huì)真正釋放鎖。
8.2、死鎖
一個(gè)線(xiàn)程可以獲取一個(gè)鎖后,再繼續(xù)獲取另一個(gè)鎖。例如:
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的鎖 }
在獲取多個(gè)鎖的時(shí)候,不同線(xiàn)程獲取多個(gè)不同對(duì)象的鎖可能導(dǎo)致死鎖。對(duì)于上述代碼,線(xiàn)程1和線(xiàn)程2如果分別執(zhí)行add()和dec()方法時(shí):
- 線(xiàn)程1:進(jìn)入
add(),獲得lockA; - 線(xiàn)程2:進(jìn)入
dec(),獲得lockB。
隨后:
- 線(xiàn)程1:準(zhǔn)備獲得
lockB,失敗,等待中; - 線(xiàn)程2:準(zhǔn)備獲得
lockA,失敗,等待中。
此時(shí),兩個(gè)線(xiàn)程各自持有不同的鎖,然后各自試圖獲取對(duì)方手里的鎖,造成了雙方無(wú)限等待下去,這就是死鎖。死鎖發(fā)生后,沒(méi)有任何機(jī)制能解除死鎖,只能強(qiáng)制結(jié)束JVM進(jìn)程。因此,在編寫(xiě)多線(xiàn)程應(yīng)用時(shí),要特別注意防止死鎖。因?yàn)樗梨i一旦形成,就只能強(qiáng)制結(jié)束進(jìn)程。死鎖產(chǎn)生的條件是多線(xiàn)程各自持有不同的鎖,并互相試圖獲取對(duì)方已持有的鎖,導(dǎo)致無(wú)限等待;
避免死鎖的方法是多線(xiàn)程獲取鎖的順序要一致。
比如上面的代碼,應(yīng)該嚴(yán)格按照先獲取lockA,再獲取lockB的順序,可以將dec()方法改寫(xiě)如下:
public void dec(int m) { synchronized(lockA) { // 獲得lockA的鎖 this.value -= m; synchronized(lockB) { // 獲得lockB的鎖 this.another -= m; } // 釋放lockB的鎖 } // 釋放lockA的鎖 }
9、等待和喚醒線(xiàn)程(wait、notify、notifyAll)
9.1、將線(xiàn)程掛起(wait())
wait() 方法可將當(dāng)前線(xiàn)程掛起并放棄CPU、同步資源,使別的線(xiàn)程可訪(fǎng)問(wèn)并修改共享資源,而當(dāng)前線(xiàn)程則需等待被喚醒才有資格再次運(yùn)行。wait()方法必須在當(dāng)前獲取的鎖對(duì)象上調(diào)用。
在多線(xiàn)程進(jìn)行協(xié)調(diào)運(yùn)行時(shí),當(dāng)條件不滿(mǎn)足時(shí),線(xiàn)程應(yīng)進(jìn)入等待狀態(tài);當(dāng)條件滿(mǎn)足時(shí),線(xiàn)程再被喚醒,繼續(xù)執(zhí)行任務(wù)。否則可能會(huì)出現(xiàn)線(xiàn)程一直占用鎖,其他線(xiàn)程無(wú)法執(zhí)行的情況。
比如:
class TaskQueue { Queue<String> queue = new LinkedList<>(); public synchronized void addTask(String s) { this.queue.add(s); } public synchronized String getTask() { while (queue.isEmpty()) { } return queue.remove(); } }
上面代碼中,getTask()內(nèi)部先判斷隊(duì)列是否為空,如果為空,就循環(huán)等待判斷是否為空,直到另一個(gè)線(xiàn)程往隊(duì)列中放入了一個(gè)任務(wù),while()循環(huán)退出,就可以返回隊(duì)列的元素了。
但實(shí)際上while()循環(huán)永遠(yuǎn)不會(huì)退出。因?yàn)榫€(xiàn)程在執(zhí)行while()循環(huán)時(shí),已經(jīng)在getTask()入口獲取了this鎖,其他線(xiàn)程根本無(wú)法調(diào)用addTask(),因?yàn)?code>addTask()執(zhí)行條件也是獲取this鎖。
對(duì)于上述TaskQueue,我們應(yīng)該改造getTask()方法,在條件不滿(mǎn)足時(shí),線(xiàn)程進(jìn)入等待狀態(tài):
public synchronized String getTask() { while (queue.isEmpty()) { this.wait(); //進(jìn)入等待狀態(tài)。只能在鎖對(duì)象上調(diào)用wait()方法 } return queue.remove(); }
當(dāng)一個(gè)線(xiàn)程執(zhí)行到getTask()方法內(nèi)部的while循環(huán)時(shí),它必定已經(jīng)獲取到了this鎖,此時(shí),線(xiàn)程執(zhí)行while條件判斷,如果條件成立(隊(duì)列為空),線(xiàn)程將執(zhí)行this.wait(),進(jìn)入等待狀態(tài)。
wait()方法必須在當(dāng)前獲取的鎖對(duì)象上調(diào)用,這里獲取的是this鎖,因此調(diào)用this.wait()。方法調(diào)用時(shí),會(huì)釋放線(xiàn)程獲得的鎖,wait()方法的執(zhí)行機(jī)制非常復(fù)雜。首先,它不是一個(gè)普通的Java方法,而是定義在Object類(lèi)的一個(gè)native方法,也就是由JVM的C代碼實(shí)現(xiàn)的。其次,必須在synchronized塊中才能調(diào)用wait()方法,因?yàn)?code>wait()wait()方法返回后,線(xiàn)程又會(huì)重新試圖獲得鎖。因此,只能在鎖對(duì)象上調(diào)用wait()方法。因?yàn)樵?code>getTask()中,我們獲得了this鎖,因此,只能在this對(duì)象上調(diào)用wait()方法:
調(diào)用wait()方法后,線(xiàn)程進(jìn)入等待狀態(tài),wait()方法不會(huì)返回,直到將來(lái)某個(gè)時(shí)刻,線(xiàn)程從等待狀態(tài)被其他線(xiàn)程喚醒后,wait()方法才會(huì)返回,然后,繼續(xù)執(zhí)行下一條語(yǔ)句。
當(dāng)一個(gè)線(xiàn)程在this.wait()等待時(shí),它就會(huì)釋放this鎖,從而使得其他線(xiàn)程能夠在addTask()方法獲得this鎖。
9.2、喚醒線(xiàn)程(notify()、notifyAll())
我們可以在相同的鎖對(duì)象上調(diào)用notify()、nofityAll()方法來(lái)讓等待的線(xiàn)程從wait()方法返回,被重新喚醒。注意應(yīng)該是在相同的鎖對(duì)象上調(diào)用,并且這兩個(gè)方法都只能用在 synchronized 方法或者 synchronized 代碼塊中,否則會(huì)報(bào)異常。
我們可以將addTask()方法改造如下:
public synchronized void addTask(String s) { this.queue.add(s); this.notify(); // 喚醒在this鎖等待的線(xiàn)程 }
該方法會(huì)喚醒一個(gè)正在this鎖等待的線(xiàn)程(就是在getTask()中位于this.wait()的線(xiàn)程),從而使得某一個(gè)等待線(xiàn)程從this.wait()方法返回。
要想喚醒所有當(dāng)前正在this鎖等待的線(xiàn)程,我們可以調(diào)用notifyAll()方法,notify()只會(huì)喚醒其中一個(gè)(具體哪個(gè)依賴(lài)操作系統(tǒng),有一定的隨機(jī)性)。通常來(lái)說(shuō),notifyAll()更安全。有些時(shí)候,如果我們的代碼邏輯考慮不周,用notify()會(huì)導(dǎo)致只喚醒了一個(gè)線(xiàn)程,而其他線(xiàn)程可能永遠(yuǎn)等待下去醒不過(guò)來(lái)了。

浙公網(wǎng)安備 33010602011771號(hào)