新聞中心
本文收錄于《Java并發(fā)編程》合集,本文主要介紹Java并發(fā)編程中終止線程的手段,通過本文您可以了解到:

創(chuàng)新互聯(lián)是一家專注于成都網(wǎng)站制作、做網(wǎng)站與策劃設(shè)計(jì),宿遷網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十載,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:宿遷等地區(qū)。宿遷做網(wǎng)站價(jià)格咨詢:18982081108
- 通過Thread類提供的方法中斷線程
- 中斷線程的應(yīng)用場(chǎng)景和代碼實(shí)現(xiàn),以及實(shí)現(xiàn)中的細(xì)節(jié)處理
- stop方法中斷線程存在的隱患
- LockSupport停止和喚醒線程
- LockSupport工具類的park和unpark的原理
原本的Java線程Thread類API中提供了stop這樣的終止線程的方法,但是已被標(biāo)記為過時(shí)方法,此方法來終止線程是暴力的不安全的,沒有對(duì)線程做后續(xù)的善后操作而直接終止,往往會(huì)埋下一些隱患。我們可以通過Java線程的中斷機(jī)制,來安全的停止線程。
Java提供了線程的中斷機(jī)制:設(shè)置線程的中斷標(biāo)志,可以使用它來決定是否結(jié)束一個(gè)線程。通過設(shè)置線程的中斷標(biāo)志并不能直接終止線程,這種機(jī)制其實(shí)就是告知線程我希望打斷你,至于到底停止不停止是由線程決定。
打斷線程場(chǎng)景
比如打斷或者重新執(zhí)行一些耗時(shí)過長(zhǎng)任務(wù),多線程同時(shí)完成同一個(gè)相同任務(wù),某一線程如果執(zhí)行完就通知其他線程可以停止。比如:
- 下載任務(wù),發(fā)現(xiàn)需要耗時(shí)過長(zhǎng),可以直接取消下載,其實(shí)就是打斷這個(gè)下載數(shù)據(jù)線程
- 比如搶票軟件,開啟多個(gè)線程搶多個(gè)車次的車票,如果某一個(gè)線程搶到,就通知其他線程可以終止
- 比如服務(wù)器中的超時(shí)操作,長(zhǎng)時(shí)間沒有獲取到數(shù)據(jù),就終止線程
你在哪里使用過打斷線程呢?
Thread類相關(guān)API
- void interrupt():中斷線程,例如線程A運(yùn)行時(shí),線程B調(diào)用線程A的interrupt方法來設(shè)置線程A的中斷標(biāo)志為true。注意:這里僅僅是設(shè)置了標(biāo)志,線程A并沒有中斷,它會(huì)繼續(xù)往下執(zhí)行。如果線程A調(diào)用了wait,join,sleep方法而被阻塞掛起,如果此時(shí)調(diào)用線程A的interrupt()方法,線程A會(huì)在調(diào)用這些方法的地方拋出InterruptedException異常而返回。
- boolean isInterrupted():檢測(cè)當(dāng)前線程是否被中斷,如果是返回true,否則返回false。
- boolean interrupted():檢測(cè)當(dāng)前線程是否被中斷,這個(gè)方法是Thread類的靜態(tài)方法;與interrupt()方法的區(qū)別在于,如果發(fā)現(xiàn)當(dāng)前線程被中斷,則會(huì)清除中斷標(biāo)志。
另外需要注意的是:interrupted()方法是獲取當(dāng)前調(diào)用線程【正在運(yùn)行的線程】的中斷標(biāo)志,而不是調(diào)用interrupted()方法的線程的中斷標(biāo)志。
打斷線程
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
// 開啟線程
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "運(yùn)行......");
// 線程進(jìn)入睡眠狀態(tài)
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1線程");
// 啟動(dòng) t1 線程
t1.start();
// 等待1秒后打斷t1線程
Thread.sleep(1000);
System.out.println("打斷......");
// 打斷線程
t1.interrupt();
// 查看打斷標(biāo)記
System.out.println("打斷標(biāo)記:" + t1.isInterrupted());
}
}
運(yùn)行結(jié)果:
- 調(diào)用 interrupted方法之后如果被打斷的線程【t1】線程中調(diào)用sleep、wait、join方法會(huì)觸發(fā)異常
- 調(diào)用isInterrupted方法獲取打斷標(biāo)記,true為打斷,false為未打斷
線程中調(diào)用wait方法
public class InterruptThread {
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建線程1
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "運(yùn)行......");
},"t1線程");
// 創(chuàng)建線程2
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "運(yùn)行......");
// 調(diào)用wait方法
try {
t1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t2線程");
// 啟動(dòng)線程
t1.start();
t2.start();
// t2線程插隊(duì)執(zhí)行
t2.join();
// 打斷t2線程
t2.interrupt();
// 查看t2線程打斷標(biāo)記
System.out.println("t2打斷標(biāo)記:" + t2.isInterrupted());
}
}
運(yùn)行結(jié)果:
- 調(diào)用wait方法之后仍然會(huì)觸發(fā)異常
- 打斷標(biāo)記被重置變?yōu)閒alse,所以可以在catch塊中設(shè)置打斷標(biāo)記為true
終止線程
此時(shí)可以根據(jù)打斷標(biāo)記,在線程內(nèi)部判斷是否需要打斷
案例:小明放假回家,媽媽想著給小明做好吃的,但是小明失戀了沒有胃口,就給媽媽說不要做了,媽媽收到打斷消息之后就停止做飯。
public class KitChenThread {
public static void main(String[] args) throws InterruptedException {
// 創(chuàng)建媽媽線程
Thread t1 = new Thread(() -> {
// 整東西吃
System.out.println("兒子放假了,整個(gè)燴面!吭哧吭哧~~~");
while (true) {
// 獲取當(dāng)前線程打斷狀態(tài)
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
System.out.println("whis today,餓你輕!");
// 停止
break;
}
}
},"媽媽線程:");
// 創(chuàng)建小明線程
Thread t2 = new Thread(() -> {
System.out.println("媽,我不想吃,你別弄了......");
// 打斷媽媽線程
t1.interrupt();
},"小明線程:");
// 啟動(dòng)線程
t1.start();
t2.start();
// 控制執(zhí)行順序
t1.join();
t2.join();
}
}
運(yùn)行結(jié)果:媽媽線程做飯,當(dāng)小明線程打斷之后,媽媽線程就收到打斷信息,停止運(yùn)行
通過代碼我們發(fā)現(xiàn),其實(shí)線程的終止權(quán)在被打斷的線程中,通過判斷打斷標(biāo)記來控制是否終止,也就是小明不讓媽媽做飯,媽媽可以不做也可以繼續(xù)做。
停止線程其他方法
- 使用Thread類中的stop()方法
- 調(diào)用 stop() 方法會(huì)立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 語(yǔ)句中的,并拋出ThreadDeath異常(通常情況下此異常不需要顯示的捕獲),因此可能會(huì)導(dǎo)致一些清理性的工作的得不到完成,如關(guān)閉文件數(shù)據(jù)流,關(guān)閉數(shù)據(jù)庫(kù)連接等。
- 調(diào)用 stop() 方法會(huì)立即釋放該線程所持有的所有的鎖,導(dǎo)致數(shù)據(jù)得不到同步,出現(xiàn)數(shù)據(jù)不一致的問題。
- 使用System.exit(int)方法
- 該方法會(huì)直接停止JVM,不單單停止一個(gè)線程,殺傷力太大
線程終止案例
小明是大強(qiáng)的秘書,負(fù)責(zé)記錄會(huì)議內(nèi)容,小明會(huì)每1秒記錄一次重要講話內(nèi)容,當(dāng)會(huì)議結(jié)束后,就終止記錄工作,但是在終止時(shí)會(huì)再整理一下會(huì)議內(nèi)容
會(huì)議線程:
public class MeetingThread {
// 開會(huì)
public void meeting() {
while (true) {
// 判斷是否結(jié)束會(huì)議
if(Thread.currentThread().isInterrupted()) {
System.out.println("會(huì)議結(jié)束,整理會(huì)議記錄");
break;
}
// 每1秒記錄一次重要內(nèi)容
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "記錄會(huì)議內(nèi)容......");
} catch (InterruptedException e) {
// 調(diào)用sleep,join,wait等方法,發(fā)生異常,打斷標(biāo)記會(huì)被重新設(shè)置為false,所以需要再次打斷
Thread.currentThread().interrupt();
}
}
}
}
測(cè)試類:
public class MeetingThreadMain {
public static void main(String[] args) throws InterruptedException {
MeetingThread meeting = new MeetingThread();
// 小明線程
Thread xiaoming = new Thread(() -> {
// 記錄會(huì)議內(nèi)容
meeting.meeting();
},"小明:");
xiaoming.start();
// 5秒后結(jié)束會(huì)議
Thread.sleep(5000);
// 打斷小明線程
xiaoming.interrupt();
System.out.println("會(huì)議結(jié)束......");
}
}
運(yùn)行結(jié)果:
如果沒有在catch中重新打斷線程,則會(huì)不斷記錄下去,記?。寒?dāng)線程中調(diào)用了sleep,wait,join方法時(shí),線程的打斷標(biāo)記會(huì)被重置,需要在catch塊中重新打斷
isInterrupted 和 interrupted區(qū)別
文章開頭介紹過,兩個(gè)方法都是判斷線程的打斷狀態(tài),interrupted 是Thread類的靜態(tài)方法,獲取打斷狀態(tài)之后會(huì)重置打斷狀態(tài)為false,而isInterrupted是Thread對(duì)象的方法,非靜態(tài)方法不會(huì)重置打斷狀態(tài)
isInterrupted 方法
interrupted方法
發(fā)現(xiàn)查看小明線程狀態(tài)時(shí)已經(jīng)變?yōu)?false
注意:Thread.interrupted查看的是正在執(zhí)行的線程的狀態(tài),而isInterrupted是可以指定線程的,根據(jù)線程變量名查看狀態(tài)
LockSupport
LockSupport是JUC中一個(gè)工具類,構(gòu)造方法私有,并且不存在獲取實(shí)例的靜態(tài)方法,提供了一堆靜態(tài)方法幫助完成對(duì)線程的操作,主要是為了暫停和恢復(fù)線程,主要方法有兩個(gè):
- park():暫停線程
- unpark(Thread thread):恢復(fù)指定線程對(duì)象運(yùn)行
park停止當(dāng)前線程
import java.util.concurrent.locks.LockSupport;
public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(current.getName() + "啟動(dòng),被park");
// 停止當(dāng)前線程
LockSupport.park();
System.out.println(current.getName() + "park之后");
// 輸出線程狀態(tài)
System.out.println(current.getName() + "打斷狀態(tài)" + current.isInterrupted());
}, "t1");
t1.start();
}
}
運(yùn)行結(jié)果:發(fā)現(xiàn)線程執(zhí)行啟動(dòng)后就停止,因?yàn)橛龅搅薒ockSupport.park();導(dǎo)致t1線程停止運(yùn)行,但是線程還未結(jié)束,所以程序并未停止【左側(cè)紅色小框標(biāo)識(shí)】
打斷park線程:如下圖,在main線程中,等待 1秒 之后,執(zhí)行t1.interrupt();,打斷t1線程,此時(shí)t1線程已經(jīng)被停止運(yùn)行,發(fā)現(xiàn)調(diào)用中斷方法之后,t1線程又繼續(xù)執(zhí)行,并且打斷標(biāo)記為true
park細(xì)節(jié)
如果park之后,阻塞線程,繼續(xù)執(zhí)行,再次調(diào)用LockSupport.park();方法阻塞線程時(shí)無效,如下進(jìn)行阻塞二次:
import java.util.concurrent.locks.LockSupport;
public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(current.getName() + "啟動(dòng),被park");
// 停止當(dāng)前線程
LockSupport.park();
System.out.println(current.getName() + "park之后");
// 輸出線程狀態(tài)
System.out.println(current.getName() + "打斷狀態(tài)" + current.isInterrupted());
// 再次打斷線程
LockSupport.park();
System.out.println(current.getName() + "再次打斷無效");
}, "t1");
t1.start();
// 停止1S后,打斷t1線程
Thread.sleep(1000);
t1.interrupt();
}
}
運(yùn)行結(jié)果:
如果你還想再次打斷,可以調(diào)用一次 Thread.interrupted() 獲取線程打斷狀態(tài),并且再設(shè)置為false,發(fā)現(xiàn)已成功阻塞
unpark:恢復(fù)指定線程對(duì)象
- t1線程執(zhí)行2S后被阻塞
- 主線程中等待1S后喚醒t1線程
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.locks.LockSupport;
public class LockSupportMain {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
Thread current = Thread.currentThread();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":?jiǎn)?dòng)");
try {
// 睡眠1秒后停止線程
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":park");
// 停止當(dāng)前線程
LockSupport.park();
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-" + current.getName() + ":繼續(xù)執(zhí)行");
}, "t1");
// 啟動(dòng)t1線程
t1.start();
// 停止2S
Thread.sleep(2000);
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")) + "-喚醒t1線程");
// 喚醒t1線程
LockSupport.unpark(t1);
}
}
執(zhí)行后發(fā)現(xiàn):t1線程被正常喚醒,繼續(xù)執(zhí)行
如果我們調(diào)換一下阻塞和喚醒的順序,是否仍然可以正常喚醒呢?也就是:先喚醒t1線程,t1線程再進(jìn)入阻塞狀態(tài),發(fā)現(xiàn)仍然可以喚醒成功
為什么可以先調(diào)用unpark在調(diào)用park呢?
這是一道高頻面試題,在下邊也有總結(jié),在這里我們不妨先分析一下,這涉及到了 park和unpark原理
當(dāng)我們調(diào)用park方法時(shí)其實(shí)調(diào)用的是 sun.misc.Unsafe UNSAFE 類的 park 方法,這里提到【許可證】這個(gè)詞,就是park和unpark的關(guān)鍵
每個(gè)線程都有一個(gè)自己的Parker對(duì)象,該對(duì)象由三部分組成_counter、_cond、_mutex
可以想一下,unpark其實(shí)就相當(dāng)于一個(gè)許可,告訴特定工廠你可以繼續(xù)生產(chǎn),特定工廠想要park停止生產(chǎn)的時(shí)候一看到有許可,就可以會(huì)繼續(xù)運(yùn)行。因此其執(zhí)行順序可以顛倒。
Parker實(shí)例:
park方法:
當(dāng)調(diào)用park()時(shí),先嘗試能否直接拿到【許可】,即_counter>0,如果獲取成功,則把_counter設(shè)置為0,并返回
如果不成功,則構(gòu)造一個(gè)ThreadBlockInVM,然后檢查_counter是不是>0,如果是,則把_counter設(shè)置為0,并返回
否則,再判斷等待的時(shí)間,然后再調(diào)用pthread_cond_wait函數(shù)等待,如果等待返回,則把_counter設(shè)置為0并返回:
這就是整個(gè)park的過程,總結(jié)來說就是消耗【許可】的過程。
unpark方法
JDK源碼unpark方法其實(shí)也是調(diào)用了sun.misc.Unsafe UNSAFE類的unpark方法,注釋的意思是給線程【許可證】
當(dāng)調(diào)用unpark()時(shí),直接設(shè)置_counter為1,如果_counter之前的值是0,則還要調(diào)用pthread_cond_signal喚醒在park中等待的線程:
_counter的值最大為1,即使多次調(diào)用unpark()許可證的個(gè)數(shù)也最多是1
即使我們先調(diào)用了unpark,再調(diào)用park也是可以正常喚醒線程的,因?yàn)閡npark獲取了一個(gè)許可證,之后再調(diào)用park方法,就可以名正言順的憑證消費(fèi),故不會(huì)阻塞。
思考:如果喚醒兩次后阻塞兩次,會(huì)是什么結(jié)果呢?
許可的數(shù)量最多為1,連續(xù)調(diào)用兩次unpark和調(diào)用一次unpark效果一樣,只會(huì)增加一個(gè)許可;而調(diào)用兩次park卻需要消費(fèi)兩個(gè)許可,證不夠,不能放行。線程就會(huì)阻塞
總結(jié)
- 調(diào)用park()時(shí)判斷許可是否大于0,如果大于0繼續(xù)運(yùn)行,如果不大于0線程就進(jìn)入阻塞,此時(shí)會(huì)再創(chuàng)建一個(gè) ThreadBlockInVM 許可是否大于0,如果大于0就將許可設(shè)置為0,放行運(yùn)行
- 調(diào)用unpark()將許可設(shè)置為1,無論調(diào)用多少次都是1,如果許可為0,則還會(huì)調(diào)用 pthread_cond_signal喚醒在park中的線程
- 先調(diào)用unpark,再調(diào)用park,線程仍然可以被喚醒繼續(xù)執(zhí)行
根據(jù)合集中《Java線程通信》一文,我們將線程阻塞和喚醒的4種方案介紹完畢,如果在工作或者面試中碰到一定要想起來使用方法和細(xì)節(jié)
文章出自:石添的編程哲學(xué),如有轉(zhuǎn)載本文請(qǐng)聯(lián)系【石添的編程哲學(xué)】今日頭條號(hào)。
網(wǎng)站欄目:領(lǐng)導(dǎo)說誰(shuí)再用Stop直接下崗,這樣終止線程更優(yōu)雅
文章來源:http://m.fisionsoft.com.cn/article/dhjpojh.html


咨詢
建站咨詢
