聊聊 Java 多線程(2)- 怎么實(shí)現(xiàn)多線程同步

目前魁亦,多線程編程可以說(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):

  1. 使用上缺少靈活性别厘。鎖的申請(qǐng)和釋放操作被限制在一個(gè)代碼塊或者方法體內(nèi)部
  2. 功能有限虱饿。例如,當(dāng)一個(gè)線程申請(qǐng)某個(gè)正被其它線程持有的內(nèi)部鎖時(shí)触趴,該線程只能被暫停氮发,等待鎖被釋放后再次申請(qǐng),而無(wú)法取消申請(qǐng)或者是限時(shí)申請(qǐng)冗懦,且不支持線程中斷
  3. 僅支持非公平調(diào)度策略

其優(yōu)點(diǎn)主要有以下幾點(diǎn):

  1. 使用簡(jiǎn)單
  2. 由于 Java 編譯器的保障爽冕,所以使用時(shí)不會(huì)造成鎖泄露,保障了安全性

顯式鎖是基于對(duì)象的鎖

其缺點(diǎn)主要有以下幾點(diǎn):

  1. 需要開發(fā)者自己來(lái)保障不會(huì)發(fā)生鎖泄露

其優(yōu)點(diǎn)主要有以下幾點(diǎn):

  1. 相對(duì)內(nèi)部鎖在使用上更具靈活性披蕉,可以跨方法來(lái)完成鎖的申請(qǐng)和釋放操作
  2. 功能相對(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() 支持線程中斷
  3. 同時(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):

  1. 上下文切換與線程調(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ì)列的開銷
  2. 內(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)的開銷
  3. 限制可伸縮性梧乘。采用鎖的目的是使得多個(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)題:

  1. 過(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() 方法暫停
  2. 多次的線程上下文切換。對(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)榱愣羝Ia(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() {

        }

    }

}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侦高,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子厌杜,更是在濱河造成了極大的恐慌奉呛,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夯尽,死亡現(xiàn)場(chǎng)離奇詭異瞧壮,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)匙握,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門咆槽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人圈纺,你說(shuō)我怎么就攤上這事秦忿÷笊洌” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵灯谣,是天一觀的道長(zhǎng)潜秋。 經(jīng)常有香客問(wèn)我,道長(zhǎng)胎许,這世上最難降的妖魔是什么峻呛? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮呐萨,結(jié)果婚禮上杀饵,老公的妹妹穿的比我還像新娘。我一直安慰自己谬擦,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布朽缎。 她就那樣靜靜地躺著惨远,像睡著了一般。 火紅的嫁衣襯著肌膚如雪话肖。 梳的紋絲不亂的頭發(fā)上北秽,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音最筒,去河邊找鬼贺氓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛床蜘,可吹牛的內(nèi)容都是我干的辙培。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼邢锯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扬蕊!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起丹擎,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤尾抑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后蒂培,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體再愈,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年护戳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了翎冲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灸异,死狀恐怖府适,靈堂內(nèi)的尸體忽然破棺而出羔飞,到底是詐尸還是另有隱情,我是刑警寧澤檐春,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布逻淌,位于F島的核電站,受9級(jí)特大地震影響疟暖,放射性物質(zhì)發(fā)生泄漏卡儒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一俐巴、第九天 我趴在偏房一處隱蔽的房頂上張望骨望。 院中可真熱鬧,春花似錦欣舵、人聲如沸擎鸠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)劣光。三九已至,卻和暖如春糟把,著一層夾襖步出監(jiān)牢的瞬間绢涡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工遣疯, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雄可,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓缠犀,卻偏偏與公主長(zhǎng)得像数苫,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夭坪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 一文判、線程同步的概念 在多線程環(huán)境下,一些敏感數(shù)據(jù)不允許被多個(gè)線程同時(shí)訪問(wèn)室梅,為保證數(shù)據(jù)的完整性戏仓,需要一種技術(shù)來(lái)保證敏...
    愚工J閱讀 289評(píng)論 0 0
  • Java多線程的同步機(jī)制 其實(shí)就是Java實(shí)現(xiàn)同步的方法吧 線程同步主要用于協(xié)調(diào)對(duì)臨界資源的訪問(wèn),臨界資源可以是硬...
    逗逼程序員閱讀 1,758評(píng)論 0 0
  • Java內(nèi)存模型基礎(chǔ)知識(shí)重排序與happens-beforevolatilesynchronized與鎖CAS與原...
    碼代碼的小矮子閱讀 234評(píng)論 0 0
  • 在多線程一:GCD[http://www.reibang.com/p/4edf7c930095]中我們?cè)敿?xì)了解了...
    小心韓國(guó)人閱讀 874評(píng)論 0 1
  • 久違的晴天亡鼠,家長(zhǎng)會(huì)赏殃。 家長(zhǎng)大會(huì)開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了间涵。班主任說(shuō)已經(jīng)安排了三個(gè)家長(zhǎng)分享經(jīng)驗(yàn)仁热。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,523評(píng)論 16 22