提示:下面講解線(xiàn)程時(shí)劫侧,如果不特別聲明,默認(rèn)是一個(gè)線(xiàn)程運(yùn)行在一個(gè)CPU上
1、概述
進(jìn)程應(yīng)用程序向操作系統(tǒng)申請(qǐng)計(jì)算機(jī)資源(文件句柄烧栋,內(nèi)存等)的最小單位写妥,而線(xiàn)程是進(jìn)程中可獨(dú)立執(zhí)行的最小單位。一個(gè)進(jìn)程中可以包含多個(gè)線(xiàn)程审姓,這些線(xiàn)程共享這個(gè)進(jìn)程的資源珍特。線(xiàn)程所以完成的計(jì)算工作叫做任務(wù),比如文件下載魔吐,解壓縮等都屬于一個(gè)任務(wù)扎筒。
2、線(xiàn)程:
1酬姆、線(xiàn)程在Java中作為
Thread
類(lèi)的實(shí)例存在嗜桌,創(chuàng)建線(xiàn)程的兩種方式new Thread(),new Thread(new Runnable())
辞色,主要區(qū)別在于前者通過(guò)繼承
骨宠,耦合性較高,后者通過(guò)組合
相满,耦合性較低层亿。另外通過(guò)組合還可以實(shí)現(xiàn)多個(gè)線(xiàn)程共享Runnable
實(shí)例的數(shù)據(jù)
2、線(xiàn)程的start
方法只能被調(diào)用一次立美,再次調(diào)用會(huì)拋出IllegalThreadStateException
異常
3匿又、創(chuàng)建線(xiàn)程對(duì)象時(shí),還會(huì)給線(xiàn)程的調(diào)用棧Call Stack
分配內(nèi)存,有些版本的虛擬機(jī)還會(huì)給線(xiàn)程關(guān)聯(lián)一個(gè)內(nèi)核線(xiàn)程建蹄,所以線(xiàn)程對(duì)象的創(chuàng)建比普通對(duì)象的成本高一些
4碌更、Java中的線(xiàn)程可分為守護(hù)線(xiàn)程 / 非守護(hù)線(xiàn)程,應(yīng)用線(xiàn)程 / 垃圾收集線(xiàn)程
5躲撰、線(xiàn)程是否是守護(hù)線(xiàn)程和父線(xiàn)程相同而且線(xiàn)程優(yōu)先級(jí)相等,并且可以通過(guò)Thread.setDaemon(Boolean)
方法更改線(xiàn)程類(lèi)型击费,并且這個(gè)方法要在線(xiàn)程被啟動(dòng)之前調(diào)用
6拢蛋、Java中線(xiàn)程基本狀態(tài):NEW
(已創(chuàng)建但是未start
),RUNNABLE
(運(yùn)行中或者就緒隊(duì)列上等待運(yùn)行)蔫巩,BLOCKED
(阻塞谆棱,特指在申請(qǐng)某些互斥資源失敗后的狀態(tài)),WAITING
(特指調(diào)用了Object.wait()或者LookSupport.park()
后的狀態(tài)),TIMED_WAITING
(指可設(shè)置等待時(shí)間的WAITING
狀態(tài))圆仔,TERMINATED
(線(xiàn)程執(zhí)行結(jié)束的狀態(tài))
7垃瞧、理解線(xiàn)程饑餓、死鎖坪郭、鎖死个从、活鎖的概念
3、多線(xiàn)程編程的目標(biāo)與挑戰(zhàn)
1、理解串行嗦锐,并發(fā)嫌松,并行在生活中的概念以及在編程中的概念
2、計(jì)算的正確性依賴(lài)于相對(duì)時(shí)間的順序或者線(xiàn)程的交錯(cuò)奕污,這種現(xiàn)象就叫做競(jìng)態(tài)
3萎羔、理解原子性,可見(jiàn)性和有序性的基本概念碳默,理解數(shù)據(jù)訪(fǎng)問(wèn)的概念:通常讀寫(xiě)操作叫做數(shù)據(jù)訪(fǎng)問(wèn)
4贾陷、原子性問(wèn)題:32位JVM訪(fǎng)問(wèn)double、long變量嘱根,讀-修改-寫(xiě)髓废,先檢驗(yàn)后執(zhí)行。對(duì)于第一個(gè)解決方法可以使用volatile保證變量的訪(fǎng)問(wèn)原子性儿子,可以使用鎖保證一系列操作的原子性瓦哎。
5、可見(jiàn)性問(wèn)題:JIT將程序當(dāng)做單線(xiàn)程應(yīng)用編譯時(shí)指令重排序優(yōu)化導(dǎo)致線(xiàn)程壓根沒(méi)有按照代碼編寫(xiě)順序執(zhí)行任務(wù)和訪(fǎng)問(wèn)數(shù)據(jù)柔逼、線(xiàn)程更新的數(shù)據(jù)其他線(xiàn)程無(wú)法及時(shí)讀取蒋譬。常見(jiàn)的是第二種,可簡(jiǎn)單將JVM內(nèi)存模型抽象為四層:CPU愉适,寫(xiě)緩沖器犯助,高速緩存,主內(nèi)存(其實(shí)不止维咸,還有寄存器等其他計(jì)算機(jī)元件)剂买。當(dāng)一個(gè)線(xiàn)程在一個(gè)CPU上運(yùn)行,更新數(shù)據(jù)先將數(shù)據(jù)更新到寫(xiě)緩沖器癌蓖,并且根據(jù)一定策略同步到高速緩存和主內(nèi)存瞬哼。線(xiàn)程A所在的CPU是無(wú)法直接讀取線(xiàn)程B所在CPU的的寫(xiě)緩沖器數(shù)據(jù)的,但是A-CPU可以通過(guò)緩存一致性協(xié)議同步B-CPU高速緩存中的內(nèi)容(緩存同步)或者讀取主存上的內(nèi)容租副。將當(dāng)前線(xiàn)程所在CPU的寫(xiě)緩沖器的內(nèi)容同步到自己的高速緩存或者主內(nèi)存的過(guò)程叫做沖刷處理器緩存坐慰,而將主內(nèi)存或者其他CPU高速緩存中的內(nèi)容更新到當(dāng)前線(xiàn)程所在CPU的高速緩存過(guò)程叫做刷新處理器緩存玫荣∫校可以通過(guò)volatile提醒編譯器變某些變量可能用于多線(xiàn)程環(huán)境下來(lái)取消其默認(rèn)的指令重排序優(yōu)化述召,而且能夠使CPU在更新volatile變量后立刻沖刷處理器緩存奸笤,在讀取volatile變量之前必定進(jìn)行刷新處理器緩存啡彬》保可見(jiàn)性得以保證只能讓線(xiàn)程計(jì)算任務(wù)導(dǎo)致的變量更新結(jié)果第一時(shí)間讓其他線(xiàn)程看到沮脖,但是依然存在原子性問(wèn)題:比如兩個(gè)線(xiàn)程在兩個(gè)CPU上同時(shí)進(jìn)行計(jì)算任務(wù)其中一個(gè)線(xiàn)程的計(jì)算結(jié)果會(huì)被覆蓋凯正。所以衍生出了相對(duì)新值和最新值的概念院仿,即可見(jiàn)性只能保證當(dāng)前線(xiàn)程CPU讀取到的是一個(gè)相對(duì)新值秸抚,如果想要讀取最新值還需要原子性得以保證速和。而對(duì)于單處理器的計(jì)算機(jī),通過(guò)時(shí)間片調(diào)度來(lái)實(shí)現(xiàn)多線(xiàn)程耸别,CPU寄存器中的變量屬于線(xiàn)程上下文健芭,上下文切換時(shí)如果寄存器的內(nèi)容沒(méi)有刷新到高速緩存或者主存依然會(huì)造成線(xiàn)程間變量的不可見(jiàn)性。
6秀姐、有序性問(wèn)題:重排序是編譯器(普通編譯器慈迈,JIT編譯器)、處理器省有、存儲(chǔ)子系統(tǒng)(寫(xiě)緩沖器痒留,高速緩存)在不影響單線(xiàn)程程序正確性的前提下,對(duì)內(nèi)存訪(fǎng)問(wèn)操作所做的一種優(yōu)化蠢沿。這種優(yōu)化在多線(xiàn)程環(huán)境下可能會(huì)導(dǎo)致源代碼順序伸头,程序順序,執(zhí)行順序舷蟀,感知順序出現(xiàn)不一致的情況恤磷,被稱(chēng)作有序性問(wèn)題。常見(jiàn)的重排序類(lèi)型及表現(xiàn)如下:
重排序類(lèi)型 | 重排序來(lái)源 | 重排序表現(xiàn) |
---|---|---|
指令重排序 | javac編譯器(靜態(tài)編譯器) | 程序順序與源碼順序不一致 |
指令重排序 | JIT編譯器(動(dòng)態(tài)編譯器)野宜、處理器 | 執(zhí)行順序與程序順序不一致 |
存儲(chǔ)子系統(tǒng)重排序(內(nèi)存重排序) | 高速緩存扫步,寫(xiě)緩沖器 | 源代碼順序、程序順序匈子、執(zhí)行順序一致河胎,感知順序不同 |
實(shí)際編程中,靜態(tài)編譯器指令重排序的概率很低虎敦,比較經(jīng)典的動(dòng)態(tài)編譯器指令重排序是在創(chuàng)建對(duì)象時(shí)
Object A = new Object();
在多線(xiàn)程環(huán)境中游岳,可能僅僅為對(duì)象分配了內(nèi)存但是構(gòu)造函數(shù)Object()
還沒(méi)有執(zhí)行完,就已經(jīng)將這個(gè)內(nèi)存地址賦值給A
其徙,這就導(dǎo)致其他線(xiàn)程可以訪(fǎng)問(wèn)A
胚迫,但是由于構(gòu)造函數(shù)尚未執(zhí)行完畢,在讀取A
的數(shù)據(jù)時(shí)拋出異常唾那。
現(xiàn)代處理器在運(yùn)行程序時(shí)會(huì)順序的一條條讀取編譯好的指令访锻,即順序讀取,但是指令執(zhí)行所需的操作數(shù)是各不相同的通贞,處理器對(duì)于操作數(shù)已經(jīng)就緒的指令會(huì)優(yōu)先執(zhí)行朗若,可能會(huì)導(dǎo)致執(zhí)行的順序(執(zhí)行順序)和指令讀取的順序(程序順序)是不相同的恼五,即亂序執(zhí)行昌罩。但是處理器的計(jì)算結(jié)果會(huì)先存入到重排序緩沖區(qū),等到相關(guān)的操作計(jì)算完成再按照讀取順序一次性提交到寄存器灾馒、高速緩存或者主存茎用,即順序提交。此外處理器還會(huì)進(jìn)行猜測(cè)執(zhí)行,也可能導(dǎo)致執(zhí)行重排序轨功。但是這些重排序在單線(xiàn)程環(huán)境下不會(huì)有任何問(wèn)題旭斥,多線(xiàn)程環(huán)境下如果不采取某種措施則會(huì)超出我們的設(shè)想。
我們將寄存器古涧、寫(xiě)緩沖器垂券、高速緩存稱(chēng)為存儲(chǔ)子系統(tǒng),處理器在存儲(chǔ)子系統(tǒng)和主存之間有兩種內(nèi)存訪(fǎng)問(wèn)操作羡滑。將數(shù)據(jù)從存儲(chǔ)子系統(tǒng)寫(xiě)入到主存即Store操作菇爪,還有就是將數(shù)據(jù)從主內(nèi)存加載到存儲(chǔ)子系統(tǒng),即Load操作柒昏。指令重排序是編譯器和處理器實(shí)實(shí)在在的改變了指令的順序凳宙,重排序的對(duì)象就是指令,是一種動(dòng)作职祷。而內(nèi)存重排序則是一種現(xiàn)象氏涩,重排序的對(duì)象是指令執(zhí)行的結(jié)果(內(nèi)存訪(fǎng)問(wèn)操作),假設(shè)當(dāng)前線(xiàn)程的處理器和編譯器都按照源代碼順序進(jìn)行工作有梆,有兩個(gè)指令M1和M2是尖,經(jīng)過(guò)執(zhí)行后需要進(jìn)行內(nèi)存訪(fǎng)問(wèn)操作O1和O2將執(zhí)行結(jié)果同步到主存上,處理器首先將操作結(jié)果寫(xiě)入到存儲(chǔ)子系統(tǒng)淳梦,但是存儲(chǔ)子系統(tǒng)比如高速緩沖區(qū)析砸,為了提高效率,在于主存進(jìn)行同步時(shí)采取了某種算法爆袍,打亂了O1和O2的順序首繁,這在其他線(xiàn)程(處理器)看來(lái),可能是O2→O1陨囊,即感知順序和源代碼順序不同弦疮,由于內(nèi)存訪(fǎng)問(wèn)操作只分兩種,所以?xún)?nèi)存重排序共有 2 * 2 = 4種:LoadLoad蜘醋、LoadStore胁塞、StoreLoad、StoreStore压语。
所有的重排序指令之間都遵循特定的規(guī)則從而給單線(xiàn)程應(yīng)用造成一種是按照源碼順序執(zhí)行的假象啸罢,這種假象叫做貌似串行化語(yǔ)義。而多線(xiàn)程環(huán)境下則無(wú)法保證這種假象胎食。
而這個(gè)特定的規(guī)則就是沒(méi)有數(shù)據(jù)依賴(lài)扰才。如果兩個(gè)指令涉及同一變量,且至少有一個(gè)指令中包含對(duì)變量的寫(xiě)操作厕怜,則這兩個(gè)指令存在數(shù)據(jù)依賴(lài)衩匣,即這兩個(gè)指令一定不會(huì)被重排序蕾总,另外需要注意的是指令間只存在控制依賴(lài)關(guān)系的話(huà)是允許進(jìn)行重排序的。
對(duì)于單處理多線(xiàn)程則比較特殊琅捏,只有靜態(tài)處理器的指令重排序會(huì)影響多線(xiàn)程的正確性生百。而對(duì)于運(yùn)行時(shí)期的指令重排序如處理器亂序執(zhí)行,JIT編譯器柄延,存儲(chǔ)子系統(tǒng)重排序都不會(huì)對(duì)其產(chǎn)生影響蚀浆。因?yàn)閱翁幚砥鞫嗑€(xiàn)程是依靠時(shí)間片和上下文切換來(lái)實(shí)現(xiàn)的,而上下文的切換是等到重排序的相關(guān)指令都執(zhí)行完之后才進(jìn)行搜吧,所以對(duì)于其他線(xiàn)程來(lái)說(shuō)就像沒(méi)有發(fā)生過(guò)重排序一樣蜡坊。
7、線(xiàn)程所執(zhí)行的任務(wù)的進(jìn)度信息(寄存器內(nèi)容赎败,程序計(jì)數(shù)器內(nèi)容等)就是線(xiàn)程上下文秕衙。運(yùn)行中的線(xiàn)程丟失處理器使用權(quán)被暫停,進(jìn)度信息被暫存到內(nèi)存僵刮,另一個(gè)線(xiàn)程獲得處理器使用權(quán)并且從內(nèi)存中載入自己的進(jìn)度信息然后開(kāi)始或者繼續(xù)運(yùn)行据忘,這樣的過(guò)程就是上下文切換。從Java應(yīng)用的角度來(lái)說(shuō)搞糕,一個(gè)線(xiàn)程從RUNNABLE
變成非RUNNABLE
(暫停線(xiàn)程)的過(guò)程就叫做上下文切換勇吊,而一個(gè)線(xiàn)程從非RUNNABLE
變成RUNNABLE
(喚醒線(xiàn)程)只能說(shuō)其獲得了可以獲得CPU使用權(quán)的一個(gè)機(jī)會(huì),當(dāng)其獲得了使用權(quán)從內(nèi)存中恢復(fù)自己的進(jìn)度信息窍仰,也是上下文切換汉规。上下文切換分為自發(fā)性和非自發(fā)性。
8驹吮、同一時(shí)間內(nèi)處于RUNNING
狀態(tài)的線(xiàn)程越多针史,則代表并發(fā)程度越高,簡(jiǎn)稱(chēng)高并發(fā)碟狞。某些線(xiàn)程申請(qǐng)?jiān)L問(wèn)一個(gè)正在被其他線(xiàn)程持有的排他性資源被稱(chēng)作爭(zhēng)用啄枕,這些線(xiàn)程越多則代表爭(zhēng)用程度越高,簡(jiǎn)稱(chēng)高爭(zhēng)用族沃。而多線(xiàn)程編程的目標(biāo)在于高并發(fā)且低爭(zhēng)用频祝。
9、計(jì)算機(jī)如何為線(xiàn)程分配排他性資源就是資源的調(diào)度脆淹,在不同的場(chǎng)景下使用不同的調(diào)度策略常空,有利于降低爭(zhēng)用。資源調(diào)度分為公平調(diào)度和非公平調(diào)度盖溺。無(wú)論哪種方式漓糙,線(xiàn)程搶占資源失敗都會(huì)被暫停,保存上下文咐柜,并且進(jìn)入等待隊(duì)列兼蜈。當(dāng)被允許搶占資源時(shí)才會(huì)被從等待隊(duì)列上喚醒,等到獲得CPU使用權(quán)拙友,然后加載上下文并且申請(qǐng)資源使用權(quán)为狸。公平調(diào)度在等待隊(duì)列為空時(shí)才允許線(xiàn)程申請(qǐng)資源,對(duì)于可以申請(qǐng)資源的線(xiàn)程遗契,成功則獲得使用權(quán)辐棒,失敗則被暫停然后進(jìn)入等待隊(duì)列,等待資源被當(dāng)前持有線(xiàn)程釋放時(shí)牍蜂,調(diào)度器按照等待隊(duì)列先進(jìn)先出的策略喚醒等待隊(duì)列上的線(xiàn)程去搶占CPU并且申請(qǐng)資源漾根。非公平調(diào)度則允許當(dāng)前的RUNNING
線(xiàn)程直接申請(qǐng)資源,成功獲得使用權(quán)鲫竞,失敗則被暫停然后進(jìn)入等待隊(duì)列辐怕,等待資源被當(dāng)前持有線(xiàn)程釋放時(shí),調(diào)度器喚醒等待隊(duì)列中一個(gè)線(xiàn)程(是不是第一個(gè)線(xiàn)程有待考證)从绘,等到獲取CPU使用權(quán)加載上下文然后申請(qǐng)資源寄疏,但是在這個(gè)被喚醒的線(xiàn)程尚在加載上下文過(guò)程中,是允許在RUNNING
的線(xiàn)程直接申請(qǐng)資源的僵井,所以可能導(dǎo)致其開(kāi)始運(yùn)行時(shí)由于資源又被搶占了而被暫停重新回到等待隊(duì)列陕截。可以看出批什,公平調(diào)度線(xiàn)程搶到資源的時(shí)間都差不多农曲,而非公平調(diào)度中,可能有些線(xiàn)程反復(fù)被暫停喚醒N次才能搶到資源驻债,甚至造成饑餓乳规。但是在爭(zhēng)用較高的情況下,公平調(diào)度由于每次搶占資源都需要伴隨一次上下文切換合呐,開(kāi)銷(xiāo)較大驯妄,而非公平調(diào)度允許RUNNING
線(xiàn)程直接搶占資源,如果線(xiàn)程持有資源的時(shí)間較少合砂,甚至在等待被喚醒線(xiàn)程載入上下文過(guò)程中資源以及被搶占了好多次青扔,這樣就會(huì)大大減少上下文切換的開(kāi)銷(xiāo)增加吞吐率。但是在線(xiàn)程占用資源時(shí)間過(guò)長(zhǎng)的情況下翩伪,非公平策略沒(méi)有任何好處微猖,還會(huì)帶來(lái)線(xiàn)程饑餓的問(wèn)題。非公平策略是一般情況下的默認(rèn)策略缘屹。
4凛剥、線(xiàn)程同步機(jī)制——共享數(shù)據(jù)訪(fǎng)問(wèn)控制
1、默認(rèn)情況下轻姿,多線(xiàn)程程序由于硬件(存儲(chǔ)子系統(tǒng)犁珠、寫(xiě)緩沖器)和軟件(編譯器)對(duì)程序的優(yōu)化會(huì)產(chǎn)生一些預(yù)想不到的行為即多線(xiàn)程問(wèn)題逻炊。我們?nèi)绻氆@得正確的運(yùn)行結(jié)果就需要在代碼層面通過(guò)一些措施來(lái)協(xié)調(diào)線(xiàn)程間的數(shù)據(jù)訪(fǎng)問(wèn)和活動(dòng),這種措施就是線(xiàn)程同步機(jī)制犁享。Java平臺(tái)提供的線(xiàn)程同步機(jī)制包括:鎖余素,volatile關(guān)鍵字、final關(guān)鍵字炊昆、static關(guān)鍵字以及一些API(wait() / notify())
2桨吊、鎖可以控制線(xiàn)程一段代碼的執(zhí)行權(quán)限(控制哪個(gè)線(xiàn)程可以執(zhí)行),而這段被鎖控制的代碼叫做臨界區(qū)凤巨。當(dāng)一個(gè)線(xiàn)程想執(zhí)行臨界區(qū)時(shí)视乐,必須持有引導(dǎo)該臨界區(qū)的鎖。前面我們已經(jīng)知道敢茁,并發(fā)編程的主要問(wèn)題在于原子性佑淀、可見(jiàn)性、有序性彰檬。Java中的鎖有幾種實(shí)現(xiàn)渣聚,而合理的使用不同的鎖則可以使得這三個(gè)性質(zhì)得以保障。
原子性:如果想保證原子性僧叉,則需要將本來(lái)對(duì)于共享數(shù)據(jù)的并發(fā)訪(fǎng)問(wèn)變成邏輯上的串行訪(fǎng)問(wèn)奕枝。即我們將所有訪(fǎng)問(wèn)共享數(shù)據(jù)的操作都寫(xiě)在臨界區(qū)內(nèi)并且使用互斥鎖(排它鎖)就行∑慷椋互斥鎖只能被一個(gè)線(xiàn)程持有隘道,也就是說(shuō)同一時(shí)間只有一個(gè)線(xiàn)程可以執(zhí)行臨界區(qū)代碼。
可見(jiàn)性:線(xiàn)程在獲得鎖時(shí)會(huì)隱式的刷新處理器緩存郎笆,線(xiàn)程在釋放鎖時(shí)會(huì)隱式的沖刷處理器緩存谭梗,使得臨界區(qū)中的共享變量都是相對(duì)新值。不僅如此宛蚓,鎖還能保證引用類(lèi)型的變量所指向的內(nèi)容都是相對(duì)新值激捏!比如數(shù)組,集合凄吏,其內(nèi)部的對(duì)象和對(duì)象的字段都會(huì)是相對(duì)新值远舅!如果當(dāng)前鎖是互斥鎖,那么原子性和可見(jiàn)性都得到了保障痕钢,從而這些共享數(shù)據(jù)都會(huì)是最新值~而由于JIT重排序?qū)е碌目梢?jiàn)性問(wèn)題則待會(huì)再說(shuō)图柏。
有序性:如果原子性和可見(jiàn)性都得到了保障,那么從實(shí)際結(jié)果來(lái)看任连,相當(dāng)于有序性得到了保障蚤吹,即使發(fā)生了重排序,因?yàn)榕R界區(qū)的代碼執(zhí)行是串行的而且能夠保障執(zhí)行完的結(jié)果其他線(xiàn)程能夠看到,而且由于鎖的功能裁着,臨界區(qū)內(nèi)的指令不會(huì)和臨界區(qū)外的指令進(jìn)行重排序繁涂。
3、理解鎖粒度的概念二驰。
4扔罪、鎖的使用會(huì)增加處理器時(shí)間消耗:鎖的申請(qǐng)和釋放、鎖導(dǎo)致的線(xiàn)程上下文切換诸蚕。并且還會(huì)導(dǎo)致鎖泄露,死鎖等一系列線(xiàn)程活性問(wèn)題氧猬。
5背犯、內(nèi)部鎖(synchronized)是Java自帶的一個(gè)互斥鎖伦糯,被synchronized關(guān)鍵字修飾的方法或者代碼塊就是臨界區(qū)吞歼。需要知道,Java中所有對(duì)象內(nèi)部都有一個(gè)互斥資源(Monitor
)他去,這個(gè)才是鎖的本體妄均。synchronized可以修飾方法和代碼塊柱锹,當(dāng)線(xiàn)程訪(fǎng)問(wèn)一個(gè)synchronized實(shí)例方法時(shí),需要申請(qǐng)的鎖就是這個(gè)實(shí)例的Monitor
丰包;線(xiàn)程訪(fǎng)問(wèn)一個(gè)synchronized靜態(tài)方法時(shí)禁熏,需要申請(qǐng)的鎖就是這個(gè)類(lèi)的Class對(duì)象的Monitor
;線(xiàn)程訪(fǎng)問(wèn)一個(gè)synchronized代碼塊時(shí)邑彪,需要申請(qǐng)我們指定的對(duì)象(鎖句柄)的Monitor
瞧毙;由于鎖句柄是我們指定的,為了保證其不會(huì)被改變寄症,通常使用private final
修飾宙彪。而之所以被稱(chēng)為內(nèi)部鎖是因?yàn)殒i的申請(qǐng)和釋放都是JVM
進(jìn)行的不用我們介入,而且由于JVM
的控制所以不會(huì)出現(xiàn)鎖泄露有巧。
鎖作為一個(gè)互斥資源释漆,其等待隊(duì)列是JVM
給其分配的一個(gè)叫做EntrySet
(入口集)的數(shù)據(jù)結(jié)構(gòu),就是一個(gè)普通隊(duì)列篮迎。內(nèi)部鎖默認(rèn)是非公平的男图,而且只能是非公平的。
6甜橱、顯式鎖:JUC.locks
下的鎖享言,典型的有Lock
(接口),ReentrantLock
渗鬼,ReadWriteLock
览露。由于這些鎖是代碼層面的,鎖的申請(qǐng)和釋放需要由開(kāi)發(fā)人員顯式控制譬胎,所以被叫做顯式鎖差牛。ReentrantLock
是可重入鎖命锄,并且其既支持公平鎖也支持非公平鎖。與內(nèi)部鎖不同的是偏化,顯式鎖的使用更自由脐恩,不局限于一個(gè)方法或者代碼塊,而且顯式鎖提供了tryLock
操作侦讨,當(dāng)執(zhí)行線(xiàn)程使用tryLock
失敗時(shí)驶冒,并不會(huì)被暫停。
7韵卤、Java1.6骗污、1.7
對(duì)內(nèi)部鎖進(jìn)行了優(yōu)化:鎖消除、鎖粗化沈条、偏向鎖和適配性鎖需忿。Java1.5
中再高爭(zhēng)用情況下,內(nèi)部鎖性能急劇下降而顯式鎖相對(duì)穩(wěn)定蜡歹,不過(guò)經(jīng)過(guò)內(nèi)部鎖優(yōu)化之后屋厘,兩種鎖就沒(méi)有太大差距了,所以現(xiàn)在如果僅僅是使用非公平互斥鎖的情況下還是直接使用內(nèi)部鎖月而。但是某些情況下我們可能需要一些具有某些特性的鎖汗洒,比如讀寫(xiě)鎖。讀寫(xiě)鎖分為讀鎖和寫(xiě)鎖父款,這不是說(shuō)ReadWriteLock
是兩個(gè)鎖仲翎,而是其作為一個(gè)鎖有兩種角色。線(xiàn)程讀取共享數(shù)據(jù)時(shí)必須持有其讀鎖铛漓,讀鎖不是互斥的溯香,但是如果共享數(shù)據(jù)的寫(xiě)鎖已經(jīng)被持有,則無(wú)法獲得其讀鎖浓恶。線(xiàn)程更新共享數(shù)據(jù)時(shí)必須持有寫(xiě)鎖玫坛,寫(xiě)鎖是互斥的,如果共享數(shù)據(jù)的讀鎖或者寫(xiě)鎖已經(jīng)被持有包晰,則無(wú)法再獲得寫(xiě)鎖湿镀。讀寫(xiě)鎖的使用可以實(shí)現(xiàn)多線(xiàn)程程序并發(fā)的只讀某些數(shù)據(jù),當(dāng)只讀操作比更新操作頻繁的多伐憾、讀線(xiàn)程持有鎖時(shí)間較長(zhǎng)的情況下建議使用讀寫(xiě)鎖勉痴。
8、前面介紹了可見(jiàn)性和有序性的保障為沖刷處理器緩存和刷新處理器緩存和禁止重排序树肃,而這幾個(gè)動(dòng)作則是依靠內(nèi)存屏障來(lái)實(shí)現(xiàn)的蒸矛,并且內(nèi)存屏障只針對(duì)于對(duì)于主內(nèi)存的訪(fǎng)問(wèn)操作指令(比如從主內(nèi)存讀取數(shù)據(jù)的Load / Read指令、將數(shù)據(jù)從寫(xiě)入主內(nèi)存的Store / Write指令)。內(nèi)存屏障其實(shí)就是一些特殊的指令雏掠,當(dāng)這些指令能夠起到限制重排序邊界和提醒處理器操作緩存的作用斩祭,但是對(duì)于不同的處理器架構(gòu),這些指令是不同的乡话,無(wú)需在意只要知道常見(jiàn)的一些就行摧玫。
按照可見(jiàn)性劃分:加載屏障和存儲(chǔ)屏障。在鎖獲得指令之后添加加載屏障绑青,可刷新處理器緩存诬像。在鎖釋放之后添加存儲(chǔ)屏障,可沖刷處理器緩存闸婴。
按照有序性劃分:獲取屏障坏挠、釋放屏障。在讀操作之后插入一個(gè)獲取屏障掠拳,可以禁止這個(gè)讀操作之后的讀寫(xiě)操作與其重排序癞揉。在寫(xiě)操作之前插入一個(gè)釋放屏障可禁止前面的讀寫(xiě)操作與其發(fā)生重排序纸肉。前面已經(jīng)知道溺欧,臨界區(qū)內(nèi)的指令不會(huì)與臨界區(qū)之外的指令發(fā)生重排序,并且能夠保證原子性柏肪,可見(jiàn)性姐刁,有序性。現(xiàn)在應(yīng)該清楚了烦味∧羰梗互斥資源Montor
作為鎖用來(lái)保證臨界區(qū)的原子性,在獲得鎖指令EnterMonitor
(是一個(gè)讀操作)之后插入獲取屏障谬俄,在釋放鎖指令ExitMonitor
(是一個(gè)寫(xiě)操作)之前插入釋放屏障用來(lái)保證臨界區(qū)代碼與外部的有序性柏靶,在EnterMonitor
之后插入加載屏障,在ExitMonitor
之后插入存儲(chǔ)屏障用來(lái)保證臨界區(qū)的可見(jiàn)性溃论。
假設(shè)臨界區(qū)內(nèi)沒(méi)有其他臨界區(qū):由于重排序是一種提高程序性能的動(dòng)作屎蜓,只是在多線(xiàn)程環(huán)境中會(huì)出現(xiàn)意想不到的結(jié)果,為了不使性能受到太大損傷钥勋,現(xiàn)代編譯器(JIT)的一些優(yōu)化是在生成代碼屏障之前炬转,可根據(jù)某些策略將臨界區(qū)外的代碼先移到臨界區(qū)內(nèi),然后再插入內(nèi)存屏障算灸,一旦編譯結(jié)束扼劈、屏障生成,則這些代碼不允許被重排序到臨界區(qū)外菲驴,而被移到臨界區(qū)內(nèi)的代碼則可以在滿(mǎn)足貌似串行化語(yǔ)義的情況下被重排序荐吵。
9、之前提到過(guò)volatile關(guān)鍵字可以保證64位JDK下double/long變量讀取的原子性以及volatile修飾變量的可見(jiàn)性以及訪(fǎng)問(wèn)這個(gè)變量的有序性,由于其不會(huì)引起上下文切換且功能有限所以也被稱(chēng)為輕量級(jí)鎖捍靠。編譯器不會(huì)將volatile變量分配到寄存器中存儲(chǔ)沐旨,處理器對(duì)于volatile變量的訪(fǎng)問(wèn)操作只會(huì)在高速緩存或者主內(nèi)存上進(jìn)行。處理器讀volatile變量時(shí)的行為類(lèi)似于獲得鎖榨婆,寫(xiě)volatile變量時(shí)的行為類(lèi)似于釋放鎖磁携。即在寫(xiě)volatile變量操作之前會(huì)插入釋放屏障防止和其之前的讀寫(xiě)該變量的操作重排序保證有序性,在其之后插入存儲(chǔ)屏障用來(lái)保證對(duì)volatile的寫(xiě)入及其之前其他變量寫(xiě)入操作對(duì)其他線(xiàn)程的可見(jiàn)性良风。在讀volatile變量操作之前會(huì)插入加載屏障谊迄,在其之后插入獲取屏障來(lái)保證volatile變量的讀操作和其之后的讀寫(xiě)操作不會(huì)發(fā)生重排序,并且由于加載屏障的存在烟央,不僅能保證其讀取的是相對(duì)新值统诺,也能保證其之后的其他變量的讀寫(xiě)操作的數(shù)值是相對(duì)新值。
與synchronized不同的是疑俭,volatile修飾引用類(lèi)型如數(shù)組粮呢,對(duì)象時(shí)只能保證對(duì)這個(gè)引用起作用,二隊(duì)引用對(duì)象的內(nèi)部數(shù)據(jù)不起作用钞艇。
由于volatile每次讀取和寫(xiě)入時(shí)都需刷新/沖刷處理器緩存啄寡,而且不能存儲(chǔ)在寄存器中,所以其訪(fǎng)問(wèn)開(kāi)銷(xiāo)介乎于普通變量的訪(fǎng)問(wèn)和臨界區(qū)變量的訪(fǎng)問(wèn)之間哩照。
volatile比較常見(jiàn)的使用場(chǎng)景是一個(gè)線(xiàn)程設(shè)立標(biāo)志位挺物,其他線(xiàn)程讀取標(biāo)志位時(shí)用來(lái)保證可見(jiàn)性和該變量的原子性有序性,即一寫(xiě)多讀飘弧。還有就是如果一組共享變量的更新?tīng)顟B(tài)要保持一致识藤,比如4個(gè)變量都被A線(xiàn)程修改,結(jié)果前兩個(gè)變量更新被其他線(xiàn)程觀察到次伶,后兩個(gè)沒(méi)有痴昧,預(yù)期應(yīng)該是要不都沒(méi)被觀察到,要不都被觀察到冠王。這個(gè)時(shí)候可以將四個(gè)共享變量塞入一個(gè)volatile的對(duì)象中赶撰,直接更換這個(gè)對(duì)象的地址是原子性的,可以實(shí)現(xiàn)其他線(xiàn)程查看這個(gè)對(duì)象中的四個(gè)共享變量的更新?tīng)顟B(tài)是一定保持一致的版确。
來(lái)看一個(gè)多線(xiàn)程入門(mén)必會(huì)的實(shí)例:雙重檢查鎖定——單例模式
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
雙重檢查是為了不讓線(xiàn)程在獲取單例實(shí)例時(shí)每次都申請(qǐng)鎖扣囊;synchronized修飾代碼塊是為了不讓單例類(lèi)被多次創(chuàng)建;volatile是為了不讓分配內(nèi)存指令绒疗、**對(duì)象初始化指令侵歇、重排序到地址賦值指令(寫(xiě)操作)之后,詳解如下:
instance = new Singleton(); 實(shí)際上對(duì)應(yīng)著三個(gè)不同的指令
1 objRef = allocate(Singleton.class) 給對(duì)象分配內(nèi)存
2 invokeConstructor(objRef); 執(zhí)行objRef引用對(duì)象的構(gòu)造函數(shù)
3 instance = objRef 將構(gòu)造完成的對(duì)象的地址賦值給instance
如果我們不用volatile修飾
instance
吓蘑,則在臨界區(qū)中2可能重排序到3之前惕虑,那樣其他線(xiàn)程可能發(fā)現(xiàn)instance
并不為null
坟冲,但是構(gòu)造函數(shù)還沒(méi)有執(zhí)行完全,這樣在后續(xù)的代碼執(zhí)行時(shí)可能造成空指針異常溃蔫。如果想實(shí)現(xiàn)多線(xiàn)程下的延遲加載單例模式而又不想像雙重檢查鎖定一樣復(fù)雜健提,則可以使用靜態(tài)內(nèi)部成員變量和枚舉的方式實(shí)現(xiàn):
靜態(tài)內(nèi)部成員變量,類(lèi)成員變量在被初次訪(fǎng)問(wèn)時(shí)才會(huì)觸發(fā)該類(lèi)的初始化伟叛,且JVM能保證靜態(tài)變量初始化的多線(xiàn)程特性私痹,
即默認(rèn)多線(xiàn)程環(huán)境下,能且僅能保證靜態(tài)變量的初始化值是可靠的
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
public static Singleton getInstance() {
return INSTANCE;
}
}
枚舉值统刮,枚舉實(shí)例在初次被調(diào)用時(shí)才會(huì)被初始化紊遵,同樣由JVM保證其多線(xiàn)程特性
public enum Singleton{
INSTANCE;
}
Singleton.INSTANCE就是單例類(lèi)的實(shí)例
10、CAS與原子變量:
count++
在多線(xiàn)程環(huán)境下是不安全的侥蒙,同步方法很簡(jiǎn)單暗膜,使用內(nèi)部鎖或者顯式鎖就行。只為了這樣一個(gè)最簡(jiǎn)單的自增操作就使用開(kāi)銷(xiāo)較大的鎖不夠優(yōu)雅鞭衩,CAS則是我們更好的選擇学搜。CAS全稱(chēng)為compare and swap
,這是一個(gè)由處理器提供的無(wú)需加鎖的原子操作论衍,包含了很多指令瑞佩。把CAS抽象成函數(shù):
boolean CAS(Variable V,int oldValue饲齐,int newValue) {
if (V.getValue() == oldValue) {
setValue(newValue);
} else {
return false;
}
}
簡(jiǎn)單來(lái)說(shuō)就是钉凌,我們使用CAS 指令時(shí)需要傳給處理器三個(gè)值咧最,需要更新的變量捂人,更新線(xiàn)程當(dāng)前看到的該變量的值,更新線(xiàn)程需要將該變量更新的值矢沿。而處理器會(huì)直接看這個(gè)地址上的值是否和更新線(xiàn)程看到的值相等滥搭,相等則說(shuō)明在更新線(xiàn)程執(zhí)行計(jì)算到位變量賦值的過(guò)程中沒(méi)有其他線(xiàn)程更改過(guò)變量值,則可以執(zhí)行更新捣鲸,如果不相同則說(shuō)明發(fā)生了競(jìng)態(tài)瑟匆,放棄此次更新操作(但會(huì)發(fā)生
ABA
問(wèn)題)。由于CAS只保證原子性栽惶,但是其內(nèi)部有一個(gè)setValue
操作愁溜,所以需要將這個(gè)value設(shè)置為volatile,保證其可見(jiàn)性
11外厂、static和final:前面講單例的時(shí)候說(shuō)道static類(lèi)型的變量在多個(gè)線(xiàn)程共享時(shí)冕象,能夠保證其在第一次調(diào)用時(shí)被初始化且僅被初始化一次,但是如果static變量是一個(gè)引用類(lèi)型汁蝶,由于重排序等作用渐扮,則不能保證這個(gè)static變量?jī)?nèi)部的成員變量也被初始化完畢论悴,但是能保證其也被初始化。但是final類(lèi)型變量則更厲害墓律。在其修飾變量能夠被其他線(xiàn)程看到膀估,其被第一次讀取的時(shí)候總是能讀取到初始值,并且如果這個(gè)final變量是引用類(lèi)型耻讽,其成員變量如果有初始值也一定會(huì)被初始化完畢(禁止了JIT重排序和JIT的內(nèi)聯(lián)優(yōu)化)察纯。
12、對(duì)象的發(fā)布與逸出:共享變量的引起多線(xiàn)程問(wèn)題的原因针肥,但是如果共享變量是private的就只能通過(guò)其他public等可見(jiàn)的方法或者內(nèi)部類(lèi)的方式來(lái)房屋共享變量捐寥,這種非直接訪(fǎng)問(wèn)的途徑統(tǒng)稱(chēng)為對(duì)象發(fā)布,常見(jiàn)的對(duì)象發(fā)布有將private的共享變量注冊(cè)到public的容器中祖驱,內(nèi)部類(lèi)的this指針握恳,public方法中訪(fǎng)問(wèn)等。由于對(duì)象發(fā)布可能造成多線(xiàn)程問(wèn)題捺僻,如果出現(xiàn)了問(wèn)題就叫做對(duì)象逸出乡洼。
5、線(xiàn)程間協(xié)作
前面了解了并發(fā)環(huán)境下多線(xiàn)程可能產(chǎn)生的問(wèn)題匕坯。但是并發(fā)線(xiàn)程之間通常不是各自為戰(zhàn)的束昵,而是需要相互結(jié)合來(lái)完成一項(xiàng)復(fù)雜的計(jì)算任務(wù),即線(xiàn)程間協(xié)作葛峻。
1锹雏、暫停和喚醒:所有對(duì)象內(nèi)部都含有一個(gè)由JVM控制的monitor
互斥變量,即所有的對(duì)象都能用來(lái)引導(dǎo)臨界區(qū)术奖。Object
類(lèi)有兩個(gè)原子方法wait() notify()
礁遵,即所有對(duì)象都有這兩個(gè)方法。當(dāng)一個(gè)線(xiàn)程進(jìn)入到某個(gè)對(duì)象obj所引導(dǎo)的臨界區(qū)內(nèi)采记,調(diào)用obj.wait()
方法佣耐,則會(huì)導(dǎo)致當(dāng)前線(xiàn)程暫停并且釋放obj.monitor
,然后進(jìn)入obj的等待隊(duì)列(也被叫做WaitSet)唧龄,狀態(tài)變?yōu)?code>WAITING(注意兼砖,不是之前講的EntrySet,下文中將用阻塞隊(duì)列來(lái)替代EntrySet)既棺,等待隊(duì)列中的線(xiàn)程只能被其他處于obj所引導(dǎo)的臨界區(qū)內(nèi)的線(xiàn)程調(diào)用obj.notify()
才可能被喚醒回到RUNNALBE
狀態(tài)等待CPU資源獲得重新進(jìn)入臨界區(qū)繼續(xù)執(zhí)行之前線(xiàn)程上下文的機(jī)會(huì)讽挟。為什么說(shuō)可能
被喚醒?是因?yàn)榈却?duì)列上可能會(huì)有多個(gè)線(xiàn)程丸冕,而notify
一次只能喚醒一個(gè)隨機(jī)線(xiàn)程耽梅,如果想喚醒所有線(xiàn)程則可以調(diào)用obj.notifyAll()
,除此之外Object類(lèi)還有wait(long time)
方法晨仑,即線(xiàn)程在等待隊(duì)列中等待了time
毫秒依然沒(méi)被其他線(xiàn)程喚醒則由JVM進(jìn)行喚醒褐墅。暫停/喚醒是線(xiàn)程間協(xié)作最基本的方法拆檬。
wait/notify的問(wèn)題:notify喚醒隨機(jī)線(xiàn)程,notifyAll喚醒所有線(xiàn)程妥凳,這也就導(dǎo)致了一個(gè)最顯而易見(jiàn)的問(wèn)題竟贯,無(wú)法喚醒確切的線(xiàn)程,如果要想讓我們想喚醒的線(xiàn)程一定能被喚醒逝钥,唯一的辦法就是喚醒所有線(xiàn)程屑那。但是無(wú)緣無(wú)故被喚醒的線(xiàn)程其關(guān)鍵數(shù)據(jù)尚未準(zhǔn)備好,導(dǎo)致其繼續(xù)進(jìn)入WAITING
狀態(tài)艘款,即會(huì)產(chǎn)生過(guò)早喚醒問(wèn)題持际。
還有就是如果暫停線(xiàn)程并不是放在循環(huán)語(yǔ)句中,且通知線(xiàn)程
的執(zhí)行在暫停線(xiàn)程
之前哗咆,這樣就導(dǎo)致暫停線(xiàn)程再也得不到喚醒蜘欲,即信號(hào)丟失問(wèn)題佛致。
Thread.join()
是Java用wait/notify
實(shí)現(xiàn)的
2秃殉、條件變量:wait/notify
太過(guò)于底層,且一堆問(wèn)題稽荧,甚至都不能分清暫停線(xiàn)程是被notify
喚醒還是wait
超時(shí)喚醒年碘。而使用JUC.locks包下的條件變量Condition能夠解決這兩個(gè)問(wèn)題澈歉。Condition通過(guò)顯式鎖構(gòu)造,像
Condition condition = lock.newCondition();
就創(chuàng)造了一個(gè)顯式鎖lock
的條件變量屿衅。類(lèi)似于Object埃难,Condition同樣有暫停和喚醒方法await() / signal() awaitUntil(Date deadline) signalAll()
,并且這幾個(gè)方法的調(diào)用也需要線(xiàn)程持有該顯式鎖涤久;類(lèi)似于WaitSet涡尘,Condition中也維護(hù)了一個(gè)等待隊(duì)列用于存放WAITING
狀態(tài)的線(xiàn)程。但是一個(gè)顯式鎖對(duì)象lock
可以創(chuàng)造多個(gè)Condition實(shí)例拴竹,如果為使用不同條件的暫停/喚醒線(xiàn)程創(chuàng)造不同的Condition實(shí)例悟衩,則可以實(shí)現(xiàn)喚醒指定線(xiàn)程的功能剧罩。Condition.awaitUntil(Date date)
栓拜,如果date
大于當(dāng)前時(shí)間,則返回true
惠昔,表示未超時(shí)幕与,返回false
則表示超時(shí)。所以當(dāng)線(xiàn)程被喚醒時(shí)镇防,調(diào)用該方法發(fā)現(xiàn)返回值為true
則表示是被顯示歡喜而不是超時(shí)喚醒的啦鸣。
3、倒計(jì)時(shí)協(xié)調(diào)器来氧,CountDownLatch:現(xiàn)有線(xiàn)程A诫给、B香拉,B線(xiàn)程中調(diào)用A線(xiàn)程的join
方法,可讓B在A線(xiàn)程執(zhí)行完之后再執(zhí)行中狂,借此可以衍生出兩個(gè)問(wèn)題:①如何讓B不等到A結(jié)束凫碌,而是等到A執(zhí)行某個(gè)操作之后繼續(xù)執(zhí)行 ②如何讓另外幾個(gè)線(xiàn)程C、D......和B一同執(zhí)行
CountDownLatch完美解決這兩個(gè)問(wèn)題胃榕,A執(zhí)行的某個(gè)引發(fā)B或者B和其他線(xiàn)程繼續(xù)執(zhí)行的操作被叫做先決操作盛险,CountDownLatch中維護(hù)了一個(gè)先決操作個(gè)數(shù)的變量,姑且稱(chēng)之為count勋又,當(dāng)一個(gè)線(xiàn)程調(diào)用CountDownLatch.await()
時(shí)苦掘,如果count
不等于0,則會(huì)被暫停楔壤。當(dāng)其他線(xiàn)程調(diào)用一次CountDownLatch.countDown()
時(shí)鹤啡,相當(dāng)于count--
,如果調(diào)用時(shí)count = 0
則調(diào)用無(wú)效蹲嚣,count-- == 0
揉忘,則會(huì)喚醒所有調(diào)用過(guò)await()
方法被暫停的線(xiàn)程。從count
第一次為0開(kāi)始端铛,CountDownLatch就失效了泣矛,此時(shí)無(wú)論是再調(diào)用await
或者是countDown
都無(wú)效,即CountDownLatch
是一次性的禾蚕。
4您朽、CyclicBarrier,柵欄:柵欄也可以暫停線(xiàn)程换淆,暫停在柵欄上的線(xiàn)程被稱(chēng)為參與方哗总。柵欄從表面意思來(lái)看就是起攔截作用,柵欄外堆積到一定的人數(shù)就釋放這一批人然后繼續(xù)攔截下一批人倍试。一次攔截的人數(shù)就是count
讯屈,每有一個(gè)人到柵欄外等待就相當(dāng)于有一個(gè)線(xiàn)程調(diào)用了CyclicBarrier.await()
方法使得count--
然后被暫停進(jìn)入WAITING
狀態(tài),當(dāng)?shù)?code>count個(gè)線(xiàn)程調(diào)用await()
方法時(shí)count-- == 0
县习,同時(shí)喚醒之前的count-1
個(gè)參與方涮母,使得所有參與方從同一狀態(tài)開(kāi)始運(yùn)行。和CountDownLatch不同的是躁愿,Cyclic表示其是可重復(fù)使用的叛本,當(dāng)count == 0
喚醒所有參與方之后,會(huì)將count
重置為初始狀態(tài)彤钟,攔截下一批參與方来候。并且在CyclicBarrier創(chuàng)建時(shí)可傳入一個(gè)Runnable
參數(shù),當(dāng)最后一個(gè)到達(dá)的線(xiàn)程喚醒之前暫停線(xiàn)程之前將調(diào)用這個(gè)Runnable
對(duì)象的run()
(注意不是start()
)方法逸雹。柵欄往往用來(lái)進(jìn)行循環(huán)統(tǒng)一線(xiàn)程執(zhí)行狀態(tài)和構(gòu)造高并發(fā)環(huán)境营搅。
5云挟、阻塞隊(duì)列:能夠?qū)е戮€(xiàn)程被暫停(WATING BLOCKED
)的操作或方法叫做阻塞操作/阻塞方法。Java1.5引入的JUC.BlockingQueue
接口定義了阻塞隊(duì)列的概念转质,并且有幾個(gè)不同特性的默認(rèn)實(shí)現(xiàn)類(lèi):ArrayBlockingQueue LinkedBlockingQueue SynchronousQueue
等植锉。按照容量又可分為有界隊(duì)列(指定容量)和無(wú)界隊(duì)列(Integer.MaxValue),入隊(duì)列叫put
峭拘,出隊(duì)列叫take
俊庇。
ArrayBlockingQueue
從名字就可以看出是一個(gè)通過(guò)數(shù)組實(shí)現(xiàn)的隊(duì)列,因此其是有界隊(duì)列鸡挠,有界隊(duì)列的特點(diǎn)是容量從開(kāi)始就分配好辉饱,put take
不會(huì)對(duì)垃圾收集造成壓力(連續(xù)內(nèi)存空間)。但是其put
和take
使用的是同一個(gè)顯示鎖拣展,所以鎖爭(zhēng)用開(kāi)銷(xiāo)較高彭沼。
LinkeddBlockingQueue
:從名字就可以看出是一個(gè)通過(guò)鏈表實(shí)現(xiàn)的隊(duì)列,但是其既可以構(gòu)造時(shí)指定容量作為有界隊(duì)列备埃,也可以不指定從而是無(wú)界隊(duì)列姓惑。由于鏈表的動(dòng)態(tài)生成節(jié)點(diǎn)特性,put
或者take
會(huì)給垃圾收集帶來(lái)負(fù)擔(dān)(碎片內(nèi)存空間)按脚。其put take
使用的是兩個(gè)顯式鎖于毙,所以鎖爭(zhēng)用較小。內(nèi)部使用了AtomicInteger
防止更新size
時(shí)出現(xiàn)線(xiàn)程同步問(wèn)題辅搬。
synchronousQueue
比較特殊唯沮,只有一個(gè)容量,且put
時(shí)如果沒(méi)有take
線(xiàn)程阻塞在隊(duì)列上堪遂,則put
線(xiàn)程會(huì)阻塞介蛉。take
時(shí)如果沒(méi)有put
線(xiàn)程阻塞在隊(duì)列上,則take
線(xiàn)程會(huì)阻塞溶褪”揖桑可見(jiàn)一定有一個(gè)線(xiàn)程會(huì)阻塞。
LinkedBlockingQueue
僅支持非公平調(diào)度猿妈,ArrayBlockingQueue
和SynchronoussBlockingQueue
既支持公平調(diào)度又支持非公平調(diào)度吹菱。
當(dāng)put
線(xiàn)程和get
線(xiàn)程效率差不多時(shí),推薦使用synchronousBlockingQueue
于游,put get
線(xiàn)程并發(fā)程度較大時(shí)建議使用LinkedBlockingQueue
毁葱,并發(fā)程度較小時(shí)建議使用ArrayBlockingQueue
。
6贰剥、信號(hào)量Semaphore:如果使用隊(duì)列,當(dāng)put
的效率遠(yuǎn)大于take
的效率筷频,那么就會(huì)造成無(wú)界隊(duì)列內(nèi)消息大量積壓蚌成,而信號(hào)量則可以在此時(shí)用來(lái)控制put
或者take
的速度前痘,即流量訪(fǎng)問(wèn)控制。Semaphore
最主要的兩個(gè)方法為acquire
(申請(qǐng)一個(gè)信號(hào)量)和release
(釋放一個(gè)信號(hào)量)担忧;當(dāng)信號(hào)量為0時(shí)申請(qǐng)線(xiàn)程會(huì)被暫停在信號(hào)量的等待隊(duì)列上芹缔,當(dāng)信號(hào)量為0時(shí)釋放線(xiàn)程會(huì)喚醒等待隊(duì)列上一個(gè)隨機(jī)的申請(qǐng)線(xiàn)程,看到這里發(fā)現(xiàn)信號(hào)量與鎖好像沒(méi)什么區(qū)別瓶盛。不過(guò)主要區(qū)別有兩個(gè):信號(hào)量可以初始化為多個(gè)值最欠,當(dāng)信號(hào)量初始值為1時(shí)就變成了類(lèi)似monitor
的互斥資源;另外就是信號(hào)量可以由任何線(xiàn)程使用惩猫,acquire
和release
可以被任意調(diào)用沒(méi)有任何限制條件芝硬,不像鎖的釋放需要先獲得鎖。因此如果初始值設(shè)為1轧房,可以實(shí)現(xiàn)一個(gè)線(xiàn)程釋放另一個(gè)線(xiàn)程持有的鎖的功能拌阴。同樣的,既然有爭(zhēng)用那就分公平和非公平奶镶,信號(hào)量默認(rèn)是非公平的迟赃。
7、PipedInputStream厂镇,管道:管道可以用來(lái)連接兩個(gè)線(xiàn)程A纤壁、B,使得A能夠通過(guò)Java流直接將數(shù)據(jù)寫(xiě)入到B捺信,就像一條管道將兩個(gè)線(xiàn)程連接了一樣摄乒。這個(gè)將在學(xué)習(xí)IO的時(shí)候再仔細(xì)看,先了解是怎么回事就行残黑,在多線(xiàn)程里不經(jīng)常使用馍佑。
8、Exchanger和雙緩沖:緩沖區(qū)可看做生產(chǎn)者與消費(fèi)者之間的數(shù)據(jù)容器梨水。多線(xiàn)程環(huán)境下拭荤,可以用兩個(gè)緩沖區(qū)交換的方式來(lái)實(shí)現(xiàn)數(shù)據(jù)生產(chǎn)與消費(fèi)的并發(fā)。public V Exchanger.exchange(V v)
疫诽,參數(shù)v
是當(dāng)前線(xiàn)程持有的緩沖區(qū)舅世,返回值v
是對(duì)方線(xiàn)程持有的緩沖區(qū)。一般情景是當(dāng)生產(chǎn)緩沖區(qū)滿(mǎn)了的時(shí)候奇徒,生產(chǎn)者調(diào)用exchange
暫停當(dāng)前線(xiàn)程雏亚,等待消費(fèi)者緩沖區(qū)為空時(shí)消費(fèi)者線(xiàn)程調(diào)用exchange
時(shí)交換緩沖區(qū)喚醒生產(chǎn)者,兩個(gè)線(xiàn)程繼續(xù)運(yùn)行摩钙,當(dāng)生產(chǎn)者調(diào)用exchange
的時(shí)候如果已經(jīng)有消費(fèi)者調(diào)用了exchange
處于暫停狀態(tài)罢低,則交換緩沖區(qū)喚醒消費(fèi)者,兩個(gè)線(xiàn)程急繼續(xù)運(yùn)行。即生產(chǎn)者和消費(fèi)者一直都是工作在兩個(gè)緩沖區(qū)上的网持,不存在爭(zhēng)用宜岛。仔細(xì)想一想。這個(gè)count = 2
的柵欄CyclicBarrier
是不是有點(diǎn)像呢功舀,exchange
就類(lèi)似于await
9萍倡、線(xiàn)程中斷:線(xiàn)程的中斷請(qǐng)求并不是一個(gè)結(jié)果,而是一個(gè)請(qǐng)求或者說(shuō)是消息辟汰,至于這個(gè)請(qǐng)求/消息目標(biāo)線(xiàn)程不保證給予響應(yīng)(由開(kāi)發(fā)人員控制)列敲。JVM給每個(gè)線(xiàn)程都維護(hù)了一個(gè)被叫做中斷標(biāo)記的布爾值,用來(lái)表示當(dāng)前線(xiàn)程是否收到了中斷請(qǐng)求帖汞, 可以使用Thread.currentThread().isInterrupted()
方法來(lái)獲取當(dāng)前線(xiàn)程的中斷標(biāo)記戴而,也可以使用Thread.interrupted()
獲取并且重置當(dāng)前線(xiàn)程的中斷標(biāo)記。如果當(dāng)前線(xiàn)程收到了中斷請(qǐng)求涨冀,則中斷標(biāo)記為true
填硕,否則為false
÷贡睿可以調(diào)用線(xiàn)程對(duì)象的interrupt()
方法向其發(fā)送中斷請(qǐng)求扁眯。
Java中有些方法可以響應(yīng)中斷標(biāo)記,拋出線(xiàn)程中斷異常翅帜,比如ReentrantLock.lockInterruptibly()
姻檀。但也有一些毫無(wú)影響比如Inputstream.read()
,ReentrantLock.lock()
涝滴,以及內(nèi)部鎖的申請(qǐng)等绣版。依照慣例,響應(yīng)中斷請(qǐng)求并且拋出InterruptedException
異常的方法歼疮,通常在拋異常之前會(huì)調(diào)用當(dāng)前線(xiàn)程的interrupted()
方法杂抽。如果A線(xiàn)程給B線(xiàn)程發(fā)送中斷請(qǐng)求時(shí)B線(xiàn)程已經(jīng)處于暫停狀態(tài),JVM可能會(huì)將B喚醒讓其繼續(xù)運(yùn)行從而選擇是否響應(yīng)這個(gè)中斷韩脏,即中斷請(qǐng)求在某些情況下還能起到喚醒線(xiàn)程的作用缩麸。
10、線(xiàn)程停止:線(xiàn)程停止時(shí)狀態(tài)會(huì)變?yōu)?code>TERMINATED赡矢。但是Java并沒(méi)有提供可以直接讓線(xiàn)程終止的API杭朱。所以需要靠一些技巧來(lái)實(shí)現(xiàn)。比如可以設(shè)置一個(gè)共享變量來(lái)作為目標(biāo)線(xiàn)程是否需要進(jìn)行停止的標(biāo)志吹散。在標(biāo)志其需要停止時(shí)最好也為其設(shè)置中斷弧械,中斷主要是為了喚醒恰好處于阻塞狀態(tài)的線(xiàn)程,讓其處理線(xiàn)程停止請(qǐng)求空民。
6刃唐、從代碼層面保證線(xiàn)程安全
1、無(wú)狀態(tài)對(duì)象
***
:對(duì)象包含的數(shù)據(jù)就是對(duì)象的狀態(tài),無(wú)狀態(tài)的對(duì)象一定沒(méi)有數(shù)據(jù)唁桩,即實(shí)例變量和靜態(tài)變量闭树。但是沒(méi)有數(shù)據(jù)的對(duì)象不一定是無(wú)狀態(tài)對(duì)象耸棒。
2荒澡、不可變對(duì)象***
:將對(duì)象設(shè)計(jì)成不可變的即,類(lèi)型用final
修飾与殃,類(lèi)中的所有字段都是用final
和private
修飾单山,如果要將數(shù)據(jù)暴露給外界,不能直接返回當(dāng)前引用幅疼,要進(jìn)行防御性復(fù)制米奸。
3、線(xiàn)程持有對(duì)象:如果一個(gè)對(duì)象即其內(nèi)部所有的數(shù)據(jù)都只被一個(gè)線(xiàn)程訪(fǎng)問(wèn)爽篷,那么這個(gè)對(duì)象就是實(shí)際上的線(xiàn)程安全對(duì)象悴晰,雖然不是定義上的安全對(duì)象。這種對(duì)象也叫作線(xiàn)程特有對(duì)象逐工,這個(gè)線(xiàn)程叫做持有線(xiàn)程铡溪。
ThreadLocal<T>
(線(xiàn)程局部變量)是Java提供的用來(lái)保存線(xiàn)程特有對(duì)象的容器。T
就是特有對(duì)象的類(lèi)型泪喊,每一個(gè)ThreadLocal<T>
都保存了多個(gè)線(xiàn)程各自的同一種線(xiàn)程特有對(duì)象棕硫。由于特有對(duì)象默認(rèn)是靠線(xiàn)程對(duì)象來(lái)和當(dāng)前線(xiàn)程關(guān)聯(lián),也就是說(shuō)每一個(gè)ThreadLocal<T>
實(shí)例中袒啼,只能有一個(gè)當(dāng)前線(xiàn)程的特有對(duì)象哈扮。使用ThreadLocal.set(T)
可以設(shè)置特有對(duì)象,使用ThreadLocal.get(T)
可以取出特有對(duì)象蚓再。需要注意的是get(T)
方法中會(huì)調(diào)用一個(gè)initvalue()
的方法滑肉,但默認(rèn)返回的是null,可以構(gòu)造器子類(lèi)重寫(xiě)這個(gè)方法來(lái)實(shí)現(xiàn)某些初始化動(dòng)作摘仅。ThreadLocal<T>
一般是作為某個(gè)類(lèi)的靜態(tài)變量使用靶庙。但是需要注意的是,ThreadLocal<T>
并不是沒(méi)有缺點(diǎn)的:①在某些場(chǎng)景下會(huì)產(chǎn)生退化和數(shù)據(jù)錯(cuò)亂的問(wèn)題实檀。②可能導(dǎo)致內(nèi)存泄露和偽內(nèi)存泄露惶洲,比如一個(gè)線(xiàn)程設(shè)立了特有對(duì)象后一直處于暫停狀態(tài),期間引用的ThreadLocal
對(duì)象被垃圾收集清理了膳犹,但特有對(duì)象既無(wú)法被垃圾收集恬吕,也無(wú)法新加入特有對(duì)象時(shí)觸發(fā)無(wú)效條目清理。
4须床、裝飾器模式:JUC
下面的Collection.synchronizedXXX(Collection)
能夠使用裝飾器模式給非線(xiàn)程安全對(duì)象使用鎖重寫(xiě)其方法铐料,實(shí)現(xiàn)線(xiàn)程安全,這些方法返回的對(duì)象叫做同步集合。使用裝飾器模式可以實(shí)現(xiàn)更新操作的線(xiàn)程安全钠惩,但是不能保證遍歷操作的線(xiàn)程安全柒凉。
5、并發(fā)集合:JUC
也提供了一些本來(lái)就可以保障線(xiàn)程安全的并發(fā)集合篓跛。比如:
普通集合 | 并發(fā)集合 | 共同接口 | 遍歷方式 |
---|---|---|---|
ArrayList | CopyOnWriteArrayList | List | 快照 |
HashSet | CopyOnWriteArratSet | Set | 快照 |
LinkedList | ConcurrentLinkedQueue | Queue | 準(zhǔn)實(shí)時(shí) |
HashMap | ConcurrentHashMap | Map | 準(zhǔn)實(shí)時(shí) |
TreeMap | ConcurrentSkipListMap | SortedMap | 準(zhǔn)實(shí)時(shí) |
TreeSet | ConcurrentSkipListSet | SortedSet | 準(zhǔn)實(shí)時(shí) |
并發(fā)集合支持被不同線(xiàn)程同時(shí)更新和遍歷膝捞。并發(fā)集合的遍歷有兩種方式:快照和準(zhǔn)實(shí)時(shí)±⒐担快照是在
Iterator
被建立的那一刻的只讀副本(不支持remove())蔬咬,是集合一瞬間的狀態(tài)。不同線(xiàn)程遍歷時(shí)持有各自的快照沐寺,相當(dāng)于持有特有對(duì)象林艘。優(yōu)點(diǎn)就是不會(huì)受到更新操作的影響,但是如果集合太大很消耗內(nèi)存和復(fù)制快照開(kāi)銷(xiāo)很大混坞。準(zhǔn)實(shí)時(shí)表示既不是使用快照(支持remove())狐援,也不使用鎖(CAS),更新和遍歷并發(fā)進(jìn)行究孕,遍歷時(shí)其他線(xiàn)程的更新操作可能被感知也可能不被感知啥酱。這些并發(fā)集合的特性
7、線(xiàn)程活性故障:
1蚊俺、死鎖:兩個(gè)線(xiàn)程A和B懈涛,A持有互斥資源S1,申請(qǐng)S2泳猬;B持有互斥資源S2批钠,申請(qǐng)S1。結(jié)果導(dǎo)致兩個(gè)線(xiàn)程申請(qǐng)資源時(shí)由于資源互斥也已經(jīng)被持有都進(jìn)入暫停狀態(tài)得封,導(dǎo)致兩個(gè)線(xiàn)程永遠(yuǎn)不會(huì)被喚醒埋心。死鎖的產(chǎn)生必定有以下特征:
①資源互斥:資源同一時(shí)間只能被一個(gè)線(xiàn)程持有
②資源不可搶奪:資源只可能被持有線(xiàn)程主動(dòng)釋放
③占用并等待資源:線(xiàn)程以及占用部分資源,并在等待另一部分資源的過(guò)程中不會(huì)釋放已持有資源
④循環(huán)等待資源:即A等B忙上,B等A
產(chǎn)生死鎖必定有以上條件拷呆,但同時(shí)具有以上條件不一定產(chǎn)生死鎖(萬(wàn)一正常運(yùn)行了呢)
解決死鎖:由于鎖在Java中的性質(zhì)是不可改變的,互斥且可重入疫粥、必須由持有線(xiàn)程主動(dòng)釋放茬斧。所以只能從③④來(lái)解決。
解決方式 | 原因 | 缺點(diǎn) | |
---|---|---|---|
鎖粗化 | 擴(kuò)大鎖的粒度梗逮,比如將鎖從申請(qǐng)筷子擴(kuò)大至吃飯 | 一個(gè)人吃飯時(shí)其他人連申請(qǐng)筷子也不能 | |
鎖排序 | 給互斥資源編號(hào)项秉,每個(gè)人都從編號(hào)較小的互斥資源開(kāi)始申請(qǐng) | 只需要保證排序正確,沒(méi)什么太大缺點(diǎn) | |
控制資源等待時(shí)間 | 使用ReentrantLock.tryLock(long,TimeUtil)指定一個(gè)申請(qǐng)鎖時(shí)的超時(shí)時(shí)間 | 沒(méi)有明顯缺陷 | 23 |