通過(guò)本文檔你將學(xué)習(xí)到
- 進(jìn)程
- 線程
- 并發(fā)
- 并行
- java常用的并發(fā)工具,并發(fā)問(wèn)題以及解決方案
1 概述
本次分享主要講述JUC的一些相關(guān)知識(shí)音诫,這些知識(shí)會(huì)在平時(shí)工作中用到,面試的時(shí)候也是必問(wèn)的雪位。通過(guò)本文檔的學(xué)習(xí)竭钝,你大概清楚
本次主要都是講述理論知識(shí),理論也很重要茧泪,不要輕視蜓氨,面試的時(shí)候也會(huì)問(wèn)你的。問(wèn)你什么是線程队伟?什么是進(jìn)程?你說(shuō)繼承什么什么類幽勒,什么什么的嗜侮。就那,就那意思啥容。那不就GG了锈颗。
1.1 什么是JUC
在 Java 中,線程部分是一個(gè)重點(diǎn)咪惠,本篇文章說(shuō)的 JUC 也是關(guān)于線程的击吱。JUC
就是 java.util .concurrent 工具包的簡(jiǎn)稱。這是一個(gè)處理線程的工具包遥昧,JDK
1.5 開(kāi)始出現(xiàn)的覆醇。
1.2 進(jìn)程、線程炭臭、管程
1.線程是程序執(zhí)行的最小單位永脓,而進(jìn)程是操作系統(tǒng)分配資源的最小單位;
2.一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成,線程是一個(gè)進(jìn)程中代碼的不同執(zhí)行路線;
3.進(jìn)程之間相互獨(dú)立鞋仍,但同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段常摧、數(shù)據(jù)集、堆等)及一些進(jìn)程級(jí)的資源(如打開(kāi)文件和信號(hào)),某進(jìn)程內(nèi)的線程在其它進(jìn)程不可見(jiàn);
4.調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多落午;
5.進(jìn)程和線程都是由操作系統(tǒng)內(nèi)核所管理谎懦。
為線程的容器
簡(jiǎn)單說(shuō):以上都是書(shū)本官方語(yǔ)言。一個(gè)360殺毒軟件打開(kāi)就是一個(gè)進(jìn)程溃斋,你既可以掃描垃圾党瓮,同時(shí)也可以殺毒。開(kāi)啟了兩個(gè)線程盐类。大致意思如此寞奸。
兩者的對(duì)比
- 進(jìn)程基本上相互獨(dú)立的,而線程存在于進(jìn)程內(nèi)在跳,是進(jìn)程的一個(gè)子集
- 進(jìn)程擁有共享的資源枪萄,如內(nèi)存空間等,供其內(nèi)部的線程共享
- 進(jìn)程間通信較為復(fù)雜
同一臺(tái)計(jì)算機(jī)的進(jìn)程通信稱為 IPC(Inter-process communication)
不同計(jì)算機(jī)之間的進(jìn)程通信猫妙,需要通過(guò)網(wǎng)絡(luò)瓷翻,并遵守共同的協(xié)議,例如 HTTP - 線程通信相對(duì)簡(jiǎn)單割坠,因?yàn)樗鼈児蚕磉M(jìn)程內(nèi)的內(nèi)存齐帚,一個(gè)例子是多個(gè)線程可以訪問(wèn)同一個(gè)共享變量
- 線程更輕量,線程上下文切換成本一般上要比進(jìn)程上下文切換低
管程就是我們平時(shí)說(shuō)的鎖
monitor監(jiān)視鎖
1.3 并行和并發(fā)
單核 cpu 下彼哼,線程實(shí)際還是 串行執(zhí)行 的对妄。操作系統(tǒng)中有一個(gè)組件叫做任務(wù)調(diào)度器,將 cpu 的時(shí)間片(windows
下時(shí)間片最小約為 15 毫秒)分給不同的程序使用敢朱,只是由于 cpu 在線程間(時(shí)間片很短)的切換非臣袅猓快,人類感
覺(jué)是 同時(shí)運(yùn)行的 拴签⌒⒊#總結(jié)為一句話就是: 微觀串行,宏觀并行 蚓哩,
一般會(huì)將這種 線程輪流使用 CPU 的做法稱為并發(fā)构灸, concurrent
多核 cpu下,每個(gè) 核(core) 都可以調(diào)度運(yùn)行線程岸梨,這時(shí)候線程可以是并行的喜颁。
簡(jiǎn)單說(shuō):
家庭主婦做飯、打掃衛(wèi)生盛嘿、給孩子喂奶洛巢,她一個(gè)人輪流交替做這多件事,這時(shí)就是并發(fā)
家庭主婦雇了個(gè)保姆次兆,她們一起這些事稿茉,這時(shí)既有并發(fā),也有并行(這時(shí)會(huì)產(chǎn)生競(jìng)爭(zhēng),例如鍋只有一口漓库,一
個(gè)人用鍋時(shí)恃慧,另一個(gè)人就得等待)
雇了3個(gè)保姆,一個(gè)專做飯渺蒿、一個(gè)專打掃衛(wèi)生痢士、一個(gè)專喂奶,互不干擾茂装,這時(shí)是并行
1.4 線程的狀態(tài)
這是從 操作系統(tǒng) 層面來(lái)描述的
- 【初始狀態(tài)】?jī)H是在語(yǔ)言層面創(chuàng)建了線程對(duì)象,還未與操作系統(tǒng)線程關(guān)聯(lián)
- 【可運(yùn)行狀態(tài)】(就緒狀態(tài))指該線程已經(jīng)被創(chuàng)建(與操作系統(tǒng)線程關(guān)聯(lián))少态,可以由 CPU 調(diào)度執(zhí)行
- 【運(yùn)行狀態(tài)】指獲取了 CPU 時(shí)間片運(yùn)行中的狀態(tài)
當(dāng) CPU 時(shí)間片用完城侧,會(huì)從【運(yùn)行狀態(tài)】轉(zhuǎn)換至【可運(yùn)行狀態(tài)】,會(huì)導(dǎo)致線程的上下文切換 - 【阻塞狀態(tài)】
如果調(diào)用了阻塞 API彼妻,如 BIO 讀寫(xiě)文件嫌佑,這時(shí)該線程實(shí)際不會(huì)用到 CPU,會(huì)導(dǎo)致線程上下文切換 - 【阻塞狀態(tài)】
等 BIO 操作完畢侨歉,會(huì)由操作系統(tǒng)喚醒阻塞的線程屋摇,轉(zhuǎn)換至【可運(yùn)行狀態(tài)】
與【可運(yùn)行狀態(tài)】的區(qū)別是,對(duì)【阻塞狀態(tài)】的線程來(lái)說(shuō)只要它們一直不喚醒幽邓,調(diào)度器就一直不會(huì)考慮
調(diào)度它們 - 【終止?fàn)顟B(tài)】表示線程已經(jīng)執(zhí)行完畢炮温,生命周期已經(jīng)結(jié)束,不會(huì)再轉(zhuǎn)換為其它狀態(tài)
這是從 Java API 層面來(lái)描述的
根據(jù) Thread.State 枚舉颊艳,分為六種狀態(tài)
- NEW 線程剛被創(chuàng)建茅特,但是還沒(méi)有調(diào)用 start() 方法
- RUNNABLE 當(dāng)調(diào)用了 start() 方法之后,注意棋枕,Java API 層面的 RUNNABLE 狀態(tài)涵蓋了 操作系統(tǒng) 層面的
【可運(yùn)行狀態(tài)】、【運(yùn)行狀態(tài)】和【阻塞狀態(tài)】(由于 BIO 導(dǎo)致的線程阻塞妒峦,在 Java 里無(wú)法區(qū)分重斑,仍然認(rèn)為是可運(yùn)行) - BLOCKED , WAITING 肯骇, TIMED_WAITING 都是 Java API 層面對(duì)【阻塞狀態(tài)】的細(xì)分
- TERMINATED 當(dāng)線程代碼運(yùn)行結(jié)束
BLOCKED
Java文檔官方定義BLOCKED狀態(tài)是:“這種狀態(tài)是指一個(gè)阻塞線程在等待monitor鎖窥浪。”
真實(shí)生活例子:今天你要去面試笛丙。這是你夢(mèng)想的工作漾脂,你已經(jīng)盯著它多年了。你早上起來(lái)胚鸯,準(zhǔn)備好骨稿,穿上你最好的外衣,對(duì)著鏡子打理好。當(dāng)你走進(jìn)車庫(kù)發(fā)現(xiàn)你的老婆已經(jīng)把車開(kāi)走了坦冠。在這個(gè)場(chǎng)景形耗,你只有一輛車,所以怎么辦辙浑?在真實(shí)生活中激涤,可能會(huì)打架:-)。 現(xiàn)在因?yàn)槟憷习职衍囬_(kāi)走了你被BLOCKED了判呕。你不能去參加面試倦踢。
這就是BLOCKED狀態(tài)。用技術(shù)術(shù)語(yǔ)講侠草,你是線程T1辱挥,你老婆是線程T2而鎖是車。T1被BLOCKED在鎖(例子里的車)上梦抢,因?yàn)門2已經(jīng)獲取了這個(gè)鎖般贼。
小貼士:當(dāng)線程調(diào)用Object#wait()方法進(jìn)入一個(gè)synchronized塊/方法或重進(jìn)入一個(gè)synchronized鎖/方法時(shí)會(huì)等待獲取monitor鎖。
WAITING
Java文檔官方定義WAITING狀態(tài)是:“一個(gè)線程在等待另一個(gè)線程執(zhí)行一個(gè)動(dòng)作時(shí)在這個(gè)狀態(tài)”
真實(shí)生活例子:再看下幾分鐘后你的老婆開(kāi)車回家了“路裕現(xiàn)在你意識(shí)到快到面試時(shí)間了哼蛆,而開(kāi)車過(guò)去很遠(yuǎn)。所以你拼命地踩油門霞赫。限速120KM/H而你以160KM/H的速度在開(kāi)腮介。很不幸,一個(gè)交警發(fā)現(xiàn)你超速了端衰,讓你停到路邊〉矗現(xiàn)在你進(jìn)入了WAITING狀態(tài)。你聽(tīng)下車坐在那等著交警過(guò)來(lái)檢查你并放行旅东∶鹨郑基本上,只有等他讓你走抵代,你被卡在WAITING狀態(tài)了腾节。
用技術(shù)術(shù)語(yǔ)來(lái)講,你是線程T1而交警是線程T2荤牍。你釋放你的鎖(例子中你停下了車)案腺,并進(jìn)入WAITING狀態(tài),直到警察(例子中T2)讓你走康吵,你陷入了WAITING狀態(tài)劈榨。
小貼士:當(dāng)線程調(diào)用以下方法時(shí)會(huì)進(jìn)入WAITING狀態(tài):
Object#wait() 而且不加超時(shí)參數(shù)
Thread#join() 而且不加超時(shí)參數(shù)
LockSupport#park()
在對(duì)象上的線程調(diào)用了Object.wait()會(huì)進(jìn)入WAITING狀態(tài),直到另一個(gè)線程在這個(gè)對(duì)象上調(diào)用了Object.notify()或Object.notifyAll()方法才能恢復(fù)晦嵌。一個(gè)調(diào)用了Thread.join()的線程會(huì)進(jìn)入WAITING狀態(tài)直到一個(gè)特定的線程來(lái)結(jié)束同辣。
TIMED_WAITING
Java文檔官方定義TIMED_WAITING狀態(tài)為:“一個(gè)線程在一個(gè)特定的等待時(shí)間內(nèi)等待另一個(gè)線程完成一個(gè)動(dòng)作會(huì)在這個(gè)狀態(tài)”
真實(shí)生活例子:盡管充滿戲劇性拷姿,你在面試中做的非常好,驚艷了所有人并獲得了高薪工作邑闺。(祝賀你5啊)你回家告訴你的鄰居你的新工作并表達(dá)你激動(dòng)的心情。你的朋友告訴你他也在同一個(gè)辦公樓里工作陡舅。他建議你坐他的車去上班抵乓。你想這不錯(cuò)。所以第一天靶衍,你走到他的房子灾炭。在他的房子前停好你的車。你等了10分鐘颅眶,但你的鄰居沒(méi)有出現(xiàn)蜈出。你繼續(xù)開(kāi)自己的車去上班,這樣你不會(huì)在第一天就遲到涛酗。這就是TIMED_WAITING.
用技術(shù)術(shù)語(yǔ)來(lái)解釋铡原,你是線程T1而你的鄰居是線程T2。你釋放了鎖(這里是停止開(kāi)車)并等了足足10分鐘商叹。如果你的鄰居T2沒(méi)有來(lái)燕刻,你繼續(xù)開(kāi)車。
小貼士:調(diào)用了以下方法的線程會(huì)進(jìn)入TIMED_WAITING:
Thread#sleep()
Object#wait() 并加了超時(shí)參數(shù)
Thread#join() 并加了超時(shí)參數(shù)
LockSupport#parkNanos()
LockSupport#parkUntil()
1.4.1 小知識(shí)
wait/sleep 的區(qū)別
- sleep 是 Thread 的靜態(tài)方法剖笙,wait 是 Object 的方法卵洗,任何對(duì)象實(shí)例都能調(diào)用。
- sleep 不會(huì)釋放鎖弥咪,它也不需要占用鎖过蹂。wait 會(huì)釋放鎖,但調(diào)用它的前提
是當(dāng)前線程占有鎖(即代碼要在 synchronized 中)聚至。 - 它們都可以被 interrupted 方法中斷酷勺。
1.5 常見(jiàn)的線程API
1.6 部分方法講解
打斷標(biāo)記默認(rèn)就是false
isInterrupted
判斷是否被打斷 不會(huì)清除標(biāo)記。
interrupt()
打斷睡眠中的會(huì)拋異常扳躬,并且清除打斷標(biāo)記
interrupted()
1鸥印、判斷是否被打斷
2、清除打斷標(biāo)記坦报。
這里還要在介紹一個(gè)方法就是park unpark
這里還要在介紹一個(gè)方法就是park unpark
park函數(shù)是將當(dāng)前調(diào)用Thread阻塞,而unpark函數(shù)則是將指定線程Thread喚醒狂鞋。
與Object類的wait/notify機(jī)制相比片择,park/unpark有兩個(gè)優(yōu)點(diǎn):
以thread為操作對(duì)象更符合阻塞線程的直觀定義操作更精準(zhǔn),可以準(zhǔn)確地喚醒某一個(gè)線程(notify隨機(jī)喚醒一個(gè)線程骚揍,notifyAll喚醒所有等待的線程)字管,增加了靈活性啰挪。
關(guān)于“許可”
其實(shí)park/unpark的設(shè)計(jì)原理核心是“許可”:park是等待一個(gè)許可,unpark是為某線程提供一個(gè)許可嘲叔。
如果某線程A調(diào)用park亡呵,那么除非另外一個(gè)線程調(diào)用unpark(A)給A一個(gè)許可,否則線程A將阻塞在park操作上硫戈。
- 有一點(diǎn)比較難理解的锰什,是unpark操作可以再park操作之前。也就是說(shuō)丁逝,先提供許可汁胆。當(dāng)某線程調(diào)用park時(shí),已經(jīng)有許可了霜幼,它就消費(fèi)這個(gè)許可嫩码,然后可以繼續(xù)運(yùn)行。這其實(shí)是必須的罪既。
考慮最簡(jiǎn)單的生產(chǎn)者(Producer)消費(fèi)者(Consumer)模型:Consumer需要消費(fèi)一個(gè)資源铸题,于是調(diào)用park操作等待;Producer則生產(chǎn)資源琢感,然后調(diào)用unpark給予Consumer使用的許可丢间。非常有可能的一種情況是,Producer先生產(chǎn)猩谊,這時(shí)候Consumer可能還沒(méi)有構(gòu)造好(比如線程還沒(méi)啟動(dòng)千劈,或者還沒(méi)切換到該線程)。那么等Consumer準(zhǔn)備好要消費(fèi)時(shí)牌捷,顯然這時(shí)候資源已經(jīng)生產(chǎn)好了墙牌,可以直接用,那么park操作當(dāng)然可以直接運(yùn)行下去暗甥。如果沒(méi)有這個(gè)語(yǔ)義喜滨,那將非常難以操作。所以只要我們的 park/unpark 成對(duì)出現(xiàn)撤防,無(wú)論執(zhí)行順序如何虽风,都不會(huì)因此造成死鎖但是這個(gè)“許可”是不能疊加的,“許可”是一次性的寄月。比如線程B連續(xù)調(diào)用了三次unpark函數(shù)辜膝,當(dāng)線程A調(diào)用park函數(shù)就使用掉這個(gè)“許可”,如果線程A再次調(diào)用park漾肮,則進(jìn)入等待狀態(tài)厂抖。
park不需要獲取某個(gè)對(duì)象的鎖
因?yàn)橹袛嗟臅r(shí)候park不會(huì)拋出InterruptedException異常,所以需要在park之后自行判斷中斷狀態(tài)克懊,然后做額外的處理忱辅。
如果打斷標(biāo)記已經(jīng)是 true, 則 park 會(huì)失效
簡(jiǎn)單說(shuō):
每個(gè)線程都有自己的一個(gè) Parker 對(duì)象七蜘,由三部分組成 _counter , _cond 和 _mutex 打個(gè)比喻
線程就像一個(gè)旅人墙懂,Parker 就像他隨身攜帶的背包橡卤,條件變量就好比背包中的帳篷。_counter 就好比背包中
的備用干糧(0 為耗盡损搬,1 為充足)
調(diào)用 park 就是要看需不需要停下來(lái)歇息
如果備用干糧耗盡碧库,那么鉆進(jìn)帳篷歇息
如果備用干糧充足,那么不需停留场躯,繼續(xù)前進(jìn)
調(diào)用 unpark谈为,就好比令干糧充足
如果這時(shí)線程還在帳篷,就喚醒讓他繼續(xù)前進(jìn)
如果這時(shí)線程還在運(yùn)行踢关,那么下次他調(diào)用 park 時(shí)伞鲫,僅是消耗掉備用干糧,不需停留繼續(xù)前進(jìn)
因?yàn)楸嘲臻g有限签舞,多次調(diào)用 unpark 僅會(huì)補(bǔ)充一份備用干糧
1.7 interrupt()秕脓、interrupted()和isInterrupted()方法的應(yīng)用
扯了這么多,有啥用儒搭,面試用工作用唄吠架。停止線程你準(zhǔn)備怎么做?我知道直接stop,類似于不看電視了搂鲫,直接把電源拔了傍药,太暴力,你得給人家緩沖時(shí)間吧魂仍?疫情來(lái)了拐辽,通知你隔離,是不是接收到通知后擦酌,你得收拾下東西干嘛的俱诸。
這個(gè)模式叫做兩階段終止模式
- 錯(cuò)誤思路
- 使用線程對(duì)象的 stop() 方法停止線程
stop 方法會(huì)真正殺死線程,如果這時(shí)線程鎖住了共享資源赊舶,那么當(dāng)它被殺死后就再也沒(méi)有機(jī)會(huì)釋放鎖睁搭,
其它線程將永遠(yuǎn)無(wú)法獲取鎖 -
使用 System.exit(int) 方法停止線程
目的僅是停止一個(gè)線程,但這種做法會(huì)讓整個(gè)程序都停止
2 兩階段終止模式
兩階段終止模式.png
interrupt 可以打斷正在執(zhí)行的線程笼平,無(wú)論這個(gè)線程是在 sleep园骆,wait,還是正常運(yùn)行
package cn.itcast.pattern;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestTwoPhaseTermination")
public class TestTwoPhaseTermination {
public static void main(String[] args) throws InterruptedException {
TPTInterrupt t = new TPTInterrupt ();
t.start();
Thread.sleep(3500);
log.debug("stop");
t.stop();
}
}
@Slf4j(topic = "c.TPTInterrupt")
class TPTInterrupt {
private Thread thread;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("查詢數(shù)據(jù)庫(kù)看看有沒(méi)有變化寓调,巴拉巴拉巴拉");
} catch (InterruptedException e) {
current.interrupt();
}
}
},"監(jiān)控線程");
thread.start();
}
public void stop() {
thread.interrupt();
}
}
@Slf4j(topic = "c.TPTVolatile")
class TPTVolatile {
private Thread thread;
private volatile boolean stop = false;
public void start(){
thread = new Thread(() -> {
while(true) {
Thread current = Thread.currentThread();
if(stop) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("將結(jié)果保存");
} catch (InterruptedException e) {
}
}
},"監(jiān)控線程");
thread.start();
}
public void stop() {
stop = true;
thread.interrupt();
}
}
11:49:42.915 c.TwoPhaseTermination [監(jiān)控線程] - 將結(jié)果保存
11:49:43.919 c.TwoPhaseTermination [監(jiān)控線程] - 將結(jié)果保存
11:49:44.919 c.TwoPhaseTermination [監(jiān)控線程] - 將結(jié)果保存
11:49:45.413 c.TestTwoPhaseTermination [main] - stop
11:49:45.413 c.TwoPhaseTermination [監(jiān)控線程] - 料理后事