目前魁亦,多線程編程可以說(shuō)是在大部分平臺(tái)和應(yīng)用上都需要實(shí)現(xiàn)的一個(gè)基本需求虚吟。本系列文章就來(lái)對(duì) Java 平臺(tái)下的多線程編程知識(shí)進(jìn)行講解牛郑,從概念入門怠肋、底層實(shí)現(xiàn)到上層應(yīng)用都會(huì)涉及到,預(yù)計(jì)一共會(huì)有五篇文章淹朋,希望對(duì)你有所幫助 ????
本篇文章是第二篇笙各,介紹實(shí)現(xiàn)多線程同步的各類方案,涉及多種多線程同步機(jī)制础芍,是開發(fā)者在語(yǔ)言層面上對(duì)多線程運(yùn)行所做的規(guī)則設(shè)定
一杈抢、線程同步機(jī)制
前面的文章有介紹到,多線程安全問(wèn)題概括來(lái)說(shuō)表現(xiàn)為三個(gè)方面:原子性仑性、可見性惶楼、有序性。多線程安全問(wèn)題的產(chǎn)生前提是存在多個(gè)線程并發(fā)訪問(wèn)(不全是讀)同一份共享數(shù)據(jù)诊杆,而會(huì)產(chǎn)生多線程安全問(wèn)題的根本原因是多個(gè)線程間缺少一套用于協(xié)調(diào)各個(gè)線程間的數(shù)據(jù)訪問(wèn)和行為交互的機(jī)制歼捐,即缺少線程同步機(jī)制
多線程為程序引入了異步行為,相應(yīng)的就必須提供一種線程同步機(jī)制來(lái)保障在需要時(shí)能夠強(qiáng)制多線程同步的方法晨汹。當(dāng)多個(gè)線程間存在共享資源時(shí)豹储,需要以某種方式來(lái)確保每次只有一個(gè)線程能夠使用資源。例如淘这,如果希望兩個(gè)線程進(jìn)行通信并共享某個(gè)復(fù)雜的數(shù)據(jù)結(jié)構(gòu)(例如鏈表)剥扣,就需要以某種方式來(lái)確保它們相互之間不會(huì)發(fā)生沖突。也就是說(shuō)铝穷,當(dāng)一個(gè)線程正在讀取該數(shù)據(jù)結(jié)構(gòu)時(shí)钠怯,必須阻止另外一個(gè)線程向該數(shù)據(jù)結(jié)構(gòu)寫入數(shù)據(jù)
Java 為同步提供了語(yǔ)言級(jí)的支持,同步的關(guān)鍵是監(jiān)視器曙聂,監(jiān)視器是用作互斥鎖的對(duì)象呻疹。在給定時(shí)刻,只有一個(gè)線程可以擁有監(jiān)視器筹陵。當(dāng)線程取得鎖時(shí),也就是進(jìn)入了監(jiān)視器镊尺。其它所有企圖進(jìn)入加鎖監(jiān)視器的線程都會(huì)被掛起朦佩,直到持有監(jiān)視器的線程退出監(jiān)視器
從廣義上來(lái)說(shuō),Java 平臺(tái)提供的線程同步機(jī)制包括:鎖庐氮、volatile语稠、final、static 以及一些 API(Object.wait()、Object.notify() 等)
二仙畦、鎖的分類
既然線程安全問(wèn)題的產(chǎn)生前提是存在多個(gè)線程并發(fā)訪問(wèn)(不全是讀)共享數(shù)據(jù)输涕,那么為了保障線程安全,我們就可以通過(guò)將多個(gè)線程對(duì)共享數(shù)據(jù)的并發(fā)訪問(wèn)轉(zhuǎn)換為串行訪問(wèn)慨畸,從而來(lái)避免線程安全問(wèn)題莱坎。將多個(gè)線程對(duì)共享數(shù)據(jù)的訪問(wèn)限制為串行訪問(wèn),即限制共享數(shù)據(jù)一次只能被一個(gè)線程訪問(wèn)寸士,該線程訪問(wèn)結(jié)束后其它線程才能對(duì)其進(jìn)行訪問(wèn)
Java 就是通過(guò)這種思路提供了鎖(Lock) 這種線程同步機(jī)制來(lái)保障線程安全檐什。鎖具有排他性,一次只能被一個(gè)線程持有(這里所說(shuō)的鎖不包含讀寫鎖這類共享鎖)弱卡,這種鎖就被稱為排他鎖或者互斥鎖乃正。鎖的持有線程可以對(duì)鎖保護(hù)的共享數(shù)據(jù)進(jìn)行訪問(wèn),訪問(wèn)結(jié)束后持有線程就必須釋放鎖婶博,以便其它線程能夠后續(xù)對(duì)共享數(shù)據(jù)進(jìn)行訪問(wèn)瓮具。鎖的持有線程在其獲得鎖之后和釋放鎖之前這段時(shí)間內(nèi)所執(zhí)行的代碼被稱為臨界區(qū)。因此凡人,臨界區(qū)一次只能被一個(gè)線程執(zhí)行名党,共享數(shù)據(jù)只允許在臨界區(qū)內(nèi)進(jìn)行訪問(wèn)
按照 Java 虛擬機(jī)對(duì)鎖的實(shí)現(xiàn)方式的劃分,Java 平臺(tái)中的鎖包括內(nèi)部鎖和顯式鎖划栓。內(nèi)部鎖是通過(guò) synchronize
關(guān)鍵字實(shí)現(xiàn)的兑巾。顯式鎖是通過(guò) java.util.concurrent.locks.Lock
接口的實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)的。內(nèi)部鎖僅支持非公平調(diào)度策略忠荞,顯式鎖既支持公平調(diào)度策略也支持非公平調(diào)度策略
鎖能夠保護(hù)共享數(shù)據(jù)以實(shí)現(xiàn)線程安全蒋歌,起的作用包括保障原子性、保障可見性和保障有序性
- 鎖通過(guò)互斥來(lái)保障原子性委煤。鎖保證了臨界區(qū)代碼一次只能被一個(gè)線程執(zhí)行堂油,臨界區(qū)代碼被執(zhí)行期間其它線程無(wú)法訪問(wèn)相應(yīng)的共享數(shù)據(jù),從而排除了多個(gè)線程同時(shí)訪問(wèn)共享變量從而導(dǎo)致競(jìng)態(tài)的可能性碧绞,這使得臨界區(qū)所執(zhí)行的操作具備了原子性巫员。雖然實(shí)現(xiàn)并發(fā)是多線程編程的目標(biāo)桐腌,但是這種并發(fā)往往是帶有局部串行
- 可見性的保障是通過(guò)寫線程沖刷處理器緩存和讀線程刷新處理器緩存這兩個(gè)動(dòng)作實(shí)現(xiàn)的。在 Java 平臺(tái)。鎖的獲得隱含著刷新處理器緩存這個(gè)動(dòng)作膳灶,這使得讀線程在獲得鎖之后且執(zhí)行臨界區(qū)代碼之前,可以將寫線程對(duì)共享變量所做的更新同步到該線程執(zhí)行處理器的高速緩存中悯搔;而鎖的釋放隱含著沖刷處理器緩存這個(gè)動(dòng)作灾常,這使得寫線程對(duì)共享變量所做的更新能夠被推送到該線程執(zhí)行處理器的高速緩存中,從而對(duì)讀線程可見发魄。因此盹牧,鎖能夠保障可見性
- 鎖能夠保障有序性俩垃。由于鎖對(duì)原子性和可見性的保障,使得鎖的持有線程對(duì)臨界區(qū)內(nèi)對(duì)各個(gè)共享數(shù)據(jù)的更新同時(shí)對(duì)外部線程可見汰寓,相當(dāng)于臨界區(qū)中執(zhí)行的一系列操作在外部線程看來(lái)就是完全按照源代碼順序執(zhí)行的口柳,即外部線程對(duì)這些操作的感知順序與源代碼順序一致,所以說(shuō)鎖保障了臨界區(qū)的有序性有滑。盡管鎖能夠保障有序性跃闹,但臨界區(qū)內(nèi)依然可能存在重排序,但臨界區(qū)代碼不會(huì)被重排序到臨界區(qū)之外俺孙,而臨界區(qū)之外的代碼有可能被重排序到臨界區(qū)之內(nèi)
鎖的原子性及對(duì)可見性的保障合在一起辣卒,可保障臨界區(qū)內(nèi)的代碼能夠讀取到共享數(shù)據(jù)的相對(duì)新值。再由于鎖的互斥性睛榄,同一個(gè)鎖所保護(hù)的共享數(shù)據(jù)一次只能被一個(gè)線程訪問(wèn)荣茫,因此線程在臨界區(qū)中所讀取到的共享數(shù)據(jù)的相對(duì)新值同時(shí)也是最新值
需要注意的是,鎖對(duì)可見性场靴、原子性和有序性的保障是有條件的啡莉,需要同時(shí)滿足以下兩個(gè)條件,否則就還是會(huì)存在線程安全問(wèn)題
- 多個(gè)線程在訪問(wèn)同一組共享數(shù)據(jù)的時(shí)候必須使用同一個(gè)鎖
- 即使是對(duì)共享數(shù)據(jù)進(jìn)行只讀操作旨剥,其執(zhí)行線程也必須持有相應(yīng)的鎖
之所以需要保障以上兩個(gè)要求咧欣,是由于一旦某個(gè)線程進(jìn)入了一個(gè)鎖句柄引導(dǎo)的同步方法/同步代碼塊,其它線程就都無(wú)法再進(jìn)入同個(gè)鎖句柄引導(dǎo)的任何同步方法/同步代碼塊轨帜,但是仍然可以繼續(xù)調(diào)用其它非同步方法/非同步代碼塊魄咕,而如果非同步方法/非同步代碼塊也對(duì)共享數(shù)據(jù)進(jìn)行了訪問(wèn),那么此時(shí)依然會(huì)存在競(jìng)態(tài)
1蚌父、內(nèi)部鎖
Java 平臺(tái)中的任何一個(gè)對(duì)象都有一個(gè)唯一與之關(guān)聯(lián)的鎖哮兰,被稱為監(jiān)視器或者內(nèi)部鎖。內(nèi)部鎖是通過(guò)關(guān)鍵字 synchronize
實(shí)現(xiàn)的苟弛,可用來(lái)修飾實(shí)例方法喝滞、靜態(tài)方法、代碼塊等
synchronize
關(guān)鍵字修飾的方法就被稱為同步方法膏秫,同步方法的整個(gè)方法體就是一個(gè)臨界區(qū)右遭。用 synchronize
修飾的實(shí)例方法和靜態(tài)方法就分別稱為同步實(shí)例方法和同步靜態(tài)方法
public class Test {
//同步靜態(tài)方法
public synchronized static void funName1() {
}
//同步方法
public synchronized void funName2() {
}
}
synchronize
關(guān)鍵字修飾的代碼塊就被稱為同步塊。當(dāng)中缤削,lock
被稱為鎖句柄窘哈。鎖句柄是對(duì)一個(gè)對(duì)象的引用,鎖句柄對(duì)應(yīng)的監(jiān)視器就稱為相應(yīng)同步塊的引導(dǎo)鎖
public class Test {
private final Object lock = new Object();
public void funName1() {
//同步塊
synchronized (lock) {
}
}
}
鎖句柄如果為當(dāng)前對(duì)象(this)亭敢,那就相當(dāng)于同步實(shí)例方法滚婉,如下兩個(gè)同步方法是等價(jià)的
public class Test {
public void funName1() {
synchronized (this) {
}
}
public synchronized void funName2() {
}
}
同步靜態(tài)方法則相當(dāng)于以當(dāng)前類對(duì)象為引導(dǎo)鎖的同步塊,如下兩個(gè)同步方法是等價(jià)的
public class Test {
public synchronized static void funName1() {
}
public void funName2() {
synchronized (Test.class) {
}
}
}
作為鎖句柄的變量通常使用 private final
修飾吨拗,這是因?yàn)殒i句柄的變量一旦被改變,會(huì)導(dǎo)致執(zhí)行同一個(gè)同步塊的多個(gè)線程實(shí)際上使用不同的鎖,從而導(dǎo)致競(jìng)態(tài)
對(duì)于內(nèi)部鎖來(lái)說(shuō)劝篷,線程在執(zhí)行臨界區(qū)內(nèi)代碼之前必須獲得該臨界區(qū)的引導(dǎo)鎖哨鸭,執(zhí)行完后就會(huì)自動(dòng)釋放引導(dǎo)鎖,引導(dǎo)鎖的申請(qǐng)和釋放是由 Java 虛擬機(jī)代為執(zhí)行的娇妓,這也是 synchronized
被稱為內(nèi)部鎖的原因像鸡。且由于 Java 編譯器對(duì)同步塊代碼的特殊處理,即使臨界區(qū)拋出異常哈恰,內(nèi)部鎖也會(huì)被自動(dòng)釋放只估,所以內(nèi)部鎖不會(huì)導(dǎo)致鎖泄漏
2、顯式鎖
顯式鎖從 JDK 1.5 開始被引入 着绷,其作用與內(nèi)部鎖相同蛔钙,但相比內(nèi)部鎖其功能會(huì)豐富很多。顯式鎖由 java.concurrent.locks.Lock
接口來(lái)定義荠医,默認(rèn)實(shí)現(xiàn)類是 java.util.concurrent.locks.ReentrantLock
Lock 的使用方式較為靈活吁脱,可以在方法 A 內(nèi)申請(qǐng)鎖,在方法 B 再進(jìn)行釋放彬向。其基本使用方式如下所示
private Lock lock = new ReentrantLock(false);
private void funName() {
//申請(qǐng)鎖
lock.lock();
try {
//action
} finally {
//釋放鎖
lock.unlock();
}
}
ReentrantLock 既支持公平調(diào)度策略也支持非公平調(diào)度策略兼贡,通過(guò)其一個(gè)參數(shù)的構(gòu)造函數(shù)來(lái)指定,傳值為 true 表示公平鎖娃胆,false 表示非公平鎖遍希, 默認(rèn)使用非公平調(diào)度策略。此外里烦,由于虛擬機(jī)并不會(huì)自動(dòng)為我們釋放鎖凿蒜,所以為了避免鎖泄漏,一般會(huì)將 Lock.unlock()
方法放在 finally
中執(zhí)行招驴,以保證臨界區(qū)內(nèi)的代碼不管是正常結(jié)束還是異常退出篙程,相應(yīng)的鎖釋放操作都會(huì)被執(zhí)行
3、內(nèi)部鎖和顯式鎖的比較
內(nèi)部鎖是基于代碼塊的鎖
其缺點(diǎn)主要有以下幾點(diǎn):
- 使用上缺少靈活性别厘。鎖的申請(qǐng)和釋放操作被限制在一個(gè)代碼塊或者方法體內(nèi)部
- 功能有限虱饿。例如,當(dāng)一個(gè)線程申請(qǐng)某個(gè)正被其它線程持有的內(nèi)部鎖時(shí)触趴,該線程只能被暫停氮发,等待鎖被釋放后再次申請(qǐng),而無(wú)法取消申請(qǐng)或者是限時(shí)申請(qǐng)冗懦,且不支持線程中斷
- 僅支持非公平調(diào)度策略
其優(yōu)點(diǎn)主要有以下幾點(diǎn):
- 使用簡(jiǎn)單
- 由于 Java 編譯器的保障爽冕,所以使用時(shí)不會(huì)造成鎖泄露,保障了安全性
顯式鎖是基于對(duì)象的鎖
其缺點(diǎn)主要有以下幾點(diǎn):
- 需要開發(fā)者自己來(lái)保障不會(huì)發(fā)生鎖泄露
其優(yōu)點(diǎn)主要有以下幾點(diǎn):
- 相對(duì)內(nèi)部鎖在使用上更具靈活性披蕉,可以跨方法來(lái)完成鎖的申請(qǐng)和釋放操作
- 功能相對(duì)內(nèi)部鎖要豐富許多颈畸。例如乌奇,可以通過(guò)
Lock.isLocked()
判斷當(dāng)前線程是否已經(jīng)持有該鎖、通過(guò)Lock.tryLock()
嘗試申請(qǐng)鎖以避免由于鎖被其它線程持有而導(dǎo)致當(dāng)前線程被暫停眯娱、通過(guò)Lock.tryLock(long,TimeUnit)
在指定時(shí)間范圍內(nèi)嘗試申請(qǐng)鎖礁苗、Lock.lockInterruptibly()
支持線程中斷 - 同時(shí)支持公平調(diào)度策略和非公平調(diào)度策略
4、讀寫鎖
鎖的排他性使得多個(gè)線程無(wú)法以線程安全的方式在同一時(shí)刻對(duì)共享數(shù)據(jù)進(jìn)行只讀取而不更新的操作徙缴,這在共享數(shù)據(jù)讀取頻繁但更新頻率較低的情況下降低了系統(tǒng)的并發(fā)性试伙,讀寫鎖就是為了應(yīng)對(duì)這種問(wèn)題而誕生的。讀寫鎖(Read/Wirte Lock)是一種改進(jìn)型的排他鎖于样,也被稱為共享/排他鎖疏叨。讀寫鎖允許多個(gè)線程同時(shí)讀取共享變量,但是一次只允許一個(gè)線程對(duì)共享變量進(jìn)行更新穿剖。任何線程讀取共享變量的時(shí)候蚤蔓,其它線程無(wú)法更新這些變量;一個(gè)線程更新共享變量的時(shí)候携御,其它線程都無(wú)法讀取和更新這些變量
Java 平臺(tái)的讀寫鎖由 java.util.concurrent.locks.ReadWriteLock
接口來(lái)定義昌粤,其默認(rèn)實(shí)現(xiàn)類是 java.util.concurrent.locks.ReentrantReadWriteLock
ReadWriteLock
接口定義了兩個(gè)方法,分別用來(lái)獲取讀鎖(ReadLock)和寫鎖(WriteLock)
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
讀線程在訪問(wèn)共享變量的時(shí)候必須持有讀鎖啄刹,讀鎖是可以共享的涮坐,它可以同時(shí)被多個(gè)線程持有,提高了只讀操作的并發(fā)性誓军。寫線程在訪問(wèn)共享變量的時(shí)候必須持有寫鎖袱讹,寫鎖是排他的,即一個(gè)線程持有寫鎖的時(shí)候其它線程無(wú)法獲得同個(gè)讀寫鎖的讀鎖和寫鎖
讀寫鎖的使用方式與顯式鎖相似昵时,也需要由開發(fā)者自己來(lái)保障避免鎖泄露
public class Test {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
public void reader() {
readLock.lock();
try {
//在此區(qū)域讀取共享變量
} finally {
readLock.unlock();
}
}
public void writer() {
writeLock.lock();
try {
//在此區(qū)域更新共享變量
} finally {
writeLock.unlock();
}
}
}
讀寫鎖在原子性捷雕、可見性和有序性保障方面,它所起的作用和普通的排他鎖是一樣的壹甥,但由于讀寫鎖內(nèi)部實(shí)現(xiàn)比內(nèi)部鎖和其它顯式鎖要復(fù)雜很多救巷,因此讀寫鎖適合于在以下條件同時(shí)得以滿足的場(chǎng)景下使用:
- 只讀操作比寫操作要頻繁得多
- 讀線程持有鎖的時(shí)間比較長(zhǎng)
只有同時(shí)滿足以上兩個(gè)條件的時(shí)候讀寫鎖才是比較適合的,否則可能反而會(huì)比普通排他鎖增大性能開銷
此外句柠,ReentrantReadWriteLock 支持鎖的降級(jí)浦译,即一個(gè)線程持有寫鎖的同時(shí)可以繼續(xù)獲得相應(yīng)的讀鎖。但 ReentrantReadWriteLock 不支持鎖的升級(jí)溯职,即無(wú)法在持有讀鎖的同時(shí)獲得相應(yīng)的寫鎖
5精盅、內(nèi)部鎖和讀寫鎖的性能比較
這里,我們以一個(gè)簡(jiǎn)單的例子來(lái)比較下內(nèi)部鎖和讀寫鎖之間的性能差異谜酒。假設(shè)存在數(shù)量相等的讀線程和寫線程叹俏,讀線程負(fù)責(zé)打印出共享變量整數(shù)值 index 的當(dāng)前值大小,寫線程負(fù)責(zé)對(duì)共享變量整數(shù)值 index 進(jìn)行遞增加一僻族。讀線程和寫線程各自有多個(gè)粘驰,每個(gè)線程間的行為是互相獨(dú)立的屡谐。這里分別通過(guò)使用“內(nèi)部鎖”和“讀寫鎖”來(lái)規(guī)范每個(gè)線程的行為必須是串行的,通過(guò)比較不同方式下所需的時(shí)間耗時(shí)來(lái)對(duì)比兩種鎖之間的性能高低
首先蝌数,Printer
接口定義了讀線程和寫線程需要做的行為操作康嘉,ReadWriteLockPrinter
類是讀寫鎖方式的實(shí)現(xiàn),SynchronizedPrinter
類是內(nèi)部鎖方式的實(shí)現(xiàn)
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
interface Printer {
fun read()
fun write()
fun sleep() {
Thread.sleep(200)
}
}
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
class ReadWriteLockPrinter : Printer {
private val readWriteLock = ReentrantReadWriteLock(true)
private val readLock = readWriteLock.readLock()
private val writeLock = readWriteLock.writeLock()
private var index = 0
override fun read() {
readLock.lock()
try {
sleep()
} finally {
println("讀取到數(shù)據(jù): $index" + "籽前,time: " + System.currentTimeMillis())
readLock.unlock()
}
}
override fun write() {
writeLock.lock()
try {
sleep()
index++
} finally {
println("寫入數(shù)據(jù): $index" + ",time: " + System.currentTimeMillis())
writeLock.unlock()
}
}
}
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
class SynchronizedPrinter : Printer {
private var index = 0
@Synchronized
override fun read() {
sleep()
println("讀取到數(shù)據(jù): $index" + "敷钾,time: " + System.currentTimeMillis())
}
@Synchronized
override fun write() {
sleep()
index++
println("寫入數(shù)據(jù): $index" + "枝哄,time: " + System.currentTimeMillis())
}
}
再來(lái)定義讀線程和寫線程,兩種線程使用的是同個(gè) Printer 對(duì)象
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
class PrinterReadThread(private val printer: Printer) : Thread() {
override fun run() {
printer.read()
}
}
class PrinterWriteThread(private val printer: Printer) : Thread() {
override fun run() {
printer.write()
}
}
通過(guò)切換不同的 Printer 實(shí)現(xiàn)即可大致對(duì)比不同的鎖的性能高低
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
fun main() {
val printer: Printer = SynchronizedPrinter()
//val printer: Printer = ReadWriteLockPrinter()
val threadNum = 10
val writeThreadList = mutableListOf<Thread>()
for (i in 1..threadNum) {
writeThreadList.add(PrinterWriteThread(printer))
}
val readThreadList = mutableListOf<Thread>()
for (i in 1..threadNum) {
readThreadList.add(PrinterReadThread(printer))
}
//啟動(dòng)所有讀線程和所有寫線程
writeThreadList.forEach {
it.start()
}
readThreadList.forEach {
it.start()
}
}
最后的日志輸出類似如下所示阻荒。雖然即使多次運(yùn)行來(lái)取平均值也不具備嚴(yán)格的對(duì)比意義挠锥,但是也可以大致對(duì)比出不同鎖之間的性能高低。從日志也可以看出侨赡,當(dāng)使用讀寫鎖時(shí)多個(gè)讀線程讀取數(shù)據(jù)所需要的總耗時(shí)幾乎是零
# 內(nèi)部鎖 消耗 3801 毫秒
寫入數(shù)據(jù): 1蓖租,time: 1597151018862
讀取到數(shù)據(jù): 1,time: 1597151019062
讀取到數(shù)據(jù): 1羊壹,time: 1597151019262
讀取到數(shù)據(jù): 1蓖宦,time: 1597151019462
讀取到數(shù)據(jù): 1,time: 1597151019662
讀取到數(shù)據(jù): 1油猫,time: 1597151019862
讀取到數(shù)據(jù): 1稠茂,time: 1597151020062
讀取到數(shù)據(jù): 1,time: 1597151020262
讀取到數(shù)據(jù): 1情妖,time: 1597151020462
讀取到數(shù)據(jù): 1睬关,time: 1597151020662
讀取到數(shù)據(jù): 1,time: 1597151020862
寫入數(shù)據(jù): 2毡证,time: 1597151021062
寫入數(shù)據(jù): 3电爹,time: 1597151021262
寫入數(shù)據(jù): 4,time: 1597151021462
寫入數(shù)據(jù): 5料睛,time: 1597151021663
寫入數(shù)據(jù): 6丐箩,time: 1597151021863
寫入數(shù)據(jù): 7,time: 1597151022063
寫入數(shù)據(jù): 8秦效,time: 1597151022263
寫入數(shù)據(jù): 9雏蛮,time: 1597151022463
寫入數(shù)據(jù): 10,time: 1597151022663
# 讀寫鎖 消耗 2000 毫秒
寫入數(shù)據(jù): 1阱州,time: 1597151078704
寫入數(shù)據(jù): 2挑秉,time: 1597151078904
寫入數(shù)據(jù): 3,time: 1597151079104
寫入數(shù)據(jù): 4苔货,time: 1597151079304
寫入數(shù)據(jù): 5犀概,time: 1597151079504
寫入數(shù)據(jù): 6立哑,time: 1597151079704
寫入數(shù)據(jù): 7,time: 1597151079904
寫入數(shù)據(jù): 8姻灶,time: 1597151080104
寫入數(shù)據(jù): 9铛绰,time: 1597151080304
寫入數(shù)據(jù): 10,time: 1597151080504
讀取到數(shù)據(jù): 10产喉,time: 1597151080704
讀取到數(shù)據(jù): 10捂掰,time: 1597151080704
讀取到數(shù)據(jù): 10,time: 1597151080704
讀取到數(shù)據(jù): 10曾沈,time: 1597151080704
讀取到數(shù)據(jù): 10这嚣,time: 1597151080704
讀取到數(shù)據(jù): 10,time: 1597151080704
讀取到數(shù)據(jù): 10塞俱,time: 1597151080704
讀取到數(shù)據(jù): 10姐帚,time: 1597151080704
讀取到數(shù)據(jù): 10,time: 1597151080704
讀取到數(shù)據(jù): 10障涯,time: 1597151080704
6罐旗、鎖的開銷
鎖的開銷主要包含幾點(diǎn):
- 上下文切換與線程調(diào)度開銷。一個(gè)線程在申請(qǐng)已經(jīng)被其它線程持有的鎖時(shí)唯蝶,該線程就有可能會(huì)被暫停運(yùn)行九秀,直到鎖被釋放后被該線程申請(qǐng)到,也有可能不會(huì)被暫停運(yùn)行粘我,而是采用忙等策略直到鎖被釋放颤霎。如果申請(qǐng)鎖的線程被暫停,Java 虛擬機(jī)就需要為被暫停的線程維護(hù)一個(gè)等待隊(duì)列涂滴,以便后續(xù)鎖的持有線程釋放鎖時(shí)將這些線程喚醒友酱。線程的暫停與喚醒就是一個(gè)上下文切換的過(guò)程,并且 Java 虛擬機(jī)維護(hù)等待隊(duì)列也是有著一定消耗柔纵。如果是非爭(zhēng)用鎖則不會(huì)產(chǎn)生上下文切換和等待隊(duì)列的開銷
- 內(nèi)存同步缔杉、編譯器優(yōu)化受限的開銷。鎖的底層實(shí)現(xiàn)需要使用到內(nèi)存屏障搁料,而內(nèi)部屏障會(huì)產(chǎn)生直接和間接的開銷或详。直接開銷是內(nèi)存屏障所的沖刷寫處理器、清空無(wú)效化隊(duì)列等行為所導(dǎo)致的開銷郭计。間接開銷包含:禁止部分代碼重排序從而阻礙編譯器優(yōu)化霸琴。無(wú)論是爭(zhēng)用鎖還是非爭(zhēng)用鎖都會(huì)產(chǎn)生這部分開銷,但如果非爭(zhēng)用鎖最終可以被采用鎖消除技術(shù)進(jìn)行優(yōu)化的話昭伸,那么就可以消除掉這個(gè)鎖帶來(lái)的開銷
- 限制可伸縮性梧乘。采用鎖的目的是使得多個(gè)線程間的并發(fā)改為帶有局部串行的并發(fā),實(shí)現(xiàn)這個(gè)目的后帶來(lái)的副作用就是使得系統(tǒng)的局部計(jì)算行為(同步代碼塊)的吞吐率降低,限制系統(tǒng)的可伸縮性选调,導(dǎo)致處理器資源的浪費(fèi)
三夹供、wait / notify
在單線程編程中,如果程序要執(zhí)行的操作需要滿足一定的運(yùn)行條件后才可以執(zhí)行仁堪,那么我們可以將目標(biāo)操作放到一個(gè) if 語(yǔ)句中哮洽,讓目標(biāo)操作只有在運(yùn)行條件得以滿足時(shí)才會(huì)被執(zhí)行
而在多線程編程中,目標(biāo)操作的運(yùn)行條件可能涉及到多個(gè)線程間的共享變量弦聂,即運(yùn)行條件可能是由多個(gè)線程來(lái)共同決定的鸟辅。對(duì)于目標(biāo)操作的執(zhí)行線程來(lái)說(shuō),運(yùn)行條件可能只是暫時(shí)未滿足的莺葫,其它線程可能在稍后就會(huì)更新運(yùn)行條件涉及的共享變量從而使得運(yùn)行條件成立剔桨。因此,我們可以選擇將當(dāng)前線程暫停徙融,等待其它線程更新了共享變量使得運(yùn)行條件成立后,再由其它線程來(lái)將被暫停的線程喚醒以便讓其執(zhí)行目標(biāo)操作
當(dāng)中瑰谜,一個(gè)線程因?yàn)橐獔?zhí)行的目標(biāo)動(dòng)作所需的保護(hù)條件未滿足而被暫停的過(guò)程就被稱為等待(wait)欺冀。一個(gè)線程更新了共享變量,使得其它線程所需的保護(hù)條件得以滿足并喚醒那些被暫停的線程的過(guò)程就被稱為通知(notify)
在 Java 平臺(tái)上萨脑,以下兩類方法可用于實(shí)現(xiàn)等待和通知隐轩,Object 可以是任何對(duì)象。由于等待線程和通知線程在實(shí)現(xiàn)等待和通知的時(shí)候必須是調(diào)用同一個(gè)對(duì)象的 wait渤早、notify 方法职车,且其執(zhí)行線程必須持有該對(duì)象的內(nèi)部鎖,所以等待線程和通知線程是同步在同一個(gè)對(duì)象上的兩種線程
- Object.wait()/Object.wait(long)鹊杖。這兩個(gè)方法的作用是使其執(zhí)行線程暫停悴灵,生命周期變?yōu)?WAITING,可用于實(shí)現(xiàn)等待骂蓖。其執(zhí)行線程就被稱為等待線程
- Object.notify()/Object.notifyAll()积瞒。這兩個(gè)方法的作用是喚醒一個(gè)或多個(gè)被暫停的線程,可用于實(shí)現(xiàn)通知登下。其執(zhí)行線程就被稱為通知線程
1茫孔、wait
使用 Object.wait()
實(shí)現(xiàn)等待,其代碼模板如以下偽代碼所示:
//在調(diào)用 wait 方法前獲得相應(yīng)對(duì)象的內(nèi)部鎖
synchronized(someObject){
while (保護(hù)條件不成立) {
//調(diào)用 wait 方法暫停當(dāng)前線程被芳,并同時(shí)釋放已持有的鎖
someObject.wait()
}
//能執(zhí)行到這里說(shuō)明保護(hù)條件已經(jīng)滿足
//執(zhí)行目標(biāo)動(dòng)作
doAction()
}
當(dāng)中缰贝,保護(hù)條件是一個(gè)包含共享變量的布爾表達(dá)式
當(dāng)保護(hù)條件不成立時(shí),因執(zhí)行 someObject.wait()
而被暫停的線程就被稱為對(duì)象 someObject 上的等待線程畔濒。由于一個(gè)對(duì)象的 wait()
方法可以被多個(gè)線程執(zhí)行剩晴,因此一個(gè)對(duì)象可能存在多個(gè)等待線程。此外侵状,由于一個(gè)線程只有在持有一個(gè)對(duì)象的內(nèi)部鎖的情況下才能夠調(diào)用該對(duì)象的 wait()
方法李破,因此 Object.wait()
總是放在相應(yīng)對(duì)象所引導(dǎo)的臨界區(qū)之中宠哄。someObject.wait()
會(huì)以原子操作的方式使其執(zhí)行線程(即等待線程)暫停并使該線程釋放其持有的 someObject 對(duì)應(yīng)的內(nèi)部鎖。當(dāng)?shù)却€程被暫停的時(shí)候其對(duì) someObject.wait()
方法的調(diào)用并不會(huì)返回嗤攻,只有當(dāng)?shù)却€程被通知線程喚醒且重新申請(qǐng)到 someObject 對(duì)應(yīng)的內(nèi)部鎖時(shí)毛嫉,才會(huì)繼續(xù)執(zhí)行 someObject.wait()
內(nèi)部剩余的指令,這時(shí) wait()
才會(huì)返回
當(dāng)?shù)却€程被喚醒時(shí)妇菱,等待線程在其被喚醒繼續(xù)運(yùn)行到其再次申請(qǐng)到相應(yīng)對(duì)象的內(nèi)部鎖的這段時(shí)間內(nèi)承粤,其它線程有可能會(huì)搶先獲得相應(yīng)的內(nèi)部鎖并更新了相關(guān)共享變量導(dǎo)致保護(hù)條件再次不成立,因此 someObject.wait()
調(diào)用返回之后我們需要再次判斷此時(shí)保護(hù)條件是否成立闯团。所以辛臊,對(duì)保護(hù)條件的判斷以及 someObject.wait()
的調(diào)用應(yīng)該放在循環(huán)語(yǔ)句之中,以確保目標(biāo)動(dòng)作一定只在保護(hù)條件成立的情況下才會(huì)被執(zhí)行
此外房交,等待線程對(duì)保護(hù)條件的判斷以及目標(biāo)動(dòng)作的執(zhí)行必須是原子操作彻舰,否則可能產(chǎn)生競(jìng)態(tài),即目標(biāo)動(dòng)作被執(zhí)行前的那一刻其它線程可能對(duì)共享變量進(jìn)行了更新又使得保護(hù)條件重新不成立候味。因此刃唤,保護(hù)條件的判斷、目標(biāo)動(dòng)作的執(zhí)行白群、Object.wait() 的調(diào)用都必須放在同一個(gè)對(duì)象所引導(dǎo)的臨界區(qū)中
2尚胞、notify
使用 Object.notify()
實(shí)現(xiàn)通知,其代碼模板如以下偽代碼所示:
synchronized(someObject){
//更新等待線程的保護(hù)條件涉及的共享變量
updateSharedState()
//喚醒等待線程
someObject.notify()
}
由于只有在持有一個(gè)對(duì)象的內(nèi)部鎖的情況下才能夠執(zhí)行該對(duì)象的 notify()
方法帜慢,所以 Object.notify()
方法也總是放在相應(yīng)對(duì)象內(nèi)部鎖所引導(dǎo)的臨界區(qū)之內(nèi)笼裳。也正因?yàn)槿绱耍?Object.wait()
在暫停其執(zhí)行線程的同時(shí)也必須釋放 Object 的內(nèi)部鎖,否則通知線程就永遠(yuǎn)也無(wú)法來(lái)喚醒等待線程粱玲。和 Object.wait()
不同躬柬,Object.notify()
方法本身并不會(huì)釋放內(nèi)部鎖,只有在其所在的臨界區(qū)代碼執(zhí)行結(jié)束后才會(huì)被釋放抽减。因此楔脯,為了使得等待線程在被喚醒后能夠盡快獲得相應(yīng)的內(nèi)部鎖,我們要盡量將 Object.notify()
代碼放在靠近臨界區(qū)結(jié)束的地方胯甩,否則如果 Object.notify()
喚醒了等待線程而通知線程又遲遲不釋放內(nèi)部鎖昧廷,就有可能導(dǎo)致等待線程再次經(jīng)歷上下文切換,從而浪費(fèi)系統(tǒng)資源
調(diào)用 Object.notify()
所喚醒的線程僅是 Object 對(duì)象上的任意一個(gè)等待線程偎箫,所以被喚醒的線程有可能并不是我們真正想要喚醒的線程木柬。因此,有時(shí)我們需要改用 Object.notifyAll()
方法淹办,該方法可以喚醒 Object 上的所有等待線程眉枕。被喚醒的線程就都有了搶奪相應(yīng) Object 對(duì)象的內(nèi)部鎖的機(jī)會(huì)。而如果被喚醒的線程在占用處理器繼續(xù)運(yùn)行后且申請(qǐng)到內(nèi)部鎖之前,有其它線程(被喚醒的等待線程之一或者是新到來(lái)的線程)先持有了內(nèi)部鎖速挑,那么這個(gè)被喚醒的線程可能又會(huì)再次被暫停谤牡,等待再次被喚醒的機(jī)會(huì),而這個(gè)過(guò)程會(huì)導(dǎo)致上下文切換
wait/notify 機(jī)制也被應(yīng)用于 Thread 類內(nèi)部姥宝。例如翅萤,Thread.join()
方法提供了在某個(gè)線程運(yùn)行結(jié)束前暫停該方法調(diào)用者線程的功能,內(nèi)部也使用到 wait()
方法來(lái)暫停調(diào)用者線程腊满,等到線程終止后 JVM 內(nèi)部就會(huì)通過(guò) notifyAll()
方法來(lái)喚醒所有等待線程
public final synchronized void join(long millis) throws InterruptedException {
···
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
···
}
}
3套么、wait / notify 存在的問(wèn)題
用 wait / notify 實(shí)現(xiàn)的等待和通知可能會(huì)遇到以下兩個(gè)問(wèn)題:
- 過(guò)早喚醒。假設(shè)存在多個(gè)等待線程同步在對(duì)象 someObject 上碳蛋,每個(gè)等待線程的運(yùn)行保護(hù)條件并不完成相同胚泌。當(dāng)通知線程更新了某個(gè)等待線程的運(yùn)行保護(hù)條件涉及的共享變量并使之成立時(shí),由于
someObject.notify()
方法具體會(huì)喚醒哪個(gè)線程對(duì)于開發(fā)者來(lái)說(shuō)是不可預(yù)知的肃弟,所以我們只能使用someObject.notifyAll()
方法玷室,此時(shí)就會(huì)導(dǎo)致那些運(yùn)行條件還不成立的等待線程也被喚醒,這種現(xiàn)象就叫做過(guò)早喚醒笤受。過(guò)早喚醒會(huì)使得那些運(yùn)行條件還不滿足的等待線程也被喚醒運(yùn)行穷缤,當(dāng)這些線程再次判斷到當(dāng)前運(yùn)行條件不滿足時(shí)又會(huì)再次調(diào)用someObject.wait()
方法暫停 - 多次的線程上下文切換。對(duì)于一次完整的 wait 和 notify 過(guò)程感论,等待線程執(zhí)行
someObject.wait()
方法至少會(huì)導(dǎo)致等待線程對(duì)相應(yīng)內(nèi)部鎖的兩次申請(qǐng)和兩次釋放,通知線程執(zhí)行someObject.notify()
方法則會(huì)導(dǎo)致通知線程對(duì)相應(yīng)內(nèi)部鎖的一次申請(qǐng)和一次釋放紊册。每個(gè)線程每次鎖的申請(qǐng)與釋放操作都對(duì)應(yīng)著一次線程上下文切換
4比肄、生產(chǎn)者與消費(fèi)者
wait 和 notify 兩個(gè)方法的協(xié)作可以通過(guò)一個(gè)經(jīng)典的問(wèn)題來(lái)展示:生產(chǎn)者與消費(fèi)者問(wèn)題。對(duì)于一家商店來(lái)說(shuō)囊陡,其能承載的商品最大數(shù)是固定的芳绩,最多為 MAX_COUNT。存在多個(gè)生產(chǎn)者為商店生產(chǎn)商品撞反,生產(chǎn)者生產(chǎn)商品不能使得商店內(nèi)的商品數(shù)量超出 MAX_COUNT妥色。存在多個(gè)消費(fèi)者從商店消費(fèi)商品,消費(fèi)者消費(fèi)過(guò)后最低使商店的商品總數(shù)變?yōu)榱愣羝Ia(chǎn)者只有當(dāng)商店內(nèi)的商品總數(shù)小于 MAX_COUNT 時(shí)才能繼續(xù)生產(chǎn)嘹害,消費(fèi)者只有當(dāng)商店內(nèi)的商品總數(shù)大于零時(shí)才能進(jìn)行消費(fèi)。因此吮便,當(dāng)商店內(nèi)的商品總數(shù)小于 MAX_COUNT 時(shí)需要通知生產(chǎn)者開始生產(chǎn)笔呀,否則需要暫停生產(chǎn)。當(dāng)商店內(nèi)的商品總數(shù)大于零時(shí)需要通知消費(fèi)者來(lái)消費(fèi)髓需,否則需要暫停消費(fèi)
上述的生產(chǎn)者與消費(fèi)者分別對(duì)應(yīng)多個(gè)線程许师,商店就相當(dāng)于多個(gè)線程間的共享數(shù)據(jù)。生產(chǎn)者線程的運(yùn)行保護(hù)條件是:當(dāng)前商品總數(shù)不能大于等于 MAX_COUNT。消費(fèi)者線程的運(yùn)行保護(hù)條件是:當(dāng)前商品總數(shù)要大于 0
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
private val LOCK = Object()
private const val MAX_COUNT = 10
class Shop(var goodsCount: Int)
fun main() {
val shop = Shop(0)
val producerSize = 4
val consumerSize = 8
for (i in 1..producerSize) {
Producer(shop, "生產(chǎn)者-${i}").apply {
start()
}
}
for (i in 1..consumerSize) {
Consumer(shop, "消費(fèi)者-${i}").apply {
start()
}
}
}
class Producer(private val shop: Shop, name: String) : Thread(name) {
override fun run() {
while (true) {
Tools.randomSleep()
synchronized(LOCK) {
while (shop.goodsCount >= MAX_COUNT) {
println("${name}-商品總數(shù)已達(dá)到最大數(shù)量微渠,停止生產(chǎn)")
LOCK.wait()
}
val number = Tools.randomInt(1, MAX_COUNT - shop.goodsCount)
shop.goodsCount = shop.goodsCount + number
println("$name ===== 新增了 $number 件商品搭幻,當(dāng)前剩余: ${shop.goodsCount}")
LOCK.notifyAll()
}
}
}
}
class Consumer(private val shop: Shop, name: String) : Thread(name) {
override fun run() {
while (true) {
Tools.randomSleep()
synchronized(LOCK) {
while (shop.goodsCount <= 0) {
println("${name}-商品已經(jīng)被消費(fèi)光了,停止消費(fèi)")
LOCK.wait()
}
val number = Tools.randomInt(1, shop.goodsCount)
shop.goodsCount = shop.goodsCount - number
println("$name ---- 消費(fèi)了 $number 件商品逞盆,當(dāng)前剩余: ${shop.goodsCount}")
LOCK.notifyAll()
}
}
}
}
其運(yùn)行結(jié)果類似于如下所示
生產(chǎn)者-2 ===== 新增了 7 件商品檀蹋,當(dāng)前剩余: 7
消費(fèi)者-7 ---- 消費(fèi)了 4 件商品,當(dāng)前剩余: 3
消費(fèi)者-7 ---- 消費(fèi)了 2 件商品纳击,當(dāng)前剩余: 1
消費(fèi)者-8 ---- 消費(fèi)了 1 件商品续扔,當(dāng)前剩余: 0
生產(chǎn)者-4 ===== 新增了 2 件商品,當(dāng)前剩余: 2
消費(fèi)者-2 ---- 消費(fèi)了 1 件商品焕数,當(dāng)前剩余: 1
生產(chǎn)者-1 ===== 新增了 8 件商品纱昧,當(dāng)前剩余: 9
消費(fèi)者-1 ---- 消費(fèi)了 3 件商品,當(dāng)前剩余: 6
消費(fèi)者-1 ---- 消費(fèi)了 2 件商品堡赔,當(dāng)前剩余: 4
消費(fèi)者-3 ---- 消費(fèi)了 1 件商品识脆,當(dāng)前剩余: 3
消費(fèi)者-1 ---- 消費(fèi)了 2 件商品,當(dāng)前剩余: 1
消費(fèi)者-5 ---- 消費(fèi)了 1 件商品善已,當(dāng)前剩余: 0
消費(fèi)者-8-商品已經(jīng)被消費(fèi)光了灼捂,停止消費(fèi)
消費(fèi)者-7-商品已經(jīng)被消費(fèi)光了,停止消費(fèi)
生產(chǎn)者-3 ===== 新增了 1 件商品换团,當(dāng)前剩余: 1
消費(fèi)者-7 ---- 消費(fèi)了 1 件商品悉稠,當(dāng)前剩余: 0
消費(fèi)者-8-商品已經(jīng)被消費(fèi)光了,停止消費(fèi)
消費(fèi)者-2-商品已經(jīng)被消費(fèi)光了,停止消費(fèi)
生產(chǎn)者-2 ===== 新增了 3 件商品肘迎,當(dāng)前剩余: 3
消費(fèi)者-2 ---- 消費(fèi)了 2 件商品爷贫,當(dāng)前剩余: 1
消費(fèi)者-8 ---- 消費(fèi)了 1 件商品,當(dāng)前剩余: 0
消費(fèi)者-6-商品已經(jīng)被消費(fèi)光了卦尊,停止消費(fèi)
······
四、線程同步工具類
1舌厨、Condition
wait/notify 存在過(guò)早喚醒岂却、可能多次線程上下文切換次數(shù)、無(wú)法區(qū)分 Obejct.wait(long)
方法在返回時(shí)是由于超時(shí)還是由于線程被喚醒等一系列問(wèn)題裙椭,可以使用 JDK 1.5 開始引入的 java.util.concurrent.locks.Condition
接口來(lái)解決這些問(wèn)題
Condition 接口定義的 await()躏哩、singal()、singalAll() 等方法相當(dāng)于 Object.wait()揉燃、Object.notify()震庭、Object.notifyAll()。Object.wait()/notify() 方法要求其執(zhí)行線程必須持有相應(yīng)對(duì)象的內(nèi)部鎖你雌,類似的器联,Condition.await()/singal() 也要求其執(zhí)行線程必須持有創(chuàng)建該 Condition 實(shí)例的顯式鎖
使用 Condition 實(shí)現(xiàn)等待和通知二汛,其代碼模板如以下偽代碼所示。Lock.newCondition()
方法的返回值就是一個(gè) Condition 實(shí)例拨拓。每個(gè) Condition 實(shí)例內(nèi)部都維護(hù)了一個(gè)用于存儲(chǔ)等待線程的隊(duì)列肴颊,Condition.await()
方法的執(zhí)行線程會(huì)被暫停并存入等待隊(duì)列中。Condition.notify()
方法會(huì)分別使等待隊(duì)列中的一個(gè)線程被喚醒渣磷,而用同一個(gè) Lock 創(chuàng)建的其它 Condition 實(shí)例中的等待線程并不會(huì)收到影響婿着。這就使得我們可以精準(zhǔn)喚醒目標(biāo)線程,避免過(guò)早喚醒醋界,減少線程上下文切換的次數(shù)
class ConditionDemo {
private val lock = ReentrantLock()
private val conditionA = lock.newCondition()
private val conditionB = lock.newCondition()
fun waitA() {
lock.lock()
try {
while (運(yùn)行保護(hù)條件A不成立) {
conditionA.await()
}
//執(zhí)行目標(biāo)操作
doAction()
} finally {
lock.unlock()
}
}
fun notifyA() {
lock.lock()
try {
//更新等待線程的保護(hù)條件涉及的共享變量
updateSharedStateA()
//喚醒等待線程
conditionA.signal()
} finally {
lock.unlock()
}
}
fun waitB() {
lock.lock()
try {
while (運(yùn)行保護(hù)條件B不成立) {
conditionB.await()
}
//執(zhí)行目標(biāo)操作
doAction()
} finally {
lock.unlock()
}
}
fun notifyB() {
lock.lock()
try {
//更新等待線程的保護(hù)條件涉及的共享變量
updateSharedStateB()
//喚醒等待線程
conditionB.signal()
} finally {
lock.unlock()
}
}
}
這里通過(guò)設(shè)計(jì)一個(gè)簡(jiǎn)單的阻塞隊(duì)列來(lái)演示下 Condition 的用法
在 Queue 中竟宋,Lock 保障了 put 操作和 take 操作的線程安全性,兩個(gè)不同的 Condition 實(shí)例又保障了 putThread 和 takeThread 在各自運(yùn)行保護(hù)條件成立時(shí)形纺,可以只喚醒相應(yīng)的線程
class Queue<T> constructor(private val size: Int) {
private val lock = ReentrantLock()
//當(dāng)隊(duì)列已滿時(shí)丘侠,put thread 就成為 notFull 上的等待線程
private val notFull = lock.newCondition()
//當(dāng)隊(duì)列為空時(shí),take thread 就成為 notEmpty 上的等待線程
private val notEmpty = lock.newCondition()
private val items = mutableListOf<T>()
fun put(x: T) {
lock.lock()
try {
while (items.size == size) {
println("當(dāng)前隊(duì)列已滿逐样,暫停 put 操作...")
notFull.await()
}
println("當(dāng)前隊(duì)列未滿蜗字,執(zhí)行 put 操作...")
items.add(x)
//喚醒 TakeThread
notEmpty.signal()
} finally {
lock.unlock()
}
}
fun take(): T {
lock.lock()
try {
while (items.size == 0) {
println("當(dāng)前隊(duì)列為空,暫停 take 操作...")
notEmpty.await()
}
println("當(dāng)前隊(duì)列不為空脂新,執(zhí)行 take 操作...")
val x = items[0]
items.removeAt(0)
//喚醒 PutThread
notFull.signal()
return x
} finally {
lock.unlock()
}
}
}
PutThread 負(fù)責(zé)循環(huán)向阻塞隊(duì)列存入八條數(shù)據(jù)挪捕,TakeThread 負(fù)責(zé)循環(huán)從阻塞隊(duì)列獲取九條數(shù)據(jù),TakeThread 隨機(jī)休眠的時(shí)間相對(duì) PutThread 會(huì)更長(zhǎng)争便,而阻塞隊(duì)列的隊(duì)長(zhǎng)為四级零,那么在程序運(yùn)行過(guò)程中大概率可以看到由于阻塞隊(duì)列已滿從而導(dǎo)致 PutThread 被暫停的現(xiàn)象,且程序運(yùn)行到最后 TakeThread 會(huì)為了獲取第九條數(shù)據(jù)而一直處于等待狀態(tài)
class PutThread(private val intQueue: Queue<Int>) : Thread() {
override fun run() {
for (i in 1..8) {
sleep(Random.nextLong(1, 50))
intQueue.put(i)
}
}
}
class TakeThread(private val intQueue: Queue<Int>) : Thread() {
override fun run() {
for (i in 1..9) {
sleep(Random.nextLong(10, 100))
println("TakeThread get value: " + intQueue.take())
}
}
}
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
fun main() {
val intQueue = Queue<Int>(4)
val putThread = PutThread(intQueue)
val takeThread = TakeThread(intQueue)
putThread.start()
takeThread.start()
}
從程序的輸出結(jié)果可以看到 TakeThread 最終將一直處于等待狀態(tài)
當(dāng)前隊(duì)列未滿滞乙,執(zhí)行 put 操作...
當(dāng)前隊(duì)列未滿奏纪,執(zhí)行 put 操作...
當(dāng)前隊(duì)列未滿,執(zhí)行 put 操作...
當(dāng)前隊(duì)列不為空酷宵,執(zhí)行 take 操作...
TakeThread get value : 1
當(dāng)前隊(duì)列未滿亥贸,執(zhí)行 put 操作...
當(dāng)前隊(duì)列未滿躬窜,執(zhí)行 put 操作...
當(dāng)前隊(duì)列已滿浇垦,暫停 put 操作...
當(dāng)前隊(duì)列不為空,執(zhí)行 take 操作...
TakeThread get value : 2
當(dāng)前隊(duì)列未滿荣挨,執(zhí)行 put 操作...
當(dāng)前隊(duì)列不為空男韧,執(zhí)行 take 操作...
TakeThread get value : 3
當(dāng)前隊(duì)列未滿,執(zhí)行 put 操作...
當(dāng)前隊(duì)列不為空默垄,執(zhí)行 take 操作...
TakeThread get value : 4
當(dāng)前隊(duì)列未滿此虑,執(zhí)行 put 操作...
當(dāng)前隊(duì)列不為空,執(zhí)行 take 操作...
TakeThread get value : 5
當(dāng)前隊(duì)列不為空口锭,執(zhí)行 take 操作...
TakeThread get value : 6
當(dāng)前隊(duì)列不為空朦前,執(zhí)行 take 操作...
TakeThread get value : 7
當(dāng)前隊(duì)列不為空介杆,執(zhí)行 take 操作...
TakeThread get value : 8
當(dāng)前隊(duì)列為空,暫停 take 操作...
2韭寸、CountDownLatch
有時(shí)候會(huì)存在某個(gè)線程需要等待其它線程完成特定操作后才能繼續(xù)運(yùn)行的需求春哨,此時(shí)使用 Object.wait()
和 Object.notify()
也可以滿足需求,但是使用上會(huì)比較繁瑣恩伺,此時(shí)可以考慮通過(guò) CountDownLatch 來(lái)實(shí)現(xiàn)
CountDownLatch 可用來(lái)實(shí)現(xiàn)一個(gè)或多個(gè)線程等待其它線程完成特定操作后才繼續(xù)運(yùn)行的功能赴背,這組操作被稱為先決條件。CountDownLatch 內(nèi)部會(huì)維護(hù)一個(gè)用于標(biāo)記需要等待完成的先決條件的數(shù)量的計(jì)數(shù)器晶渠,當(dāng)每個(gè)先決條件完成時(shí)凰荚,先決條件的執(zhí)行線程就通過(guò)調(diào)用 CountDownLatch.countDown()
來(lái)使計(jì)算器減一。而 CountDownLatch.await()
就相當(dāng)于一個(gè)受保護(hù)方法褒脯,其保護(hù)條件為“計(jì)算器值為零”便瑟,當(dāng)計(jì)算器值不為零時(shí),調(diào)用了 CountDownLatch.await()
方法的執(zhí)行線程都會(huì)被暫停憨颠。當(dāng)所有先決條件都完成時(shí)胳徽,即當(dāng)“計(jì)算器值為零”的保護(hù)條件成立時(shí),CountDownLatch 上的所有等待線程就都會(huì)被喚醒爽彤,繼續(xù)運(yùn)行
當(dāng)計(jì)數(shù)器的值達(dá)到 0 之后养盗,該計(jì)數(shù)器的值就不再發(fā)生變化,后續(xù)繼續(xù)調(diào)用 CountDownLatch.countDown()
也不會(huì)導(dǎo)致拋出異常适篙,且再次調(diào)用 CountDownLatch.await()
方法也不會(huì)導(dǎo)致線程被暫停往核。因此,一個(gè) CountDownLatch 實(shí)例只能用來(lái)實(shí)現(xiàn)一次等待和一次通知
來(lái)看一個(gè)簡(jiǎn)單的例子嚷节。假設(shè)在程序啟動(dòng)時(shí)需要確保三個(gè)基礎(chǔ)服務(wù)(ServiceA聂儒、ServiceB、ServiceC)先被初始化完成硫痰,且為了加快初始化速度衩婚,每個(gè)基礎(chǔ)服務(wù)均交由一個(gè)工作者線程來(lái)完成初始化任務(wù)。此時(shí)就可以通過(guò) CountDownLatch 來(lái)保證 main 線程一直處于等待狀態(tài)直到所有的工作者線程的任務(wù)均結(jié)束(不管初始化成功還是失斝О摺)
/**
* @Author: leavesCZY
* @Github:https://github.com/leavesCZY
*/
fun main() {
val serviceManager = ServicesManager()
serviceManager.startServices()
println("等待所有 Services 執(zhí)行完畢")
val allSuccess = serviceManager.checkState()
println("執(zhí)行結(jié)果: $allSuccess")
}
class ServicesManager {
private val countDownLatch = CountDownLatch(3)
private val serviceList = mutableListOf<AbstractService>()
init {
serviceList.add(ServiceA("ServiceA", countDownLatch))
serviceList.add(ServiceB("ServiceB", countDownLatch))
serviceList.add(ServiceC("ServiceC", countDownLatch))
}
fun startServices() {
serviceList.forEach {
it.start()
}
}
fun checkState(): Boolean {
countDownLatch.await()
return serviceList.find { !it.checkState() } == null
}
}
abstract class AbstractService(private val countDownLatch: CountDownLatch) {
private var success = false
abstract fun doTask(): Boolean
fun start() {
thread {
try {
success = doTask()
} finally {
countDownLatch.countDown()
}
}
}
fun checkState(): Boolean {
return success
}
}
class ServiceA(private val serviceName: String, countDownLatch: CountDownLatch) : AbstractService(countDownLatch) {
override fun doTask(): Boolean {
Thread.sleep(2000)
println("${serviceName}執(zhí)行完畢")
return true
}
}
class ServiceB(private val serviceName: String, countDownLatch: CountDownLatch) : AbstractService(countDownLatch) {
override fun doTask(): Boolean {
Thread.sleep(4000)
println("${serviceName}執(zhí)行完畢")
return true
}
}
class ServiceC(private val serviceName: String, countDownLatch: CountDownLatch) : AbstractService(countDownLatch) {
override fun doTask(): Boolean {
Thread.sleep(3000)
if (Random.nextBoolean()) {
throw RuntimeException("$serviceName failed")
} else {
println("${serviceName}執(zhí)行完畢")
}
return true
}
}
ServiceC 會(huì)隨機(jī)拋出異常非春,所以程序的運(yùn)行結(jié)果會(huì)分為以下兩種可能。且為了保證當(dāng)任務(wù)失敗時(shí) main 線程也可以收到喚醒通知缓屠,需要確保 CountDownLatch.countDown()
是放在 finally 代碼塊中
# 成功的情況
等待所有 Services 執(zhí)行完畢
ServiceA執(zhí)行完畢
ServiceC執(zhí)行完畢
ServiceB執(zhí)行完畢
執(zhí)行結(jié)果: true
# 失敗的情況
等待所有 Services 執(zhí)行完畢
ServiceA執(zhí)行完畢
Exception in thread "Thread-2" java.lang.RuntimeException: ServiceC failed
at thread.ServiceC.doTask(CountDownLatchTest.kt:93)
at thread.AbstractService$start$1.invoke(CountDownLatchTest.kt:55)
at thread.AbstractService$start$1.invoke(CountDownLatchTest.kt:46)
at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30)
ServiceB執(zhí)行完畢
執(zhí)行結(jié)果: false
3奇昙、CyclicBarrier
JDK 1.5 引入了 java.util.concurrent.CyclicBarrier
類用于實(shí)現(xiàn)多個(gè)線程間的相互等待。CyclicBarrier 可用于這么一種場(chǎng)景:假設(shè)存在一個(gè)集合點(diǎn)敌完,在所有線程均執(zhí)行到集合點(diǎn)之前储耐,每個(gè)執(zhí)行到指定集合點(diǎn)的線程均會(huì)被暫停。當(dāng)所有線程均執(zhí)行到指定集合點(diǎn)時(shí)滨溉,即當(dāng)最后一個(gè)線程執(zhí)行到集合點(diǎn)時(shí)什湘,所有被暫停的線程都會(huì)自動(dòng)被喚醒并繼續(xù)執(zhí)行
CyclicBarrier 的字面意思可以理解為:可循環(huán)使用的屏障长赞。而集合點(diǎn)就相當(dāng)于一個(gè)“屏障”,除非所有線程都抵達(dá)到了屏障闽撤,否則每個(gè)到達(dá)的線程都會(huì)被拒之門外(即被暫停運(yùn)行)涧卵。而當(dāng)最后一個(gè)線程到來(lái)時(shí),“屏障”就會(huì)自動(dòng)消失腹尖,即最后一個(gè)到來(lái)的線程不會(huì)被暫停柳恐,而是繼續(xù)向下執(zhí)行代碼,同時(shí)喚醒所有其它之前被暫停的線程
CyclicBarrier 類涉及到的線程數(shù)量可以通過(guò)其構(gòu)造參數(shù) parties 來(lái)指定热幔,CyclicBarrier.await()
方法就用于標(biāo)記當(dāng)前線程執(zhí)行到了指定集合點(diǎn)乐设。在功能上 CyclicBarrier 與 CountDownLatch 相似,但 CyclicBarrier 實(shí)例是可以重復(fù)使用的绎巨,在所有線程都被喚醒之后近尚,任何線程再次執(zhí)行 CyclicBarrier.await()
方法又會(huì)被暫停,直到最后一個(gè)線程也執(zhí)行了該方法场勤。所以戈锻,CyclicBarrier.await()
方法既是等待方法也是通知方法,最后一個(gè)執(zhí)行線程就相當(dāng)于通知線程和媳,其它線程就相當(dāng)于等待線程格遭,線程的具體類別由其運(yùn)行時(shí)序來(lái)動(dòng)態(tài)區(qū)分,而非靠調(diào)用方法的不同
再來(lái)看一個(gè)簡(jiǎn)單的例子留瞳。存在三個(gè)輸出不同字符串內(nèi)容的 PrintThread 線程拒迅,每個(gè)線程每輸出一次,均需要等待其它線程也輸出一次后才能再次輸出她倘,但三個(gè)線程每次的輸出先后順序可以隨意
class PrintThread(private val cyclicBarrier: CyclicBarrier, private val content: String) : Thread() {
override fun run() {
while (true) {
sleep(Random.nextLong(300, 1000))
println("打印完成:${content}")
if (cyclicBarrier.parties == cyclicBarrier.numberWaiting + 1) {
println()
}
cyclicBarrier.await()
}
}
}
fun main() {
val threadNum = 3
val cyclicBarrier = CyclicBarrier(threadNum)
val threadList = mutableListOf<Thread>()
for (i in 1..threadNum) {
threadList.add(PrintThread(cyclicBarrier, "index_$i"))
}
threadList.forEach {
it.start()
}
}
程序的輸出結(jié)果類似如下所示璧微。每一輪輸出的三條數(shù)據(jù)先后順序并不固定,但每一輪的內(nèi)容一定不會(huì)重復(fù)
打印完成:index_2
打印完成:index_1
打印完成:index_3
打印完成:index_2
打印完成:index_1
打印完成:index_3
打印完成:index_2
打印完成:index_1
打印完成:index_3
打印完成:index_3
打印完成:index_1
打印完成:index_2
打印完成:index_1
打印完成:index_3
打印完成:index_2
...
4硬梁、Semaphore
Semaphore 可用于實(shí)現(xiàn)互斥以及流量控制前硫,在某些資源有限的場(chǎng)景下限制可以同時(shí)訪問(wèn)資源的最大線程數(shù)。例如荧止,假設(shè)當(dāng)前有幾十上百個(gè)線程需要連接數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)存取屹电,而數(shù)據(jù)庫(kù)支持的最大連接數(shù)只有十個(gè),此時(shí)就可以通過(guò) Semaphore 來(lái)限制線程的最大并發(fā)數(shù)罩息,當(dāng)已經(jīng)有十個(gè)線程連接到了數(shù)據(jù)庫(kù)時(shí)嗤详,多余的請(qǐng)求線程就會(huì)被暫停
看個(gè)簡(jiǎn)單的例子个扰。對(duì)于以下代碼瓷炮,線程池同時(shí)發(fā)起的請(qǐng)求有三十個(gè),而 Semaphore 限制了最大線程并發(fā)數(shù)是四個(gè)递宅,所以最終的輸出結(jié)果就是會(huì)每隔兩秒輸出四行內(nèi)容
fun main() {
val threadNum = 30
val threadPool = Executors.newFixedThreadPool(threadNum)
val semaphore = Semaphore(4)
for (index in 1..threadNum) {
threadPool.execute {
semaphore.acquire()
try {
Thread.sleep(2000)
println("over $index")
} finally {
semaphore.release()
}
}
}
}
5娘香、Exchanger
Exchanger 可用于實(shí)現(xiàn)在兩個(gè)線程之間交換數(shù)據(jù)的功能苍狰。當(dāng)線程 A 先通過(guò) Exchanger 發(fā)起交換數(shù)據(jù)的請(qǐng)求時(shí),線程 A 會(huì)被暫停運(yùn)行直到線程 B 也發(fā)起交換數(shù)據(jù)的請(qǐng)求烘绽,當(dāng)數(shù)據(jù)交換完成后淋昭,兩個(gè)線程就會(huì)各自繼續(xù)運(yùn)行
fun main() {
val exchanger = Exchanger<String>()
val threadA = object : Thread() {
override fun run() {
sleep(2000)
val result = exchanger.exchange("A")
println("Thread A: $result")
}
}
val threadB = object : Thread() {
override fun run() {
sleep(2000)
val result = exchanger.exchange("B")
println("Thread B: $result")
}
}
threadA.start()
threadB.start()
}
Thread B: A
Thread A: B
五、ThreadLocal
以上介紹的幾個(gè)線程同步工具類安接,目的都是為了在多個(gè)線程之間實(shí)現(xiàn)一種等待機(jī)制翔忽,即在線程 A 完成目標(biāo)行為前,線程 B 能夠依靠這些線程同步工具類進(jìn)行等待盏檐,直到線程 A 完成
ThreadLocal 不太一樣歇式,ThreadLocal 也是用于多線程環(huán)境,但其實(shí)現(xiàn)的目的是為了進(jìn)行數(shù)據(jù)隔離胡野,為每一個(gè)線程維護(hù)一個(gè)獨(dú)有的全局變量材失,從而解決共享變量的并發(fā)安全問(wèn)題
例如,以下代碼中 mainThread 可以獲取到值但 subThread 獲取到的是 null硫豆,因?yàn)?mainThread 進(jìn)行了賦值操作而 subThread 沒有龙巨,每個(gè)線程只會(huì)獲取到自己對(duì) ThreadLocal 的賦值結(jié)果
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("業(yè)志陳");
Thread subThread = new Thread() {
@Override
public void run() {
System.out.println("subThread :" + threadLocal.get());
}
};
subThread.start();
subThread.join();
System.out.println("mainThread: " + threadLocal.get());
}
subThread :null
mainThread: 業(yè)志陳
這里來(lái)簡(jiǎn)單看下 ThreadLocal 的源碼實(shí)現(xiàn)
ThreadLocal 是一個(gè)泛型類,其對(duì)外部開放的方法主要就是 get()熊响、set(T)旨别、remove()、initialValue()
四個(gè)方法
get()
方法用于獲取 ThreadLocal 保存的值汗茄,該方法會(huì)根據(jù)當(dāng)前線程獲取到一個(gè) ThreadLocalMap昼榛,從名字上就可以看出 ThreadLocalMap 具有存儲(chǔ)鍵值對(duì)的特性(雖然并沒有實(shí)現(xiàn) Map 接口),會(huì)以當(dāng)前 ThreadLocal 對(duì)象作為 key 來(lái)進(jìn)行取值剔难〉ㄓ欤可以看到,該 ThreadLocalMap 就保存在當(dāng)前線程所代表的 Thread 對(duì)象中偶宫,即 threadLocals
非迹,所以不同線程間會(huì)維護(hù)單獨(dú)一份數(shù)據(jù),從而實(shí)現(xiàn)數(shù)據(jù)隔離
如果 ThreadLocal 還未賦值過(guò)纯趋,則會(huì)調(diào)用 setInitialValue()
方法來(lái)進(jìn)行初始化憎兽,所以我們可以通過(guò)重寫 initialValue()
方法來(lái)設(shè)置 ThreadLocal 對(duì)所有線程的默認(rèn)初始值
public T get() {
Thread t = Thread . currentThread ();
ThreadLocalMap map = getMap (t);
if (map != null) {
ThreadLocalMap.Entry e = map . getEntry (this);
if (e != null) {
@SuppressWarnings("unchecked")
T result =(T) e . value;
return result;
}
}
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue ();
Thread t = Thread . currentThread ();
ThreadLocalMap map = getMap (t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap (this, firstValue);
}
set(T)
和 remove()
兩個(gè)方法也是以當(dāng)前 ThreadLocal 作為 key,對(duì) ThreadLocalMap 進(jìn)行賦值操作和移除操作
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
可以看到吵冒,ThreadLocal 的主要實(shí)現(xiàn)邏輯還是在于 ThreadLocalMap纯命,每個(gè) Thread 都有自己?jiǎn)为?dú)的一個(gè) ThreadLocalMap 對(duì)象,ThreadLocal 就以自身作為 key 來(lái)存儲(chǔ)在 ThreadLocalMap 中痹栖,每個(gè) ThreadLocal 就用于為本線程維護(hù)一個(gè)特定的值亿汞,從而使得不同線程間即使是使用同個(gè) ThreadLocal 對(duì)象也可以單獨(dú)維護(hù)一個(gè)特定的值
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
由于一個(gè)線程可以關(guān)聯(lián)多個(gè) ThreadLocal,所以 ThreadLocalMap 就以數(shù)組的形式 Entry[]
來(lái)存儲(chǔ) ThreadLocal
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
···
}
ThreadLocal 和關(guān)聯(lián)的 value 會(huì)被包裝為一個(gè) Entry 對(duì)象揪阿,Entry 以弱引用的方式來(lái)保存 ThreadLocal疗我。ThreadLocalMap 的一個(gè)重要知識(shí)點(diǎn)就是考察為什么要以弱引用的方式來(lái)保存 ThreadLocal
想象這么一個(gè)場(chǎng)景咆畏,就能明白為什么不使用強(qiáng)引用的方式
假設(shè)創(chuàng)建 ThreadLocal 變量的是線程 A,線程 B 訪問(wèn)了 ThreadLocal 后就會(huì)將其保存到自身的 ThreadLocalMap 中吴裤。當(dāng)線程 A 結(jié)束運(yùn)行旧找,由于使用了弱引用,ThreadLocal 在沒有外部強(qiáng)引用時(shí)一進(jìn)行 GC 就會(huì)被回收麦牺,避免了內(nèi)存泄漏钮蛛,這種情況就是最理想的了。如果使用強(qiáng)引用愿卒,那么就需要等到線程 B 也結(jié)束運(yùn)行后 ThreadLocal 才能被回收潮秘,而這可能需要一段比較長(zhǎng)的時(shí)間枕荞,即使在這個(gè)過(guò)程中線程 B 都沒有再次使用 ThreadLocal
但依靠弱引用也無(wú)法完全避免內(nèi)存泄漏問(wèn)題,因?yàn)榫€程 A 可能是線程池中的某個(gè)線程渣刷,可以不斷被復(fù)用矗烛,此時(shí)即使 ThreadLocal 被回收了,其對(duì)應(yīng)的 value 卻會(huì)一直被保留著瞭吃,這就造成了 ThreadLocalMap 會(huì)保留著 key 為 null 但 value 不為 null 的 Entry 對(duì)象碌嘀,value 無(wú)法被回收,此時(shí)就一樣會(huì)造成內(nèi)存泄漏歪架。為了解決該問(wèn)題股冗,就需要在線程 A 不再需要使用到 ThreadLocal 時(shí)主動(dòng)調(diào)用 remove()
方法移除掉 Entry 對(duì)象
六、線程中斷機(jī)制
以上介紹的幾種線程間協(xié)作方法的使用初衷都是希望線程完成各自操作后能互相通知和蚪≈棺矗可是還存在這么一種情況:一個(gè)線程請(qǐng)求另外一個(gè)線程停止其正在執(zhí)行的任務(wù)。例如攒霹,對(duì)于一個(gè)地圖應(yīng)用來(lái)說(shuō)怯疤,當(dāng)用戶退出應(yīng)用時(shí),就需要停止后臺(tái)線程正在執(zhí)行的定位任務(wù)催束,因?yàn)榇藭r(shí)該任務(wù)對(duì)于用戶來(lái)說(shuō)是不需要的了
一個(gè)線程向另一個(gè)線程發(fā)起請(qǐng)求集峦,希望其停止任務(wù)的機(jī)制就稱為線程中斷機(jī)制。中斷(interrupt)是由發(fā)起線程向目標(biāo)線程發(fā)送的一種指示,該指示用于表示發(fā)起線程希望目標(biāo)線程停止其正在執(zhí)行的任務(wù)少梁。發(fā)起線程的中斷請(qǐng)求并不是一個(gè)強(qiáng)制性的行為,目標(biāo)線程可能會(huì)在收到中斷指示時(shí)停止任務(wù)矫付,也可能完全不做任何響應(yīng)凯沪,這取決于目標(biāo)線程對(duì)中斷請(qǐng)求的處理邏輯
Java 平臺(tái)會(huì)為每個(gè)線程維護(hù)一個(gè)被稱為中斷標(biāo)記的布爾型變量來(lái)表示相應(yīng)線程是否收到了中斷,值為 true 則表示收到了中斷請(qǐng)求买优。Thread 類包含以下幾個(gè)和中斷相關(guān)的方法
public class Thread implements Runnable {
//向此線程發(fā)起中斷請(qǐng)求
public void interrupt() {
...
}
//在獲取中斷標(biāo)記的同時(shí)將中斷標(biāo)記置為 false
public static boolean interrupted() {
...
}
//獲取中斷標(biāo)記
public boolean isInterrupted() {
...
}
}
目標(biāo)線程收到中斷請(qǐng)求后所執(zhí)行的操作杀赢,被稱為目標(biāo)線程對(duì)中斷的響應(yīng)滤淳,簡(jiǎn)稱中斷響應(yīng)。目標(biāo)線程對(duì)中斷的響應(yīng)類型一般包括:
- 無(wú)影響屁擅。例如,
ReentrantLock.lock()
或者內(nèi)部鎖申請(qǐng)等操作時(shí)胶果,都不會(huì)對(duì)中斷進(jìn)行響應(yīng),即不會(huì)停止當(dāng)前正在執(zhí)行的操作 - 取消任務(wù)的運(yùn)行贝或。例如,目標(biāo)線程可以在每次執(zhí)行任務(wù)前均檢查中斷標(biāo)記羊赵,當(dāng)中斷標(biāo)記為 true 時(shí)則取消當(dāng)前任務(wù),但還是會(huì)繼續(xù)處理其他任務(wù)
- 停止線程序矩。即令目標(biāo)線程放棄執(zhí)行所有任務(wù),生命周期狀態(tài)變更為 TERMINATED
Java 標(biāo)準(zhǔn)庫(kù)中的許多阻塞方法對(duì)中斷的響應(yīng)都是拋出 InterruptedException 等異常租幕。能夠響應(yīng)中斷的方法通常是在執(zhí)行阻塞操作前判斷中斷標(biāo)記劲绪,若中斷標(biāo)記為 true 則直接拋出 InterruptedException。例如,ReentrantLock.lockInterruptibly()
方法會(huì)在執(zhí)行申請(qǐng)鎖這個(gè)阻塞操作前檢查當(dāng)前線程的中斷標(biāo)記兔乞,當(dāng)中斷標(biāo)記為 true 時(shí)則會(huì)拋出 InterruptedException霍骄。而按照慣例,拋出 InterruptedException 異常的方法一般都會(huì)在拋出該異常之前將當(dāng)前線程的中斷標(biāo)記重置為 false米间。例如,ReentrantLock.lockInterruptibly()
方法會(huì)通過(guò)在 acquireInterruptibly()
方法里調(diào)用 Thread.interrupted()
來(lái)獲取并重置中斷標(biāo)記
public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
如果目標(biāo)線程在收到中斷請(qǐng)求的時(shí)候已經(jīng)由于執(zhí)行了一些阻塞操作而處于暫停狀態(tài)逻锐,那么 Java 虛擬機(jī)可能會(huì)將目標(biāo)線程喚醒晓淀,從而使得目標(biāo)線程被喚醒后繼續(xù)執(zhí)行的代碼可以再次得到響應(yīng)中斷的機(jī)會(huì)
七、如何實(shí)現(xiàn)單例模式
單例模式是 GOF 設(shè)計(jì)模式中比較容易理解且應(yīng)用非常廣泛的一種設(shè)計(jì)模式锄俄,但是實(shí)現(xiàn)一個(gè)能夠在多線程環(huán)境下正常運(yùn)行且兼顧到性能的單例模式卻不是一個(gè)簡(jiǎn)單的事情鱼填,這需要我們同時(shí)運(yùn)用到鎖愤惰、volatile 變量宦言、原子性、可見性银受、有序性等多方面的知識(shí)
1蜀漆、單線程環(huán)境
在單線程環(huán)境下褂始,我們無(wú)需考慮原子性肌蜻、可見性豆挽、有序性等問(wèn)題互站,所以僅需要做到懶加載即可
public final class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { //操作1
instance = new Singleton(); //操作2
}
return instance;
}
}
2容贝、雙重檢查鎖定
對(duì)于上述的在單線程環(huán)境下可以正常使用的單例模式,在多線程環(huán)境下就很容易出現(xiàn)問(wèn)題。getInstance()
方法本身是基于 check-then-act 操作來(lái)判斷是否需要初始化共享變量的掂僵,該操作并不是一個(gè)原子操作冯勉。在 instance 還為 null 時(shí)盈电,假設(shè)有兩個(gè)線程 T1 和 T2 同時(shí)執(zhí)行到操作1吸重,接著在 T1 執(zhí)行操作2之前 T2 已經(jīng)執(zhí)行完操作2田篇,在下一時(shí)刻柄瑰,當(dāng) T1 執(zhí)行到操作2的時(shí)候授翻,即使 instance 當(dāng)前已經(jīng)不為 null,但是 T1 此時(shí)依然會(huì)多創(chuàng)建一個(gè)實(shí)例,這就導(dǎo)致了多個(gè)實(shí)例的創(chuàng)建
首先速梗,我們最先想到的可能是通過(guò)加鎖來(lái)避免這種情況
public static Singleton getInstance() {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
return instance;
}
上述方式實(shí)現(xiàn)的單例模式固然是線程安全的钓试,但是這也意味著 getInstance()
方法的任何一個(gè)執(zhí)行線程都需要申請(qǐng)鎖,為了避免無(wú)謂的鎖開銷嫁赏,人們又想到以下這種方法,即雙重檢查鎖定堤魁。在執(zhí)行臨界區(qū)代碼前先判斷 instance 是否為 null迟杂,如果不為 null 监氢,則直接返回 instance 變量顿乒,否則才執(zhí)行臨界區(qū)代碼來(lái)完成 instance 變量的初始化
public static Singleton getInstance() {
if (instance == null) { //操作1
synchronized (Singleton.class) {
if (instance == null) { //操作2
instance = new Singleton(); //操作3
}
}
}
return instance;
}
上述代碼表現(xiàn)出來(lái)的初始化邏輯可以分為兩種情況犹菱,這兩種情況的前置前提是:存在兩個(gè)線程 T1 和 T2 陕凹,線程 T1 執(zhí)行到了操作1拂盯,線程 T2 執(zhí)行到了臨界區(qū)
- 當(dāng)線程 T1 執(zhí)行到操作1的時(shí)候線程 T2 已經(jīng)執(zhí)行完了操作3空凸,發(fā)現(xiàn)此時(shí) instance 不為 null卖词,直接返回 instance 變量,避免了鎖的開銷
- 當(dāng)線程 T1 執(zhí)行到操作1的時(shí)候發(fā)現(xiàn) instance 為 null彬坏,此時(shí)線程 T2 還處于執(zhí)行操作3之前领跛,那么當(dāng)線程 T2 執(zhí)行臨界區(qū)結(jié)束之前孔轴,線程 T1 均會(huì)處于等待狀態(tài)。當(dāng)線程 T2 執(zhí)行完畢署照,線程 T1 進(jìn)入臨界區(qū)后,由于此時(shí)線程 T1 是在臨界區(qū)內(nèi)讀取共享變量 instance 的秒拔,因此 T1 可以發(fā)現(xiàn)此刻 instance 不為 null曹宴,于是 T1 不會(huì)執(zhí)行操作3窄刘,從而避免了再次創(chuàng)建一個(gè)實(shí)例
上述代碼看起來(lái)似乎避免了鎖的開銷又保障了線程安全谷遂,但還是有著一些邏輯缺陷条摸,因?yàn)樵摲椒▋H考慮到了可見性茵瀑,而沒有考慮到發(fā)生重排序的情況
操作3可以分解為以下三條偽指令所代表的子操作
objRef = allocate(Singleton.class) //子操作1泼菌,分配對(duì)象所需的存儲(chǔ)空間
invokeConstructor(objRef) //子操作2颁糟,初始化 objRef 引用的對(duì)象
instance = objRef //子操作3吟宦,將對(duì)象引用寫入共享變量
由于臨界區(qū)內(nèi)的代碼是有可能被重排序的宛篇,因此,JIT 編譯器可能將上述的子操作重排序?yàn)椋鹤硬僮? -> 子操作3 -> 子操作2。即在初始化對(duì)象之前將對(duì)象的引用寫入實(shí)例變量 instance徐钠。由于鎖對(duì)有序性的保障是有條件的矮台,而線程 T1 在臨界區(qū)之外檢查 instance 是否為 null 的時(shí)候并沒有加鎖乏屯,因此上述重排序?qū)τ诰€程 T1 來(lái)說(shuō)是有影響的,這會(huì)使得線程 T1 得到一個(gè)不為 null 但內(nèi)部還未完全初始化完畢的 instance 變量瘦赫,從而造成一些意想不到的錯(cuò)誤
在分析清楚問(wèn)題的原因后辰晕,解決方法也就不難想到:只要將 instance 變量采用 volatile 修飾即可,這實(shí)際上是利用了 volatile 關(guān)鍵字的以下兩個(gè)作用:
- 保障可見性确虱。一個(gè)線程通過(guò)執(zhí)行
instance = new Singleton()
修改了 instance 變量值含友,其它線程可以讀取到相應(yīng)的值 - 保障有序性。由于 volatile 能夠禁止 volatile 變量寫操作與該操作之前的任何讀校辩、寫操作進(jìn)行重排序窘问,因此,用 volatile 修飾 instance 相當(dāng)于禁止 JIT 編譯器以及處理器將子操作2重排序到子操作3之后宜咒,這保障了一個(gè)線程讀取到 instance 變量所引用的實(shí)例時(shí)該實(shí)例已經(jīng)初始化完畢
因此惠赫,雙重檢查鎖定的單例模式其正確的實(shí)現(xiàn)方式如下所示
public final class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
}
}
3、靜態(tài)內(nèi)部類
類的靜態(tài)變量被初次訪問(wèn)時(shí)會(huì)觸發(fā) Java 虛擬機(jī)對(duì)該類進(jìn)行初始化故黑,即該類的靜態(tài)變量的值會(huì)變?yōu)槠涑跏贾刀辉偈悄J(rèn)值(例如儿咱,引用型變量的默認(rèn)值是 null,int 的默認(rèn)值是 0)场晶。因此混埠,靜態(tài)方法 getInstance()
被調(diào)用的時(shí)候 Java 虛擬機(jī)會(huì)初始化這個(gè)方法所訪問(wèn)的內(nèi)部靜態(tài)類 InstanceHolder。這使得 InstanceHolder 的靜態(tài)變量 INSTANCE 被初始化峰搪,從而使得 Singleton 類的唯一實(shí)例得以創(chuàng)建岔冀。由于類的靜態(tài)變量只會(huì)創(chuàng)建一次,因此 Singleton 也只會(huì)有一個(gè)實(shí)例變量
public final class Singleton {
private Singleton() {
}
private final static class InstanceHolder {
final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return InstanceHolder.INSTANCE;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
}
}
4概耻、枚舉類
枚舉類 Singleton 相當(dāng)于一個(gè)單例類使套,其字段 INSTANCE 相當(dāng)于該類的唯一實(shí)例。這個(gè)實(shí)例是在 Singleton.INSTANCE
初次被引用的時(shí)候才會(huì)被初始化的鞠柄。僅訪問(wèn) Singleton 本身(例如 Singleton.class.getName()
)并不會(huì)導(dǎo)致 Singleton 的唯一實(shí)例被初始化
public class SingletonExample {
public static void main(String[] args) {
Singleton.INSTANCE.doSomething();
}
public enum Singleton {
INSTANCE;
void doSomething() {
}
}
}