前言
上章介紹了線程生命周期
的就緒
和運(yùn)行
狀態(tài)
這章講下線程生命周期中最復(fù)雜的阻塞
狀態(tài)
阻塞(Blocked)
在開(kāi)始之前
我們先科普
幾個(gè)概念
阻塞,掛起,睡眠 區(qū)分
阻塞
在線程執(zhí)行時(shí),所需要的資源不能立馬得到,則線程被“阻塞”,直到滿(mǎn)足條件則會(huì)繼續(xù)執(zhí)行
阻塞是一種“被動(dòng)”的狀態(tài)
掛起
線程執(zhí)行時(shí),因?yàn)椤爸饔^”需要,需要暫停執(zhí)行當(dāng)前的線程,此時(shí)需要“掛起”當(dāng)前線程.
掛起是“主動(dòng)”的動(dòng)作行為,因?yàn)槭恰爸鲃?dòng)”的,所以“掛起”的線程需要“喚醒”才能繼續(xù)執(zhí)行
睡眠
線程執(zhí)行時(shí),因?yàn)椤爸饔^”需要,需要等待執(zhí)行一段時(shí)間后再繼續(xù)執(zhí)行,
此時(shí)需要讓當(dāng)前線程睡眠一段時(shí)間
睡眠和掛起一樣,也是“主動(dòng)”的動(dòng)作行為,不過(guò)和掛起不一樣的是,它規(guī)定了時(shí)間!
我們舉個(gè)形象的例子來(lái)說(shuō)明
假設(shè)你是個(gè)主人,雇傭了一個(gè)傭人
掛起: 你主動(dòng)對(duì)阿姨說(shuō) “你先去休息,有需要我再喊你”
睡眠: 你主動(dòng)對(duì)阿姨說(shuō) “你去睡兩個(gè)小時(shí),然后繼續(xù)干活”
阻塞: 阿姨自己沒(méi)在干活,因?yàn)楦苫畹墓ぞ卟灰?jiàn)了,等有了工具,她會(huì)自覺(jué)繼續(xù)干活
明白了以上概念,我們繼續(xù)了解,線程什么情況會(huì)阻塞?
線程阻塞原因
對(duì)于線程來(lái)講,當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):
線程調(diào)用一個(gè)
阻塞式I/O方法
,在該方法返回之前,該線程被阻塞線程試圖獲得一個(gè)
同步監(jiān)視器
,但該同步監(jiān)視器正被其他線程所持有線程調(diào)用
sleep()
: sleep()不會(huì)釋放對(duì)象鎖資源,指定的時(shí)間一過(guò),線程自動(dòng)重新進(jìn)入就緒狀態(tài)線程調(diào)用
wait()
: wait()會(huì)釋放持有的對(duì)象鎖,需要notify( )或notifyAll()喚醒線程調(diào)用
suspend()
掛起(已廢棄,不推薦使用
): resume()(已廢棄,不推薦使用)可以喚醒,使線程重新進(jìn)入就緒狀態(tài)線程調(diào)用
Join()
方法: 如線程A中調(diào)用了線程B的Join()方法,直到線程B線程執(zhí)行完畢后,線程A才會(huì)被自動(dòng)喚醒,進(jìn)入就緒狀態(tài)
需要說(shuō)明的是
在阻塞狀態(tài)的線程
只能進(jìn)入就緒狀態(tài),無(wú)法直接進(jìn)入運(yùn)行狀態(tài)
-
就緒和運(yùn)行狀態(tài)之間的轉(zhuǎn)換通常
不受程序控制
,而是由系統(tǒng)線程調(diào)度
所決定當(dāng)處于就緒狀態(tài)的線程
獲得
CPU時(shí)間片時(shí),該線程進(jìn)入運(yùn)行狀態(tài);當(dāng)處于運(yùn)行狀態(tài)的線程
失去
CPU時(shí)間片時(shí),該線程進(jìn)入就緒狀態(tài);但有一個(gè)方法例外
yield( )
:
使得線程放棄
當(dāng)前分得的CPU的時(shí)間片,
但是不使線程阻塞,即線程仍然處于就緒狀態(tài)
隨時(shí)可能再次分得CPU的時(shí)間片進(jìn)入執(zhí)行狀態(tài)
調(diào)用yield()方法可以讓運(yùn)行狀態(tài)的線程轉(zhuǎn)入就緒狀態(tài)
sleep()/suspend()/rusume()/yield()
均為T(mén)hread類(lèi)的方法,
wait()/notify()/notifyAll()
為Object類(lèi)的方法
對(duì)象鎖 和 監(jiān)視器
細(xì)心的朋友可能注意到以上提到了個(gè)名詞“監(jiān)視器
”,和“對(duì)象鎖
”,他們是個(gè)啥?
在JVM的規(guī)范中,有這么一些話:
“在JVM中,每個(gè)對(duì)象和類(lèi)在邏輯上都是和一個(gè)監(jiān)視器相關(guān)聯(lián)的”
“為了實(shí)現(xiàn)監(jiān)視器的排他性監(jiān)視能力,JVM為每一個(gè)對(duì)象和類(lèi)都關(guān)聯(lián)一個(gè)鎖”
“鎖住了一個(gè)對(duì)象,就是獲得對(duì)象相關(guān)聯(lián)的監(jiān)視器”
引用一個(gè)流傳很廣的例子來(lái)解釋
可以將監(jiān)視器比作一個(gè)建筑,
它有一個(gè)很特別的房間,
房間里有一些數(shù)據(jù),而且在同一時(shí)間只能被一個(gè)線程占據(jù),
一個(gè)線程從進(jìn)入這個(gè)房間到它離開(kāi)前,它可以獨(dú)占地訪問(wèn)房間中的全部數(shù)據(jù).
進(jìn)入這個(gè)建筑叫做"進(jìn)入監(jiān)視器"
進(jìn)入建筑中的那個(gè)特別的房間叫做"獲得監(jiān)視器"
占據(jù)房間叫做"持有監(jiān)視器"
離開(kāi)房間叫做"釋放監(jiān)視器"
離開(kāi)建筑叫做"退出監(jiān)視器"
而一個(gè)鎖就像一種任何時(shí)候只允許一個(gè)線程擁有的特權(quán),
一個(gè)線程可以允許多次對(duì)同一對(duì)象上鎖,
對(duì)于每一個(gè)對(duì)象來(lái)說(shuō),java虛擬機(jī)維護(hù)一個(gè)計(jì)數(shù)器,記錄對(duì)象被加了多少次鎖,
沒(méi)被鎖的對(duì)象的計(jì)數(shù)器是0,
線程每加鎖一次,計(jì)數(shù)器就加1,
每釋放一次,計(jì)數(shù)器就減1,
當(dāng)計(jì)數(shù)器跳到0的時(shí)候,鎖就被完全釋放了
Java中使用同步監(jiān)視器的代碼很簡(jiǎn)單,使用關(guān)鍵字 “synchronized
”即可
synchronized (obj){
//需要同步的代碼
//obj是同步監(jiān)視器
}
public synchronized void foo(){
//需同步的代碼
//當(dāng)前對(duì)象this是同步監(jiān)視器
}
關(guān)于synchronized
的詳細(xì)使用有很多注意點(diǎn), 我們后續(xù)單獨(dú)開(kāi)一章來(lái)講解.
阻塞狀態(tài)分類(lèi)
根據(jù)阻塞產(chǎn)生的原因不同,阻塞狀態(tài)又可以分為三種:
- 等待阻塞
運(yùn)行中的線程執(zhí)行wait()方法,該線程會(huì)釋放占用的所有資源對(duì)象,
JVM會(huì)把該線程放入該對(duì)象的“等待隊(duì)列”中,進(jìn)入這個(gè)狀態(tài)后,是不能自動(dòng)喚醒的,
必須依靠其他線程調(diào)用notify()或notifyAll()方法才能被喚醒,
喚醒后進(jìn)入“阻塞(同步隊(duì)列)”
- 同步阻塞
就緒狀態(tài)的線程,被分配了CPU時(shí)間片,
準(zhǔn)備執(zhí)行時(shí)發(fā)現(xiàn)需要的資源對(duì)象被synchroniza(同步)(資源對(duì)象被其它線程鎖住占用了),
獲取不到鎖標(biāo)記,該線程將會(huì)立即進(jìn)入鎖池狀態(tài),等待獲取鎖標(biāo)記,
這時(shí)的鎖池里,也許已經(jīng)有了其他線程在等待獲取鎖標(biāo)記,
這時(shí)它們處于隊(duì)列狀態(tài),既先到先得
一旦線程獲得鎖標(biāo)記后,就轉(zhuǎn)入就緒狀態(tài),繼續(xù)等待CPU時(shí)間片
- 其他阻塞
運(yùn)行的線程調(diào)用了自身的sleep()方法或其他線程的join()方法,
或者發(fā)出了I/O請(qǐng)求時(shí),JVM會(huì)把該線程置為阻塞狀態(tài).
當(dāng)sleep()狀態(tài)超時(shí)曹体、join()等待線程終止或者超時(shí)、
或者I/O處理完畢時(shí),線程重新轉(zhuǎn)入就緒狀態(tài),等待CPU分配時(shí)間片執(zhí)行
Thread類(lèi)相關(guān)方法介紹
看完以上阻塞的介紹,可能很多朋友對(duì)Thread類(lèi)的一些方法產(chǎn)生了疑問(wèn),下面我們來(lái)實(shí)際探究下這些方法的使用,相關(guān)的注意點(diǎn)我就直接寫(xiě)在注釋中方便閱讀
public class Thread{
// 線程的啟動(dòng)
public void start();
// 線程體,線程需要做的事
public void run();
// 已廢棄,停止線程
public void stop();
// 已廢棄,掛起線程,(不釋放對(duì)象鎖)
public void suspend();
// 已廢棄,喚醒掛起的線程
public void resume();
// 在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(不釋放對(duì)象鎖)
public static void sleep(long millis);
// 同上,增加了納秒?yún)?shù)(不釋放對(duì)象鎖)
public static void sleep(long millis,int nanos);
//線程讓步(不釋放對(duì)象鎖)
public static void yield();
// 測(cè)試線程是否處于活動(dòng)狀態(tài)
public boolean isAlive();
// 中斷線程
public void interrupt();
// 測(cè)試線程是否已經(jīng)中斷
public boolean isInterrupted();
// 測(cè)試當(dāng)前線程是否已經(jīng)中斷
public static boolean interrupted();
// 等待該線程終止
public void join() throws InterruptedException;
// 等待該線程終止的時(shí)間最長(zhǎng)為 millis 毫秒
public void join(long millis) throws InterruptedException;
// 等待該線程終止的時(shí)間最長(zhǎng)為 millis 毫秒 + nanos 納秒
public void join(long millis,int nanos) throws InterruptedException;
}
我們可以看出,Thread類(lèi)很多方法因?yàn)榫€程安全問(wèn)題已經(jīng)被棄用了,比如我們講的suspend()/resume()
, 因?yàn)樗鼤?huì)產(chǎn)生死鎖
現(xiàn)在
掛起是JVM的系統(tǒng)行為,我們無(wú)需干涉
suspend()/resume()產(chǎn)生死鎖的原因
當(dāng)
suspend()
的線程持有某個(gè)對(duì)象鎖,而resume()
它的線程又正好需要使用此鎖的時(shí)候,死鎖就產(chǎn)生了
舉個(gè)例子:
有兩個(gè)線程A和B,以及一個(gè)公共資源O
A執(zhí)行時(shí)需要O對(duì)象,所以A拿到了O鎖住,防止操作時(shí)O再被別的線程拿走,之后suspend掛起,
B呢,負(fù)責(zé)在適當(dāng)?shù)臅r(shí)候resume喚醒A,但是B執(zhí)行時(shí)也需要拿到O對(duì)象
此時(shí),死鎖產(chǎn)生了
A拿著O掛起,因?yàn)閞esume的實(shí)現(xiàn)機(jī)制,所以掛起時(shí)O不會(huì)被釋放,
只有A被resume喚醒繼續(xù)執(zhí)行完畢才能釋放O,
B本來(lái)負(fù)責(zé)喚醒A,但是B又拿不到O,
所以,A和B永遠(yuǎn)都在等待,執(zhí)行不了
說(shuō)到Thread類(lèi)的suspend和/resume
,順帶也提下Object類(lèi)的wait/notify
這對(duì)組合,wait/notify
屬于對(duì)象方法,意味著所有對(duì)象都會(huì)有這兩個(gè)方法.
wait/notify
這兩個(gè)方法同樣是等待/通知,但使用它們的前提是已經(jīng)獲得了鎖,且在wait(等待)期間會(huì)釋放鎖
線程要調(diào)用wait(),必須先獲得該對(duì)象的鎖
,在調(diào)用wait()之后,當(dāng)前線程釋放該對(duì)象鎖并進(jìn)入休眠
,只有以下幾種情況下會(huì)被喚醒
- 其他線程調(diào)用了該對(duì)象的notify()(隨機(jī)喚醒等待隊(duì)列的一個(gè)線程)
或notifyAll()(隨機(jī)等待隊(duì)列的所有線程);
- 當(dāng)前線程被中斷;
- 調(diào)用wait(3000)時(shí)指定的時(shí)間(3s)已到.
類(lèi)方法和對(duì)象方法區(qū)別( sleep() & wait() )
這里重點(diǎn)再?gòu)?qiáng)調(diào)下類(lèi)方法和對(duì)象方法的區(qū)別,我們以sleep
和wait
為例
sleep方法是Thread類(lèi)靜態(tài)方法,直接使用Thread.sleep()就可以調(diào)用
最好不要用Thread的實(shí)例對(duì)象調(diào)用它,因?yàn)樗叩氖冀K是當(dāng)前正在運(yùn)行的線程
它只對(duì)正在運(yùn)行狀態(tài)的線程對(duì)象有效
使用sleep方法時(shí),一定要用try catch處理InterruptedException異常
我們舉個(gè)例子
//定義一個(gè)線程類(lèi)
public class ImpRunnableThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("線程: " + Thread.currentThread().getName() + "第" + i + "次執(zhí)行攘乒!");
}
}
}
//測(cè)試
class Test {
public void main(String[] args){
Thread t = new Thread(new ImpRunnableThread());
t.start();
//很多人會(huì)以為睡眠的是t線程,但其實(shí)是main線程
t.sleep(5000);
for (int i = 0; i < 3; i++) {
System.out.println("線程: " + Thread.currentThread().getName() + "第" + i + "次執(zhí)行!");
}
}
先不說(shuō)直接 “對(duì)象.sleep()
” 這種使用方式本就不對(duì),
再者 t.sleep(5000)
很多人會(huì)以為是讓t線程睡眠5s,
但其實(shí)睡眠的是main線程!
我們執(zhí)行代碼,就可以看出,t線程會(huì)先執(zhí)行完畢,5s后主線程才會(huì)輸出!
那么問(wèn)題來(lái)了,如何讓t線程睡眠呢??
很簡(jiǎn)單,我們?cè)贗mpRunnableThread類(lèi)的run()方法中寫(xiě)sleep()即可!
public class ImpRunnableThread implements Runnable {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
//sleep()一定要try catch
try {
if (i == 2) {
//t線程睡眠5s
Thread.sleep(5000);
}
System.out.println("線程: " + Thread.currentThread().getName() + "第" + i + "次執(zhí)行碳竟!");
} catch (InterruptedException e) {
e.printStackTrace();
}}}
}
說(shuō)完類(lèi)方法,我們?cè)僬f(shuō)對(duì)象方法
對(duì)象鎖: 即針對(duì)一個(gè)“實(shí)例對(duì)象
”的鎖,java中,所有的對(duì)象都可以“鎖住
”,這里舉個(gè)簡(jiǎn)單的例子
//實(shí)例一個(gè)Object對(duì)象
Object lock = new Object();
//使用synchronized將lock對(duì)象鎖住
synchronized(lock){
//鎖保護(hù)的代碼塊
}
在Object對(duì)象中有三個(gè)方法wait()蜜徽、notify()腋么、notifyAll()
- wait()
wait()方法可以使調(diào)用該方法的線程釋放共享資源的鎖,
然后從運(yùn)行狀態(tài)退出,進(jìn)入阻塞(等待隊(duì)列),直到再次被喚醒(進(jìn)入阻塞(同步隊(duì)列)).
這里需要注意,形如wait(3000)這樣的帶參構(gòu)造,
無(wú)需其它線程notify()或notifyAll()喚醒,到了時(shí)間會(huì)自動(dòng)喚醒,
看似和sleep(3000)一樣,但其實(shí)是不同的
wait(3000)調(diào)用時(shí)會(huì)釋放對(duì)象鎖,3s過(guò)后,進(jìn)入阻塞(同步隊(duì)列),
競(jìng)爭(zhēng)到對(duì)象鎖后進(jìn)入就緒狀態(tài),而后cpu調(diào)度執(zhí)行.
所以,實(shí)際等待時(shí)間比3s會(huì)長(zhǎng)!!
而sleep(3000),不會(huì)釋放對(duì)象鎖,
3s過(guò)后,直接進(jìn)入就緒狀態(tài),等待cpu調(diào)度執(zhí)行.
- notify()
notify()方法可以隨機(jī)喚醒阻塞(等待隊(duì)列)中等待同一共享資源的一個(gè)線
程,并使得該線程退出等待狀態(tài),進(jìn)入阻塞(同步隊(duì)列)
- notifyAll()
notifyAll()和notify()類(lèi)似,不過(guò)它喚醒了阻塞(等待隊(duì)列)中等待
同一共享資源的所有線程
最后,如果wait()
方法和notify()/notifyAll()
方法不在同步方法/同步代碼塊中
被調(diào)用,那么虛擬機(jī)會(huì)拋出
java.lang.IllegalMonitorStateException
接下來(lái)我們來(lái)看看具體任何使用
定義一個(gè)等待線程WaitThread
class WaitThread extends Thread {
private Object lock;
public WaitThread(Object lock) {
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
System.out.println(
"start---" + Thread.currentThread().getName() + "---wait time = " + System.currentTimeMillis());
lock.wait();
System.out.println(
"end---" + Thread.currentThread().getName() + "---wait time = " + System.currentTimeMillis());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
定義一個(gè)喚醒線程N(yùn)otifyThread
class NotifyThread extends Thread {
private Object lock;
public NotifyThread(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(
"start---" + Thread.currentThread().getName() + "---wait time = " + System.currentTimeMillis());
lock.notify();
System.out.println(
"end---" + Thread.currentThread().getName() + "---wait time = " + System.currentTimeMillis());
}
}
}
測(cè)試代碼
public class Test {
public static void main(String[] args) throws Exception {
Object lock = new Object();
WaitThread w1 = new WaitThread(lock);
w1.setName("等待線程");
w1.start();
//main線程睡眠3s,便于我們看到效果
Thread.sleep(3000);
NotifyThread n1 = new NotifyThread(lock);
n1.setName("喚醒線程");
n1.start();
}
}
結(jié)果
start---等待線程---wait time = 1589425525994
start---喚醒線程---wait time = 1589425529001
end---喚醒線程---wait time = 1589425529001
end---等待線程---wait time = 1589425529001
結(jié)果可以看出,等待線程被喚醒線程喚醒后才繼續(xù)輸出.
需要注意的是,如果等待線程設(shè)置的是wait(3000)
,則無(wú)需喚醒線程喚醒,它自己在3s
后會(huì)繼續(xù)執(zhí)行.
等待隊(duì)列 & 同步隊(duì)
前面一直提到兩個(gè)概念,等待隊(duì)列(等待池)
,同步隊(duì)列(鎖池)
,這兩者是不一樣的.具體如下:
同步隊(duì)列(鎖池)
假設(shè)線程A已經(jīng)擁有了某個(gè)對(duì)象(注意:不是類(lèi))的鎖,
而其它的線程想要調(diào)用這個(gè)對(duì)象的某個(gè)synchronized方法(或者synchronized塊),
由于這些線程在進(jìn)入對(duì)象的synchronized方法之前必須先獲得該對(duì)象的鎖的擁有權(quán),
但是該對(duì)象的鎖目前正被線程A擁有,
所以這些線程就進(jìn)入了該對(duì)象的同步隊(duì)列(鎖池)中,
這些線程狀態(tài)為Blocked.
等待隊(duì)列(等待池)
假設(shè)一個(gè)線程A調(diào)用了某個(gè)對(duì)象的wait()方法,
線程A就會(huì)釋放該對(duì)象的鎖
(因?yàn)閣ait()方法必須出現(xiàn)在synchronized中,
這樣自然在執(zhí)行wait()方法之前線程A就已經(jīng)擁有了該對(duì)象的鎖),
同時(shí) 線程A就進(jìn)入到了該對(duì)象的等待隊(duì)列(等待池)中,
此時(shí)線程A狀態(tài)為Waiting.
如果另外的一個(gè)線程調(diào)用了相同對(duì)象的notifyAll()方法,
那么處于該對(duì)象的等待池中的線程
就會(huì)全部進(jìn)入該對(duì)象的同步隊(duì)列(鎖池)中,準(zhǔn)備爭(zhēng)奪鎖的擁有權(quán).
如果另外的一個(gè)線程調(diào)用了相同對(duì)象的notify()方法,
那么僅僅有一個(gè)處于該對(duì)象的等待池中的線程(隨機(jī))會(huì)進(jìn)入該對(duì)象的同步隊(duì)列(鎖池)
被notify()
或notifyAll()
喚起的線程是有規(guī)律
的
- 如果是通過(guò)notify來(lái)喚起的線程,那 先進(jìn)入wait的線程會(huì)先被喚起來(lái);
- 如果是通過(guò)nootifyAll喚起的線程,默認(rèn)情況是 最后進(jìn)入的會(huì)先被喚起來(lái),即LIFO的策略腾么;