日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

【2022】多线程并发编程面试真题

發(fā)布時(shí)間:2023/12/19 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【2022】多线程并发编程面试真题 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

    • 4. 多線程
        • 4.1 創(chuàng)建線程有哪幾種方式?
        • 4.2 說(shuō)說(shuō)Thread類的常用方法
        • 4.3 run()和start()有什么區(qū)別?
        • 4.4 線程是否可以重復(fù)啟動(dòng),會(huì)有什么后果?
        • 4.5 介紹一下線程的生命周期
        • 4.6 如何實(shí)現(xiàn)線程同步?
        • 4.7 說(shuō)一說(shuō)Java多線程之間的通信方式
        • 4.8 說(shuō)一說(shuō)Java同步機(jī)制中的wait和notify
        • 4.9 說(shuō)一說(shuō)sleep()和wait()的區(qū)別
        • 4.10 說(shuō)一說(shuō)notify()、notifyAll()的區(qū)別
        • 4.11 如何實(shí)現(xiàn)子線程先執(zhí)行,主線程再執(zhí)行?
        • 4.12 阻塞線程的方式有哪些?
        • 4.13 說(shuō)一說(shuō)synchronized與Lock的區(qū)別
        • 4.14 說(shuō)一說(shuō)synchronized的底層實(shí)現(xiàn)原理
        • 4.15 synchronized可以修飾靜態(tài)方法和靜態(tài)代碼塊嗎?
        • 4.16 談?wù)凴eentrantLock的實(shí)現(xiàn)原理
        • 4.17 如果不使用synchronized和Lock,如何保證線程安全?
        • 4.18 說(shuō)一說(shuō)Java中樂(lè)觀鎖和悲觀鎖的區(qū)別
        • 4.19 公平鎖與非公平鎖是怎么實(shí)現(xiàn)的?
        • 4.20 了解Java中的鎖升級(jí)嗎?
        • 4.21 如何實(shí)現(xiàn)互斥鎖(mutex)?
        • 4.22 分段鎖是怎么實(shí)現(xiàn)的?
        • 4.23 說(shuō)說(shuō)你對(duì)讀寫鎖的了解
        • 4.24 volatile關(guān)鍵字有什么用?
        • 4.25 談?wù)剉olatile的實(shí)現(xiàn)原理
        • 4.26 說(shuō)說(shuō)你對(duì)JUC的了解
        • 4.27 說(shuō)說(shuō)你對(duì)AQS的理解
        • 4.28 LongAdder解決了什么問(wèn)題,它是如何實(shí)現(xiàn)的?
        • 4.29 介紹下ThreadLocal和它的應(yīng)用場(chǎng)景
        • 4.30 請(qǐng)介紹ThreadLocal的實(shí)現(xiàn)原理,它是怎么處理hash沖突的?
        • 4.31 介紹一下線程池
        • 4.32 介紹一下線程池的工作流程
        • 4.33 線程池都有哪些狀態(tài)?
        • 4.34 談?wù)劸€程池的拒絕策略
        • 4.35 線程池的隊(duì)列大小你通常怎么設(shè)置?
        • 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?
        • 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?

4. 多線程

4.1 創(chuàng)建線程有哪幾種方式?

參考答案

創(chuàng)建線程有三種方式,分別是繼承Thread類、實(shí)現(xiàn)Runnable接口、實(shí)現(xiàn)Callable接口。

通過(guò)繼承Thread類來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:

  • 定義Thread類的子類,并重寫該類的run()方法,該run()方法將作為線程執(zhí)行體。
  • 創(chuàng)建Thread子類的實(shí)例,即創(chuàng)建了線程對(duì)象。
  • 調(diào)用線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
  • 通過(guò)實(shí)現(xiàn)Runnable接口來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:

  • 定義Runnable接口的實(shí)現(xiàn)類,并實(shí)現(xiàn)該接口的run()方法,該run()方法將作為線程執(zhí)行體。
  • 創(chuàng)建Runnable實(shí)現(xiàn)類的實(shí)例,并將其作為Thread的target來(lái)創(chuàng)建Thread對(duì)象,Thread對(duì)象為線程對(duì)象。
  • 調(diào)用線程對(duì)象的start()方法來(lái)啟動(dòng)該線程。
  • 通過(guò)實(shí)現(xiàn)Callable接口來(lái)創(chuàng)建并啟動(dòng)線程的步驟如下:

  • 創(chuàng)建Callable接口的實(shí)現(xiàn)類,并實(shí)現(xiàn)call()方法,該call()方法將作為線程執(zhí)行體,且該call()方法有返回值。然后再創(chuàng)建Callable實(shí)現(xiàn)類的實(shí)例。
  • 使用FutureTask類來(lái)包裝Callable對(duì)象,該FutureTask對(duì)象封裝了該Callable對(duì)象的call()方法的返回值。
  • 使用FutureTask對(duì)象作為Thread對(duì)象的target創(chuàng)建并啟動(dòng)新線程。
  • 調(diào)用FutureTask對(duì)象的get()方法來(lái)獲得子線程執(zhí)行結(jié)束后的返回值。
  • 擴(kuò)展閱讀

    通過(guò)繼承Thread類、實(shí)現(xiàn)Runnable接口、實(shí)現(xiàn)Callable接口都可以實(shí)現(xiàn)多線程,不過(guò)實(shí)現(xiàn)Runnable接口與實(shí)現(xiàn)Callable接口的方式基本相同,只是Callable接口里定義的方法有返回值,可以聲明拋出異常而已。因此可以將實(shí)現(xiàn)Runnable接口和實(shí)現(xiàn)Callable接口歸為一種方式。

    采用實(shí)現(xiàn)Runnable、Callable接口的方式創(chuàng)建多線程的優(yōu)缺點(diǎn):

    • 線程類只是實(shí)現(xiàn)了Runnable接口或Callable接口,還可以繼承其他類。
    • 在這種方式下,多個(gè)線程可以共享同一個(gè)target對(duì)象,所以非常適合多個(gè)相同線程來(lái)處理同一份資源的情況,從而可以將CPU、代碼和數(shù)據(jù)分開,形成清晰的模型,較好地體現(xiàn)了面向?qū)ο蟮乃枷搿?/li>
    • 劣勢(shì)是,編程稍稍復(fù)雜,如果需要訪問(wèn)當(dāng)前線程,則必須使用Thread.currentThread()方法。

    采用繼承Thread類的方式創(chuàng)建多線程的優(yōu)缺點(diǎn):

    • 劣勢(shì)是,因?yàn)榫€程類已經(jīng)繼承了Thread類,所以不能再繼承其他父類。
    • 優(yōu)勢(shì)是,編寫簡(jiǎn)單,如果需要訪問(wèn)當(dāng)前線程,則無(wú)須使用Thread.currentThread()方法,直接使用this即可獲得當(dāng)前線程。

    鑒于上面分析,因此一般推薦采用實(shí)現(xiàn)Runnable接口、Callable接口的方式來(lái)創(chuàng)建多線程。

    4.2 說(shuō)說(shuō)Thread類的常用方法

    參考答案

    Thread類常用構(gòu)造方法:

    • Thread()
    • Thread(String name)
    • Thread(Runnable target)
    • Thread(Runnable target, String name)

    其中,參數(shù) name為線程名,參數(shù) target為包含線程體的目標(biāo)對(duì)象。

    Thread類常用靜態(tài)方法:

    • currentThread():返回當(dāng)前正在執(zhí)行的線程;
    • interrupted():返回當(dāng)前執(zhí)行的線程是否已經(jīng)被中斷;
    • sleep(long millis):使當(dāng)前執(zhí)行的線程睡眠多少毫秒數(shù);
    • yield():使當(dāng)前執(zhí)行的線程自愿暫時(shí)放棄對(duì)處理器的使用權(quán)并允許其他線程執(zhí)行;

    Thread類常用實(shí)例方法:

    • getId():返回該線程的id;
    • getName():返回該線程的名字;
    • getPriority():返回該線程的優(yōu)先級(jí);
    • interrupt():使該線程中斷;
    • isInterrupted():返回該線程是否被中斷;
    • isAlive():返回該線程是否處于活動(dòng)狀態(tài);
    • isDaemon():返回該線程是否是守護(hù)線程;
    • setDaemon(boolean on):將該線程標(biāo)記為守護(hù)線程或用戶線程,如果不標(biāo)記默認(rèn)是非守護(hù)線程;
    • setName(String name):設(shè)置該線程的名字;
    • setPriority(int newPriority):改變?cè)摼€程的優(yōu)先級(jí);
    • join():等待該線程終止;
    • join(long millis):等待該線程終止,至多等待多少毫秒數(shù)。

    4.3 run()和start()有什么區(qū)別?

    參考答案

    run()方法被稱為線程執(zhí)行體,它的方法體代表了線程需要完成的任務(wù),而start()方法用來(lái)啟動(dòng)線程。

    調(diào)用start()方法啟動(dòng)線程時(shí),系統(tǒng)會(huì)把該run()方法當(dāng)成線程執(zhí)行體來(lái)處理。但如果直接調(diào)用線程對(duì)象的run()方法,則run()方法立即就會(huì)被執(zhí)行,而且在run()方法返回之前其他線程無(wú)法并發(fā)執(zhí)行。也就是說(shuō),如果直接調(diào)用線程對(duì)象的run()方法,系統(tǒng)把線程對(duì)象當(dāng)成一個(gè)普通對(duì)象,而run()方法也是一個(gè)普通方法,而不是線程執(zhí)行體。

    4.4 線程是否可以重復(fù)啟動(dòng),會(huì)有什么后果?

    參考答案

    只能對(duì)處于新建狀態(tài)的線程調(diào)用start()方法,否則將引發(fā)IllegalThreadStateException異常。

    擴(kuò)展閱讀

    當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個(gè)線程之后,該線程就處于新建狀態(tài),此時(shí)它和其他的Java對(duì)象一樣,僅僅由Java虛擬機(jī)為其分配內(nèi)存,并初始化其成員變量的值。此時(shí)的線程對(duì)象沒(méi)有表現(xiàn)出任何線程的動(dòng)態(tài)特征,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體。

    當(dāng)線程對(duì)象調(diào)用了start()方法之后,該線程處于就緒狀態(tài),Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器,處于這個(gè)狀態(tài)中的線程并沒(méi)有開始運(yùn)行,只是表示該線程可以運(yùn)行了。至于該線程何時(shí)開始運(yùn)行,取決于JVM里線程調(diào)度器的調(diào)度。

    4.5 介紹一下線程的生命周期

    參考答案

    在線程的生命周期中,它要經(jīng)過(guò)新建(New)、就緒(Ready)、運(yùn)行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態(tài)。尤其是當(dāng)線程啟動(dòng)以后,它不可能一直“霸占”著CPU獨(dú)自運(yùn)行,所以CPU需要在多條線程之間切換,于是線程狀態(tài)也會(huì)多次在運(yùn)行、就緒之間切換。

    當(dāng)程序使用new關(guān)鍵字創(chuàng)建了一個(gè)線程之后,該線程就處于新建狀態(tài),此時(shí)它和其他的Java對(duì)象一樣,僅僅由Java虛擬機(jī)為其分配內(nèi)存,并初始化其成員變量的值。此時(shí)的線程對(duì)象沒(méi)有表現(xiàn)出任何線程的動(dòng)態(tài)特征,程序也不會(huì)執(zhí)行線程的線程執(zhí)行體。

    當(dāng)線程對(duì)象調(diào)用了start()方法之后,該線程處于就緒狀態(tài),Java虛擬機(jī)會(huì)為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器,處于這個(gè)狀態(tài)中的線程并沒(méi)有開始運(yùn)行,只是表示該線程可以運(yùn)行了。至于該線程何時(shí)開始運(yùn)行,取決于JVM里線程調(diào)度器的調(diào)度。

    如果處于就緒狀態(tài)的線程獲得了CPU,開始執(zhí)行run()方法的線程執(zhí)行體,則該線程處于運(yùn)行狀態(tài),如果計(jì)算機(jī)只有一個(gè)CPU,那么在任何時(shí)刻只有一個(gè)線程處于運(yùn)行狀態(tài)。當(dāng)然,在一個(gè)多處理器的機(jī)器上,將會(huì)有多個(gè)線程并行執(zhí)行;當(dāng)線程數(shù)大于處理器數(shù)時(shí),依然會(huì)存在多個(gè)線程在同一個(gè)CPU上輪換的現(xiàn)象。

    當(dāng)一個(gè)線程開始運(yùn)行后,它不可能一直處于運(yùn)行狀態(tài),線程在運(yùn)行過(guò)程中需要被中斷,目的是使其他線程獲得執(zhí)行的機(jī)會(huì),線程調(diào)度的細(xì)節(jié)取決于底層平臺(tái)所采用的策略。對(duì)于采用搶占式策略的系統(tǒng)而言,系統(tǒng)會(huì)給每個(gè)可執(zhí)行的線程一個(gè)小時(shí)間段來(lái)處理任務(wù)。當(dāng)該時(shí)間段用完后,系統(tǒng)就會(huì)剝奪該線程所占用的資源,讓其他線程獲得執(zhí)行的機(jī)會(huì)。當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):

    • 線程調(diào)用sleep()方法主動(dòng)放棄所占用的處理器資源。
    • 線程調(diào)用了一個(gè)阻塞式IO方法,在該方法返回之前,該線程被阻塞。
    • 線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有。
    • 線程在等待某個(gè)通知(notify)。
    • 程序調(diào)用了線程的suspend()方法將該線程掛起。但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法。

    針對(duì)上面幾種情況,當(dāng)發(fā)生如下特定的情況時(shí)可以解除上面的阻塞,讓該線程重新進(jìn)入就緒狀態(tài):

    • 調(diào)用sleep()方法的線程經(jīng)過(guò)了指定時(shí)間。
    • 線程調(diào)用的阻塞式IO方法已經(jīng)返回。
    • 線程成功地獲得了試圖取得的同步監(jiān)視器。
    • 線程正在等待某個(gè)通知時(shí),其他線程發(fā)出了一個(gè)通知。
    • 處于掛起狀態(tài)的線程被調(diào)用了resume()恢復(fù)方法。

    線程會(huì)以如下三種方式結(jié)束,結(jié)束后就處于死亡狀態(tài):

    • run()或call()方法執(zhí)行完成,線程正常結(jié)束。
    • 線程拋出一個(gè)未捕獲的Exception或Error。
    • 直接調(diào)用該線程的stop()方法來(lái)結(jié)束該線程,該方法容易導(dǎo)致死鎖,通常不推薦使用。

    擴(kuò)展閱讀

    線程5種狀態(tài)的轉(zhuǎn)換關(guān)系,如下圖所示:

    4.6 如何實(shí)現(xiàn)線程同步?

    參考答案

  • 同步方法

    即有synchronized關(guān)鍵字修飾的方法,由于java的每個(gè)對(duì)象都有一個(gè)內(nèi)置鎖,當(dāng)用此關(guān)鍵字修飾方法時(shí), 內(nèi)置鎖會(huì)保護(hù)整個(gè)方法。在調(diào)用該方法前,需要獲得內(nèi)置鎖,否則就處于阻塞狀態(tài)。需要注意, synchronized關(guān)鍵字也可以修飾靜態(tài)方法,此時(shí)如果調(diào)用該靜態(tài)方法,將會(huì)鎖住整個(gè)類。

  • 同步代碼塊

    即有synchronized關(guān)鍵字修飾的語(yǔ)句塊,被該關(guān)鍵字修飾的語(yǔ)句塊會(huì)自動(dòng)被加上內(nèi)置鎖,從而實(shí)現(xiàn)同步。需值得注意的是,同步是一種高開銷的操作,因此應(yīng)該盡量減少同步的內(nèi)容。通常沒(méi)有必要同步整個(gè)方法,使用synchronized代碼塊同步關(guān)鍵代碼即可。

  • ReentrantLock

    Java 5新增了一個(gè)java.util.concurrent包來(lái)支持同步,其中ReentrantLock類是可重入、互斥、實(shí)現(xiàn)了Lock接口的鎖,它與使用synchronized方法和快具有相同的基本行為和語(yǔ)義,并且擴(kuò)展了其能力。需要注意的是,ReentrantLock還有一個(gè)可以創(chuàng)建公平鎖的構(gòu)造方法,但由于能大幅度降低程序運(yùn)行效率,因此不推薦使用。

  • volatile

    volatile關(guān)鍵字為域變量的訪問(wèn)提供了一種免鎖機(jī)制,使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能會(huì)被其他線程更新,因此每次使用該域就要重新計(jì)算,而不是使用寄存器中的值。需要注意的是,volatile不會(huì)提供任何原子操作,它也不能用來(lái)修飾final類型的變量。

  • 原子變量

    在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡(jiǎn)化線程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在應(yīng)用程序中(如以原子方式增加的計(jì)數(shù)器),但不能用于替換Integer。可擴(kuò)展Number,允許那些處理機(jī)遇數(shù)字類的工具和實(shí)用工具進(jìn)行統(tǒng)一訪問(wèn)。

  • 4.7 說(shuō)一說(shuō)Java多線程之間的通信方式

    參考答案

    在Java中線程通信主要有以下三種方式:

  • wait()、notify()、notifyAll()

    如果線程之間采用synchronized來(lái)保證線程安全,則可以利用wait()、notify()、notifyAll()來(lái)實(shí)現(xiàn)線程通信。這三個(gè)方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個(gè)對(duì)象都擁有鎖,所以讓當(dāng)前線程等待某個(gè)對(duì)象的鎖,當(dāng)然應(yīng)該通過(guò)這個(gè)對(duì)象來(lái)操作。并且因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖,如果通過(guò)線程來(lái)操作,就非常復(fù)雜了。另外,這三個(gè)方法都是本地方法,并且被final修飾,無(wú)法被重寫。

    wait()方法可以讓當(dāng)前線程釋放對(duì)象鎖并進(jìn)入阻塞狀態(tài)。notify()方法用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。notifyAll()用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。

    每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列。就緒隊(duì)列存儲(chǔ)了已就緒(將要競(jìng)爭(zhēng)鎖)的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程。當(dāng)一個(gè)阻塞線程被喚醒后,才會(huì)進(jìn)入就緒隊(duì)列,進(jìn)而等待CPU的調(diào)度。反之,當(dāng)一個(gè)線程被wait后,就會(huì)進(jìn)入阻塞隊(duì)列,等待被喚醒。

  • await()、signal()、signalAll()

    如果線程之間采用Lock來(lái)保證線程安全,則可以利用await()、signal()、signalAll()來(lái)實(shí)現(xiàn)線程通信。這三個(gè)方法都是Condition接口中的方法,該接口是在Java 1.5中出現(xiàn)的,它用來(lái)替代傳統(tǒng)的wait+notify實(shí)現(xiàn)線程間的協(xié)作,它的使用依賴于 Lock。相比使用wait+notify,使用Condition的await+signal這種方式能夠更加安全和高效地實(shí)現(xiàn)線程間協(xié)作。

    Condition依賴于Lock接口,生成一個(gè)Condition的基本代碼是lock.newCondition() 。 必須要注意的是,Condition 的 await()/signal()/signalAll() 使用都必須在lock保護(hù)之內(nèi),也就是說(shuō),必須在lock.lock()和lock.unlock之間才可以使用。事實(shí)上,await()/signal()/signalAll() 與 wait()/notify()/notifyAll()有著天然的對(duì)應(yīng)關(guān)系。即:Conditon中的await()對(duì)應(yīng)Object的wait(),Condition中的signal()對(duì)應(yīng)Object的notify(),Condition中的signalAll()對(duì)應(yīng)Object的notifyAll()。

  • BlockingQueue

    Java 5提供了一個(gè)BlockingQueue接口,雖然BlockingQueue也是Queue的子接口,但它的主要用途并不是作為容器,而是作為線程通信的工具。BlockingQueue具有一個(gè)特征:當(dāng)生產(chǎn)者線程試圖向BlockingQueue中放入元素時(shí),如果該隊(duì)列已滿,則該線程被阻塞;當(dāng)消費(fèi)者線程試圖從BlockingQueue中取出元素時(shí),如果該隊(duì)列已空,則該線程被阻塞。

    程序的兩個(gè)線程通過(guò)交替向BlockingQueue中放入元素、取出元素,即可很好地控制線程的通信。線程之間需要通信,最經(jīng)典的場(chǎng)景就是生產(chǎn)者與消費(fèi)者模型,而BlockingQueue就是針對(duì)該模型提供的解決方案。

  • 4.8 說(shuō)一說(shuō)Java同步機(jī)制中的wait和notify

    參考答案

    wait()、notify()、notifyAll()用來(lái)實(shí)現(xiàn)線程之間的通信,這三個(gè)方法都不是Thread類中所聲明的方法,而是Object類中聲明的方法。原因是每個(gè)對(duì)象都擁有鎖,所以讓當(dāng)前線程等待某個(gè)對(duì)象的鎖,當(dāng)然應(yīng)該通過(guò)這個(gè)對(duì)象來(lái)操作。并且因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖,如果通過(guò)線程來(lái)操作,就非常復(fù)雜了。另外,這三個(gè)方法都是本地方法,并且被final修飾,無(wú)法被重寫,并且只有采用synchronized實(shí)現(xiàn)線程同步時(shí)才能使用這三個(gè)方法。

    wait()方法可以讓當(dāng)前線程釋放對(duì)象鎖并進(jìn)入阻塞狀態(tài)。notify()方法用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。notifyAll()方法用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。

    每個(gè)鎖對(duì)象都有兩個(gè)隊(duì)列,一個(gè)是就緒隊(duì)列,一個(gè)是阻塞隊(duì)列。就緒隊(duì)列存儲(chǔ)了已就緒(將要競(jìng)爭(zhēng)鎖)的線程,阻塞隊(duì)列存儲(chǔ)了被阻塞的線程。當(dāng)一個(gè)阻塞線程被喚醒后,才會(huì)進(jìn)入就緒隊(duì)列,進(jìn)而等待CPU的調(diào)度。反之,當(dāng)一個(gè)線程被wait后,就會(huì)進(jìn)入阻塞隊(duì)列,等待被喚醒。

    4.9 說(shuō)一說(shuō)sleep()和wait()的區(qū)別

    參考答案

  • sleep()是Thread類中的靜態(tài)方法,而wait()是Object類中的成員方法;
  • sleep()可以在任何地方使用,而wait()只能在同步方法或同步代碼塊中使用;
  • sleep()不會(huì)釋放鎖,而wait()會(huì)釋放鎖,并需要通過(guò)notify()/notifyAll()重新獲取鎖。
  • 4.10 說(shuō)一說(shuō)notify()、notifyAll()的區(qū)別

    參考答案

    • notify()

      用于喚醒一個(gè)正在等待相應(yīng)對(duì)象鎖的線程,使其進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。

    • notifyAll()

      用于喚醒所有正在等待相應(yīng)對(duì)象鎖的線程,使它們進(jìn)入就緒隊(duì)列,以便在當(dāng)前線程釋放鎖后競(jìng)爭(zhēng)鎖,進(jìn)而得到CPU的執(zhí)行。

    4.11 如何實(shí)現(xiàn)子線程先執(zhí)行,主線程再執(zhí)行?

    參考答案

    啟動(dòng)子線程后,立即調(diào)用該線程的join()方法,則主線程必須等待子線程執(zhí)行完成后再執(zhí)行。

    擴(kuò)展閱讀

    Thread類提供了讓一個(gè)線程等待另一個(gè)線程完成的方法——join()方法。當(dāng)在某個(gè)程序執(zhí)行流中調(diào)用其他線程的join()方法時(shí),調(diào)用線程將被阻塞,直到被join()方法加入的join線程執(zhí)行完為止。

    join()方法通常由使用線程的程序調(diào)用,以將大問(wèn)題劃分成許多小問(wèn)題,每個(gè)小問(wèn)題分配一個(gè)線程。當(dāng)所有的小問(wèn)題都得到處理后,再調(diào)用主線程來(lái)進(jìn)一步操作。

    4.12 阻塞線程的方式有哪些?

    參考答案

    當(dāng)發(fā)生如下情況時(shí),線程將會(huì)進(jìn)入阻塞狀態(tài):

    • 線程調(diào)用sleep()方法主動(dòng)放棄所占用的處理器資源;
    • 線程調(diào)用了一個(gè)阻塞式IO方法,在該方法返回之前,該線程被阻塞;
    • 線程試圖獲得一個(gè)同步監(jiān)視器,但該同步監(jiān)視器正被其他線程所持有;
    • 線程在等待某個(gè)通知(notify);
    • 程序調(diào)用了線程的suspend()方法將該線程掛起,但這個(gè)方法容易導(dǎo)致死鎖,所以應(yīng)該盡量避免使用該方法。

    4.13 說(shuō)一說(shuō)synchronized與Lock的區(qū)別

    參考答案

  • synchronized是Java關(guān)鍵字,在JVM層面實(shí)現(xiàn)加鎖和解鎖;Lock是一個(gè)接口,在代碼層面實(shí)現(xiàn)加鎖和解鎖。
  • synchronized可以用在代碼塊上、方法上;Lock只能寫在代碼里。
  • synchronized在代碼執(zhí)行完或出現(xiàn)異常時(shí)自動(dòng)釋放鎖;Lock不會(huì)自動(dòng)釋放鎖,需要在finally中顯示釋放鎖。
  • synchronized會(huì)導(dǎo)致線程拿不到鎖一直等待;Lock可以設(shè)置獲取鎖失敗的超時(shí)時(shí)間。
  • synchronized無(wú)法得知是否獲取鎖成功;Lock則可以通過(guò)tryLock得知加鎖是否成功。
  • synchronized鎖可重入、不可中斷、非公平;Lock鎖可重入、可中斷、可公平/不公平,并可以細(xì)分讀寫鎖以提高效率。
  • 4.14 說(shuō)一說(shuō)synchronized的底層實(shí)現(xiàn)原理

    參考答案

    一、以下列代碼為例,說(shuō)明同步代碼塊的底層實(shí)現(xiàn)原理:

    public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("Method 1 start");}} }

    查看反編譯后結(jié)果,如下圖:

    可見,synchronized作用在代碼塊時(shí),它的底層是通過(guò)monitorenter、monitorexit指令來(lái)實(shí)現(xiàn)的。

    • monitorenter:

      每個(gè)對(duì)象都是一個(gè)監(jiān)視器鎖(monitor),當(dāng)monitor被占用時(shí)就會(huì)處于鎖定狀態(tài),線程執(zhí)行monitorenter指令時(shí)嘗試獲取monitor的所有權(quán),過(guò)程如下:

      如果monitor的進(jìn)入數(shù)為0,則該線程進(jìn)入monitor,然后將進(jìn)入數(shù)設(shè)置為1,該線程即為monitor的所有者。如果線程已經(jīng)占有該monitor,只是重新進(jìn)入,則進(jìn)入monitor的進(jìn)入數(shù)加1。如果其他線程已經(jīng)占用了monitor,則該線程進(jìn)入阻塞狀態(tài),直到monitor的進(jìn)入數(shù)為0,再重新嘗試獲取monitor的所有權(quán)。

    • monitorexit:

      執(zhí)行monitorexit的線程必須是objectref所對(duì)應(yīng)的monitor持有者。指令執(zhí)行時(shí),monitor的進(jìn)入數(shù)減1,如果減1后進(jìn)入數(shù)為0,那線程退出monitor,不再是這個(gè)monitor的所有者。其他被這個(gè)monitor阻塞的線程可以嘗試去獲取這個(gè)monitor的所有權(quán)。

      monitorexit指令出現(xiàn)了兩次,第1次為同步正常退出釋放鎖,第2次為發(fā)生異步退出釋放鎖。

    二、以下列代碼為例,說(shuō)明同步方法的底層實(shí)現(xiàn)原理:

    public class SynchronizedMethod {public synchronized void method() {System.out.println("Hello World!");} }

    查看反編譯后結(jié)果,如下圖:

    從反編譯的結(jié)果來(lái)看,方法的同步并沒(méi)有通過(guò) monitorenter 和 monitorexit 指令來(lái)完成,不過(guò)相對(duì)于普通方法,其常量池中多了 ACC_SYNCHRONIZED 標(biāo)示符。JVM就是根據(jù)該標(biāo)示符來(lái)實(shí)現(xiàn)方法的同步的:

    當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查方法的 ACC_SYNCHRONIZED 訪問(wèn)標(biāo)志是否被設(shè)置,如果設(shè)置了,執(zhí)行線程將先獲取monitor,獲取成功之后才能執(zhí)行方法體,方法執(zhí)行完后再釋放monitor。在方法執(zhí)行期間,其他任何線程都無(wú)法再獲得同一個(gè)monitor對(duì)象。

    三、總結(jié)

    兩種同步方式本質(zhì)上沒(méi)有區(qū)別,只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn),無(wú)需通過(guò)字節(jié)碼來(lái)完成。兩個(gè)指令的執(zhí)行是JVM通過(guò)調(diào)用操作系統(tǒng)的互斥原語(yǔ)mutex來(lái)實(shí)現(xiàn),被阻塞的線程會(huì)被掛起、等待重新調(diào)度,會(huì)導(dǎo)致“用戶態(tài)和內(nèi)核態(tài)”兩個(gè)態(tài)之間來(lái)回切換,對(duì)性能有較大影響。

    4.15 synchronized可以修飾靜態(tài)方法和靜態(tài)代碼塊嗎?

    參考答案

    synchronized可以修飾靜態(tài)方法,但不能修飾靜態(tài)代碼塊。

    當(dāng)修飾靜態(tài)方法時(shí),監(jiān)視器鎖(monitor)便是對(duì)象的Class實(shí)例,因?yàn)镃lass數(shù)據(jù)存在于永久代,因此靜態(tài)方法鎖相當(dāng)于該類的一個(gè)全局鎖。

    4.16 談?wù)凴eentrantLock的實(shí)現(xiàn)原理

    參考答案

    ReentrantLock是基于AQS實(shí)現(xiàn)的,AQS即AbstractQueuedSynchronizer的縮寫,這個(gè)是個(gè)內(nèi)部實(shí)現(xiàn)了兩個(gè)隊(duì)列的抽象類,分別是同步隊(duì)列和條件隊(duì)列。其中同步隊(duì)列是一個(gè)雙向鏈表,里面儲(chǔ)存的是處于等待狀態(tài)的線程,正在排隊(duì)等待喚醒去獲取鎖,而條件隊(duì)列是一個(gè)單向鏈表,里面儲(chǔ)存的也是處于等待狀態(tài)的線程,只不過(guò)這些線程喚醒的結(jié)果是加入到了同步隊(duì)列的隊(duì)尾,AQS所做的就是管理這兩個(gè)隊(duì)列里面線程之間的等待狀態(tài)-喚醒的工作。

    在同步隊(duì)列中,還存在2中模式,分別是獨(dú)占模式和共享模式,這兩種模式的區(qū)別就在于AQS在喚醒線程節(jié)點(diǎn)的時(shí)候是不是傳遞喚醒,這兩種模式分別對(duì)應(yīng)獨(dú)占鎖和共享鎖。

    AQS是一個(gè)抽象類,所以不能直接實(shí)例化,當(dāng)我們需要實(shí)現(xiàn)一個(gè)自定義鎖的時(shí)候可以去繼承AQS然后重寫獲取鎖的方式和釋放鎖的方式還有管理state,而ReentrantLock就是通過(guò)重寫了AQS的tryAcquire和tryRelease方法實(shí)現(xiàn)的lock和unlock。

    ReentrantLock 結(jié)構(gòu)如下圖所示:

    首先ReentrantLock 實(shí)現(xiàn)了 Lock 接口,然后有3個(gè)內(nèi)部類,其中Sync內(nèi)部類繼承自AQS,另外的兩個(gè)內(nèi)部類繼承自Sync,這兩個(gè)類分別是用來(lái)公平鎖和非公平鎖的。通過(guò)Sync重寫的方法tryAcquire、tryRelease可以知道,ReentrantLock實(shí)現(xiàn)的是AQS的獨(dú)占模式,也就是獨(dú)占鎖,這個(gè)鎖是悲觀鎖。

    4.17 如果不使用synchronized和Lock,如何保證線程安全?

    參考答案

  • volatile

    volatile關(guān)鍵字為域變量的訪問(wèn)提供了一種免鎖機(jī)制,使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能會(huì)被其他線程更新,因此每次使用該域就要重新計(jì)算,而不是使用寄存器中的值。需要注意的是,volatile不會(huì)提供任何原子操作,它也不能用來(lái)修飾final類型的變量。

  • 原子變量

    在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡(jiǎn)化線程同步。例如AtomicInteger 表可以用原子方式更新int的值,可用在應(yīng)用程序中(如以原子方式增加的計(jì)數(shù)器),但不能用于替換Integer。可擴(kuò)展Number,允許那些處理機(jī)遇數(shù)字類的工具和實(shí)用工具進(jìn)行統(tǒng)一訪問(wèn)。

  • 本地存儲(chǔ)

    可以通過(guò)ThreadLocal類來(lái)實(shí)現(xiàn)線程本地存儲(chǔ)的功能。每一個(gè)線程的Thread對(duì)象中都有一個(gè)ThreadLocalMap對(duì)象,這個(gè)對(duì)象存儲(chǔ)了一組以ThreadLocal.threadLocalHashCode為鍵,以本地線程變量為值的K-V值對(duì),ThreadLocal對(duì)象就是當(dāng)前線程的ThreadLocalMap的訪問(wèn)入口,每一個(gè)ThreadLocal對(duì)象都包含了一個(gè)獨(dú)一無(wú)二的threadLocalHashCode值,使用這個(gè)值就可以在線程K-V值對(duì)中找回對(duì)應(yīng)的本地線程變量。

  • 不可變的

    只要一個(gè)不可變的對(duì)象被正確地構(gòu)建出來(lái),那其外部的可見狀態(tài)永遠(yuǎn)都不會(huì)改變,永遠(yuǎn)都不會(huì)看到它在多個(gè)線程之中處于不一致的狀態(tài),“不可變”帶來(lái)的安全性是最直接、最純粹的。Java語(yǔ)言中,如果多線程共享的數(shù)據(jù)是一個(gè)基本數(shù)據(jù)類型,那么只要在定義時(shí)使用final關(guān)鍵字修飾它就可以保證它是不可變的。如果共享數(shù)據(jù)是一個(gè)對(duì)象,由于Java語(yǔ)言目前暫時(shí)還沒(méi)有提供值類型的支持,那就需要對(duì)象自行保證其行為不會(huì)對(duì)其狀態(tài)產(chǎn)生任何影響才行。String類是一個(gè)典型的不可變類,可以參考它設(shè)計(jì)一個(gè)不可變類。

  • 4.18 說(shuō)一說(shuō)Java中樂(lè)觀鎖和悲觀鎖的區(qū)別

    參考答案

    悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)阻塞直到它拿到鎖。Java中悲觀鎖是通過(guò)synchronized關(guān)鍵字或Lock接口來(lái)實(shí)現(xiàn)的。

    樂(lè)觀鎖:顧名思義,就是很樂(lè)觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒(méi)有去更新這個(gè)數(shù)據(jù)。樂(lè)觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。在JDK1.5 中新增 java.util.concurrent (J.U.C)就是建立在CAS之上的。相對(duì)于對(duì)于 synchronized 這種阻塞算法,CAS是非阻塞算法的一種常見實(shí)現(xiàn)。所以J.U.C在性能上有了很大的提升。

    4.19 公平鎖與非公平鎖是怎么實(shí)現(xiàn)的?

    參考答案

    在Java中實(shí)現(xiàn)鎖的方式有兩種,一種是使用Java自帶的關(guān)鍵字synchronized對(duì)相應(yīng)的類或者方法以及代碼塊進(jìn)行加鎖,另一種是ReentrantLock,前者只能是非公平鎖,而后者是默認(rèn)非公平但可實(shí)現(xiàn)公平的一把鎖。

    ReentrantLock是基于其內(nèi)部類FairSync(公平鎖)和NonFairSync(非公平鎖)實(shí)現(xiàn)的,并且它的實(shí)現(xiàn)依賴于Java同步器框架AbstractQueuedSynchronizer(AQS),AQS使用一個(gè)整形的volatile變量state來(lái)維護(hù)同步狀態(tài),這個(gè)volatile變量是實(shí)現(xiàn)ReentrantLock的關(guān)鍵。我們來(lái)看一下ReentrantLock的類圖:

    ReentrantLock 的公平鎖和非公平鎖都委托了 AbstractQueuedSynchronizer#acquire 去請(qǐng)求獲取。

    public` `final` `void` `acquire(``int` `arg) {`` ``if` `(!tryAcquire(arg) &&`` ``acquireQueued(addWaiter(Node.EXCLUSIVE), arg))`` ``selfInterrupt();``}
    • tryAcquire 是一個(gè)抽象方法,是公平與非公平的實(shí)現(xiàn)原理所在。
    • addWaiter 是將當(dāng)前線程結(jié)點(diǎn)加入等待隊(duì)列之中。公平鎖在鎖釋放后會(huì)嚴(yán)格按照等到隊(duì)列去取后續(xù)值,而非公平鎖在對(duì)于新晉線程有很大優(yōu)勢(shì)。
    • acquireQueued 在多次循環(huán)中嘗試獲取到鎖或者將當(dāng)前線程阻塞。
    • selfInterrupt 如果線程在阻塞期間發(fā)生了中斷,調(diào)用 Thread.currentThread().interrupt() 中斷當(dāng)前線程。

    公平鎖和非公平鎖在說(shuō)的獲取上都使用到了 volatile 關(guān)鍵字修飾的state字段, 這是保證多線程環(huán)境下鎖的獲取與否的核心。但是當(dāng)并發(fā)情況下多個(gè)線程都讀取到 state == 0時(shí),則必須用到CAS技術(shù),一門CPU的原子鎖技術(shù),可通過(guò)CPU對(duì)共享變量加鎖的形式,實(shí)現(xiàn)數(shù)據(jù)變更的原子操作。volatile 和 CAS的結(jié)合是并發(fā)搶占的關(guān)鍵。

    • 公平鎖FairSync

      公平鎖的實(shí)現(xiàn)機(jī)理在于每次有線程來(lái)?yè)屨兼i的時(shí)候,都會(huì)檢查一遍有沒(méi)有等待隊(duì)列,如果有, 當(dāng)前線程會(huì)執(zhí)行如下步驟:

      if (!hasQueuedPredecessors() && compareAndSetState(``0``, acquires)) { `` ``setExclusiveOwnerThread(current);`` ``return true``;``}

      其中hasQueuedPredecessors是用于檢查是否有等待隊(duì)列的:

      public final boolean hasQueuedPredecessors() {`` ``Node t = tail; ``// Read fields in reverse initialization order`` ``Node h = head;`` ``Node s;`` ``return h != t &&`` ``((s = h.next) == ``null || s.thread != Thread.currentThread());``}

    • 非公平鎖NonfairSync

      非公平鎖在實(shí)現(xiàn)的時(shí)候多次強(qiáng)調(diào)隨機(jī)搶占:

      if (c == ``0``) {`` ``if (compareAndSetState(``0``, acquires)) {`` ``setExclusiveOwnerThread(current);`` ``return true``;`` ``}``}

      與公平鎖的區(qū)別在于新晉獲取鎖的進(jìn)程會(huì)有多次機(jī)會(huì)去搶占鎖,被加入了等待隊(duì)列后則跟公平鎖沒(méi)有區(qū)別。

    4.20 了解Java中的鎖升級(jí)嗎?

    參考答案

    JDK 1.6之前,synchronized 還是一個(gè)重量級(jí)鎖,是一個(gè)效率比較低下的鎖。但是在JDK 1.6后,JVM為了提高鎖的獲取與釋放效率對(duì)synchronized 進(jìn)行了優(yōu)化,引入了偏向鎖和輕量級(jí)鎖 ,從此以后鎖的狀態(tài)就有了四種:無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。并且四種狀態(tài)會(huì)隨著競(jìng)爭(zhēng)的情況逐漸升級(jí),而且是不可逆的過(guò)程,即不可降級(jí),這四種鎖的級(jí)別由低到高依次是:無(wú)鎖、偏向鎖,輕量級(jí)鎖,重量級(jí)鎖。如下圖所示:

  • 無(wú)鎖

    無(wú)鎖是指沒(méi)有對(duì)資源進(jìn)行鎖定,所有的線程都能訪問(wèn)并修改同一個(gè)資源,但同時(shí)只有一個(gè)線程能修改成功。無(wú)鎖的特點(diǎn)是修改操作會(huì)在循環(huán)內(nèi)進(jìn)行,線程會(huì)不斷的嘗試修改共享資源。如果沒(méi)有沖突就修改成功并退出,否則就會(huì)繼續(xù)循環(huán)嘗試。如果有多個(gè)線程修改同一個(gè)值,必定會(huì)有一個(gè)線程能修改成功,而其他修改失敗的線程會(huì)不斷重試直到修改成功。

  • 偏向鎖

    初次執(zhí)行到synchronized代碼塊的時(shí)候,鎖對(duì)象變成偏向鎖(通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位),字面意思是“偏向于第一個(gè)獲得它的線程”的鎖。執(zhí)行完同步代碼塊后,線程并不會(huì)主動(dòng)釋放偏向鎖。當(dāng)?shù)诙蔚竭_(dá)同步代碼塊時(shí),線程會(huì)判斷此時(shí)持有鎖的線程是否就是自己(持有鎖的線程ID也在對(duì)象頭里),如果是則正常往下執(zhí)行。由于之前沒(méi)有釋放鎖,這里也就不需要重新加鎖。如果自始至終使用鎖的線程只有一個(gè),很明顯偏向鎖幾乎沒(méi)有額外開銷,性能極高。

    偏向鎖是指當(dāng)一段同步代碼一直被同一個(gè)線程所訪問(wèn)時(shí),即不存在多個(gè)線程的競(jìng)爭(zhēng)時(shí),那么該線程在后續(xù)訪問(wèn)時(shí)便會(huì)自動(dòng)獲得鎖,從而降低獲取鎖帶來(lái)的消耗,即提高性能。

    當(dāng)一個(gè)線程訪問(wèn)同步代碼塊并獲取鎖時(shí),會(huì)在 Mark Word 里存儲(chǔ)鎖偏向的線程 ID。在線程進(jìn)入和退出同步塊時(shí)不再通過(guò) CAS 操作來(lái)加鎖和解鎖,而是檢測(cè) Mark Word 里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖。輕量級(jí)鎖的獲取及釋放依賴多次 CAS 原子指令,而偏向鎖只需要在置換 ThreadID 的時(shí)候依賴一次 CAS 原子指令即可。

    偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖,線程是不會(huì)主動(dòng)釋放偏向鎖的。關(guān)于偏向鎖的撤銷,需要等待全局安全點(diǎn),即在某個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行時(shí),它會(huì)先暫停擁有偏向鎖的線程,然后判斷鎖對(duì)象是否處于被鎖定狀態(tài)。如果線程不處于活動(dòng)狀態(tài),則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài),并撤銷偏向鎖,恢復(fù)到無(wú)鎖(標(biāo)志位為01)或輕量級(jí)鎖(標(biāo)志位為00)的狀態(tài)。

  • 輕量級(jí)鎖

    輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,卻被另外的線程所訪問(wèn),此時(shí)偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,線程不會(huì)阻塞,從而提高性能。

    輕量級(jí)鎖的獲取主要由兩種情況:

  • 當(dāng)關(guān)閉偏向鎖功能時(shí);
  • 由于多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖。
  • 一旦有第二個(gè)線程加入鎖競(jìng)爭(zhēng),偏向鎖就升級(jí)為輕量級(jí)鎖(自旋鎖)。這里要明確一下什么是鎖競(jìng)爭(zhēng):如果多個(gè)線程輪流獲取一個(gè)鎖,但是每次獲取鎖的時(shí)候都很順利,沒(méi)有發(fā)生阻塞,那么就不存在鎖競(jìng)爭(zhēng)。只有當(dāng)某線程嘗試獲取鎖的時(shí)候,發(fā)現(xiàn)該鎖已經(jīng)被占用,只能等待其釋放,這才發(fā)生了鎖競(jìng)爭(zhēng)。

    在輕量級(jí)鎖狀態(tài)下繼續(xù)鎖競(jìng)爭(zhēng),沒(méi)有搶到鎖的線程將自旋,即不停地循環(huán)判斷鎖是否能夠被成功獲取。獲取鎖的操作,其實(shí)就是通過(guò)CAS修改對(duì)象頭里的鎖標(biāo)志位。先比較當(dāng)前鎖標(biāo)志位是否為“釋放”,如果是則將其設(shè)置為“鎖定”,比較并設(shè)置是原子性發(fā)生的。這就算搶到鎖了,然后線程將當(dāng)前鎖的持有者信息修改為自己。

    長(zhǎng)時(shí)間的自旋操作是非常消耗資源的,一個(gè)線程持有鎖,其他線程就只能在原地空耗CPU,執(zhí)行不了任何有效的任務(wù),這種現(xiàn)象叫做忙等(busy-waiting)。如果多個(gè)線程用一個(gè)鎖,但是沒(méi)有發(fā)生鎖競(jìng)爭(zhēng),或者發(fā)生了很輕微的鎖競(jìng)爭(zhēng),那么synchronized就用輕量級(jí)鎖,允許短時(shí)間的忙等現(xiàn)象。這是一種折衷的想法,短時(shí)間的忙等,換取線程在用戶態(tài)和內(nèi)核態(tài)之間切換的開銷。

  • 重量級(jí)鎖

    重量級(jí)鎖顯然,此忙等是有限度的(有個(gè)計(jì)數(shù)器記錄自旋次數(shù),默認(rèn)允許循環(huán)10次,可以通過(guò)虛擬機(jī)參數(shù)更改)。如果鎖競(jìng)爭(zhēng)情況嚴(yán)重,某個(gè)達(dá)到最大自旋次數(shù)的線程,會(huì)將輕量級(jí)鎖升級(jí)為重量級(jí)鎖(依然是CAS修改鎖標(biāo)志位,但不修改持有鎖的線程ID)。當(dāng)后續(xù)線程嘗試獲取鎖時(shí),發(fā)現(xiàn)被占用的鎖是重量級(jí)鎖,則直接將自己掛起(而不是忙等),等待將來(lái)被喚醒。

    重量級(jí)鎖是指當(dāng)有一個(gè)線程獲取鎖之后,其余所有等待獲取該鎖的線程都會(huì)處于阻塞狀態(tài)。簡(jiǎn)言之,就是所有的控制權(quán)都交給了操作系統(tǒng),由操作系統(tǒng)來(lái)負(fù)責(zé)線程間的調(diào)度和線程的狀態(tài)變更。而這樣會(huì)出現(xiàn)頻繁地對(duì)線程運(yùn)行狀態(tài)的切換,線程的掛起和喚醒,從而消耗大量的系統(tǒng)資。

  • 擴(kuò)展閱讀

    synchronized 用的鎖是存在Java對(duì)象頭里的,那么什么是對(duì)象頭呢?我們以 Hotspot 虛擬機(jī)為例進(jìn)行說(shuō)明,Hopspot 對(duì)象頭主要包括兩部分?jǐn)?shù)據(jù):Mark Word(標(biāo)記字段) 和 Klass Pointer(類型指針)。

    • Mark Word:默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)志位信息。這些信息都是與對(duì)象自身定義無(wú)關(guān)的數(shù)據(jù),所以Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存存儲(chǔ)盡量多的數(shù)據(jù)。它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間,也就是在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化。
    • Klass Point:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。

    那么,synchronized 具體是存在對(duì)象頭哪里呢?答案是:存在鎖對(duì)象的對(duì)象頭的Mark Word中,那么MarkWord在對(duì)象頭中到底長(zhǎng)什么樣,它到底存儲(chǔ)了什么呢?

    在64位的虛擬機(jī)中:

    在32位的虛擬機(jī)中:

    下面我們以 32位虛擬機(jī)為例,來(lái)看一下其 Mark Word 的字節(jié)具體是如何分配的:

    • 無(wú)鎖 :對(duì)象頭開辟 25bit 的空間用來(lái)存儲(chǔ)對(duì)象的 hashcode ,4bit 用于存放對(duì)象分代年齡,1bit 用來(lái)存放是否偏向鎖的標(biāo)識(shí)位,2bit 用來(lái)存放鎖標(biāo)識(shí)位為01。
    • 偏向鎖: 在偏向鎖中劃分更細(xì),還是開辟 25bit 的空間,其中23bit 用來(lái)存放線程ID,2bit 用來(lái)存放 Epoch,4bit 存放對(duì)象分代年齡,1bit 存放是否偏向鎖標(biāo)識(shí), 0表示無(wú)鎖,1表示偏向鎖,鎖的標(biāo)識(shí)位還是01。
    • 輕量級(jí)鎖:在輕量級(jí)鎖中直接開辟 30bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標(biāo)志位,其標(biāo)志位為00。
    • 重量級(jí)鎖: 在重量級(jí)鎖中和輕量級(jí)鎖一樣,30bit 的空間用來(lái)存放指向重量級(jí)鎖的指針,2bit 存放鎖的標(biāo)識(shí)位,為11。
    • GC標(biāo)記: 開辟30bit 的內(nèi)存空間卻沒(méi)有占用,2bit 空間存放鎖標(biāo)志位為11。

    其中無(wú)鎖和偏向鎖的鎖標(biāo)志位都是01,只是在前面的1bit區(qū)分了這是無(wú)鎖狀態(tài)還是偏向鎖狀態(tài)。關(guān)于內(nèi)存的分配,我們可以在git中openJDK中 markOop.hpp 可以看出:

    public:// Constantsenum { age_bits = 4,lock_bits = 2,biased_lock_bits = 1,max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,cms_bits = LP64_ONLY(1) NOT_LP64(0),epoch_bits = 2};
    • age_bits: 就是我們說(shuō)的分代回收的標(biāo)識(shí),占用4字節(jié)。
    • lock_bits: 是鎖的標(biāo)志位,占用2個(gè)字節(jié)。
    • biased_lock_bits: 是是否偏向鎖的標(biāo)識(shí),占用1個(gè)字節(jié)。
    • max_hash_bits: 是針對(duì)無(wú)鎖計(jì)算的hashcode 占用字節(jié)數(shù)量,如果是32位虛擬機(jī),就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虛擬機(jī),64 - 4 - 2 - 1 = 57 byte,但是會(huì)有 25 字節(jié)未使用,所以64位的 hashcode 占用 31 byte。
    • hash_bits: 是針對(duì) 64 位虛擬機(jī)來(lái)說(shuō),如果最大字節(jié)數(shù)大于 31,則取31,否則取真實(shí)的字節(jié)數(shù)。
    • cms_bits: 不是64位虛擬機(jī)就占用 0 byte,是64位就占用 1byte。
    • epoch_bits: 就是 epoch 所占用的字節(jié)大小,2字節(jié)。

    4.21 如何實(shí)現(xiàn)互斥鎖(mutex)?

    參考答案

    在Java里面,最基本的互斥同步手段就是synchronized關(guān)鍵字,這是一種塊結(jié)構(gòu)(Block Structured)的同步語(yǔ)法。synchronized關(guān)鍵字經(jīng)過(guò)Javac編譯之后,會(huì)在同步塊的前后分別形成monitorenter和monitorexit這兩個(gè)字節(jié)碼指令。這兩個(gè)字節(jié)碼指令都需要一個(gè)reference類型的參數(shù)來(lái)指明要鎖定和解鎖的對(duì)象。如果Java源碼中的synchronized明確指定了對(duì)象參數(shù),那就以這個(gè)對(duì)象的引用作為reference。如果沒(méi)有明確指定,那將根據(jù)synchronized修飾的方法類型(如實(shí)例方法或類方法),來(lái)決定是取代碼所在的對(duì)象實(shí)例還是取類型對(duì)應(yīng)的Class對(duì)象來(lái)作為線程要持有的鎖。

    自JDK 5起,Java類庫(kù)中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一種全新的互斥同步手段。基于Lock接口,用戶能夠以非塊結(jié)構(gòu)(Non-Block Structured)來(lái)實(shí)現(xiàn)互斥同步,從而擺脫了語(yǔ)言特性的束縛,改為在類庫(kù)層面去實(shí)現(xiàn)同步,這也為日后擴(kuò)展出不同調(diào)度算法、不同特征、不同性能、不同語(yǔ)義的各種鎖提供了廣闊的空間。

    4.22 分段鎖是怎么實(shí)現(xiàn)的?

    參考答案

    在并發(fā)程序中,串行操作是會(huì)降低可伸縮性,并且上下文切換也會(huì)減低性能。在鎖上發(fā)生競(jìng)爭(zhēng)時(shí)將通水導(dǎo)致這兩種問(wèn)題,使用獨(dú)占鎖時(shí)保護(hù)受限資源的時(shí)候,基本上是采用串行方式—-每次只能有一個(gè)線程能訪問(wèn)它。所以對(duì)于可伸縮性來(lái)說(shuō)最大的威脅就是獨(dú)占鎖。

    我們一般有三種方式降低鎖的競(jìng)爭(zhēng)程度:

  • 減少鎖的持有時(shí)間;
  • 降低鎖的請(qǐng)求頻率;
  • 使用帶有協(xié)調(diào)機(jī)制的獨(dú)占鎖,這些機(jī)制允許更高的并發(fā)性。
  • 在某些情況下我們可以將鎖分解技術(shù)進(jìn)一步擴(kuò)展為一組獨(dú)立對(duì)象上的鎖進(jìn)行分解,這稱為分段鎖。其實(shí)說(shuō)的簡(jiǎn)單一點(diǎn)就是:容器里有多把鎖,每一把鎖用于鎖容器其中一部分?jǐn)?shù)據(jù),那么當(dāng)多線程訪問(wèn)容器里不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而可以有效的提高并發(fā)訪問(wèn)效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù),首先將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn)。

    如下圖,ConcurrentHashMap使用Segment數(shù)據(jù)結(jié)構(gòu),將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問(wèn)其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問(wèn),能夠?qū)崿F(xiàn)真正的并發(fā)訪問(wèn)。所以說(shuō),ConcurrentHashMap在并發(fā)情況下,不僅保證了線程安全,而且提高了性能。

    4.23 說(shuō)說(shuō)你對(duì)讀寫鎖的了解

    參考答案

    與傳統(tǒng)鎖不同的是讀寫鎖的規(guī)則是可以共享讀,但只能一個(gè)寫,總結(jié)起來(lái)為:讀讀不互斥、讀寫互斥、寫寫互斥,而一般的獨(dú)占鎖是:讀讀互斥、讀寫互斥、寫寫互斥,而場(chǎng)景中往往讀遠(yuǎn)遠(yuǎn)大于寫,讀寫鎖就是為了這種優(yōu)化而創(chuàng)建出來(lái)的一種機(jī)制。
    注意是讀遠(yuǎn)遠(yuǎn)大于寫,一般情況下獨(dú)占鎖的效率低來(lái)源于高并發(fā)下對(duì)臨界區(qū)的激烈競(jìng)爭(zhēng)導(dǎo)致線程上下文切換。因此當(dāng)并發(fā)不是很高的情況下,讀寫鎖由于需要額外維護(hù)讀鎖的狀態(tài),可能還不如獨(dú)占鎖的效率高。因此需要根據(jù)實(shí)際情況選擇使用。

    在Java中ReadWriteLock的主要實(shí)現(xiàn)為ReentrantReadWriteLock,其提供了以下特性:

  • 公平性選擇:支持公平與非公平(默認(rèn))的鎖獲取方式,吞吐量非公平優(yōu)先于公平。
  • 可重入:讀線程獲取讀鎖之后可以再次獲取讀鎖,寫線程獲取寫鎖之后可以再次獲取寫鎖。
  • 可降級(jí):寫線程獲取寫鎖之后,其還可以再次獲取讀鎖,然后釋放掉寫鎖,那么此時(shí)該線程是讀鎖狀態(tài),也就是降級(jí)操作。
  • 4.24 volatile關(guān)鍵字有什么用?

    參考答案

    當(dāng)一個(gè)變量被定義成volatile之后,它將具備兩項(xiàng)特性:

  • 保證可見性

    當(dāng)寫一個(gè)volatile變量時(shí),JMM會(huì)把該線程本地內(nèi)存中的變量強(qiáng)制刷新到主內(nèi)存中去,這個(gè)寫會(huì)操作會(huì)導(dǎo)致其他線程中的volatile變量緩存無(wú)效。

  • 禁止指令重排

    使用volatile關(guān)鍵字修飾共享變量可以禁止指令重排序,volatile禁止指令重排序有一些規(guī)則:

    • 當(dāng)程序執(zhí)行到volatile變量的讀操作或者寫操作時(shí),在其前面的操作的更改肯定全部已經(jīng)進(jìn)行,且結(jié)果已經(jīng)對(duì)后面的操作可見,在其后面的操作肯定還沒(méi)有進(jìn)行;
    • 在進(jìn)行指令優(yōu)化時(shí),不能將對(duì)volatile變量訪問(wèn)的語(yǔ)句放在其后面執(zhí)行,也不能把volatile變量后面的語(yǔ)句放到其前面執(zhí)行。

    即執(zhí)行到volatile變量時(shí),其前面的所有語(yǔ)句都執(zhí)行完,后面所有語(yǔ)句都未執(zhí)行。且前面語(yǔ)句的結(jié)果對(duì)volatile變量及其后面語(yǔ)句可見。

  • 注意,雖然volatile能夠保證可見性,但它不能保證原子性。volatile變量在各個(gè)線程的工作內(nèi)存中是不存在一致性問(wèn)題的,但是Java里面的運(yùn)算操作符并非原子操作,這導(dǎo)致volatile變量的運(yùn)算在并發(fā)下一樣是不安全的。

    4.25 談?wù)剉olatile的實(shí)現(xiàn)原理

    參考答案

    volatile可以保證線程可見性且提供了一定的有序性,但是無(wú)法保證原子性。在JVM底層volatile是采用“內(nèi)存屏障”來(lái)實(shí)現(xiàn)的。觀察加入volatile關(guān)鍵字和沒(méi)有加入volatile關(guān)鍵字時(shí)所生成的匯編代碼發(fā)現(xiàn),加入volatile關(guān)鍵字時(shí),會(huì)多出一個(gè)lock前綴指令,lock前綴指令實(shí)際上相當(dāng)于一個(gè)內(nèi)存屏障,內(nèi)存屏障會(huì)提供3個(gè)功能:

  • 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;
  • 它會(huì)強(qiáng)制將對(duì)緩存的修改操作立即寫入主存;
  • 如果是寫操作,它會(huì)導(dǎo)致其他CPU中對(duì)應(yīng)的緩存行無(wú)效。
  • 4.26 說(shuō)說(shuō)你對(duì)JUC的了解

    參考答案

    JUC是java.util.concurrent的縮寫,該包參考自EDU.oswego.cs.dl.util.concurrent,是JSR 166標(biāo)準(zhǔn)規(guī)范的一個(gè)實(shí)現(xiàn)。JSR 166是一個(gè)關(guān)于Java并發(fā)編程的規(guī)范提案,在JDK中該規(guī)范由java.util.concurrent包實(shí)現(xiàn)。即JUC是Java提供的并發(fā)包,其中包含了一些并發(fā)編程用到的基礎(chǔ)組件。

    JUC這個(gè)包下的類基本上包含了我們?cè)诓l(fā)編程時(shí)用到的一些工具,大致可以分為以下幾類:

    • 原子更新

      Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環(huán) 境下,無(wú)鎖的進(jìn)行原子操作。在Atomic包里一共有12個(gè)類,四種原子更新方式,分別是原子更新基本類型,原子更新 數(shù)組,原子更新引用和原子更新字段。

    • 鎖和條件變量

      java.util.concurrent.locks包下包含了同步器的框架 AbstractQueuedSynchronizer,基于AQS構(gòu)建的Lock以及與Lock配合可以實(shí)現(xiàn)等待/通知模式的Condition。JUC 下的大多數(shù)工具類用到了Lock和Condition來(lái)實(shí)現(xiàn)并發(fā)。

    • 線程池

      涉及到的類比如:Executor、Executors、ThreadPoolExector、 AbstractExecutorService、Future、Callable、ScheduledThreadPoolExecutor等等。

    • 阻塞隊(duì)列

      涉及到的類比如:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、LinkedBlockingDeque等等。

    • 并發(fā)容器

      涉及到的類比如:ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、CopyOnWriteArraySet等等。

    • 同步器

      剩下的是一些在并發(fā)編程中時(shí)常會(huì)用到的工具類,主要用來(lái)協(xié)助線程同步。比如:CountDownLatch、CyclicBarrier、Exchanger、Semaphore、FutureTask等等。

    4.27 說(shuō)說(shuō)你對(duì)AQS的理解

    參考答案

    抽象隊(duì)列同步器AbstractQueuedSynchronizer (以下都簡(jiǎn)稱AQS),是用來(lái)構(gòu)建鎖或者其他同步組件的骨架類,減少了各功能組件實(shí)現(xiàn)的代碼量,也解決了在實(shí)現(xiàn)同步器時(shí)涉及的大量細(xì)節(jié)問(wèn)題,例如等待線程采用FIFO隊(duì)列操作的順序。在不同的同步器中還可以定義一些靈活的標(biāo)準(zhǔn)來(lái)判斷某個(gè)線程是應(yīng)該通過(guò)還是等待。

    AQS采用模板方法模式,在內(nèi)部維護(hù)了n多的模板的方法的基礎(chǔ)上,子類只需要實(shí)現(xiàn)特定的幾個(gè)方法(不是抽象方法!不是抽象方法!不是抽象方法!),就可以實(shí)現(xiàn)子類自己的需求。

    基于AQS實(shí)現(xiàn)的組件,諸如:

    • ReentrantLock 可重入鎖(支持公平和非公平的方式獲取鎖);
    • Semaphore 計(jì)數(shù)信號(hào)量;
    • ReentrantReadWriteLock 讀寫鎖。

    擴(kuò)展閱讀

    AQS內(nèi)部維護(hù)了一個(gè)int成員變量來(lái)表示同步狀態(tài),通過(guò)內(nèi)置的FIFO(first-in-first-out)同步隊(duì)列來(lái)控制獲取共享資源的線程。

    我們可以猜測(cè)出,AQS其實(shí)主要做了這么幾件事情:

    • 同步狀態(tài)(state)的維護(hù)管理;
    • 等待隊(duì)列的維護(hù)管理;
    • 線程的阻塞與喚醒。

    通過(guò)AQS內(nèi)部維護(hù)的int型的state,可以用于表示任意狀態(tài)!

    • ReentrantLock用它來(lái)表示鎖的持有者線程已經(jīng)重復(fù)獲取該鎖的次數(shù),而對(duì)于非鎖的持有者線程來(lái)說(shuō),如果state大于0,意味著無(wú)法獲取該鎖,將該線程包裝為Node,加入到同步等待隊(duì)列里。
    • Semaphore用它來(lái)表示剩余的許可數(shù)量,當(dāng)許可數(shù)量為0時(shí),對(duì)未獲取到許可但正在努力嘗試獲取許可的線程來(lái)說(shuō),會(huì)進(jìn)入同步等待隊(duì)列,阻塞,直到一些線程釋放掉持有的許可(state+1),然后爭(zhēng)用釋放掉的許可。
    • FutureTask用它來(lái)表示任務(wù)的狀態(tài)(未開始、運(yùn)行中、完成、取消)。
    • ReentrantReadWriteLock在使用時(shí),稍微有些不同,int型state用二進(jìn)制表示是32位,前16位(高位)表示為讀鎖,后面的16位(低位)表示為寫鎖。
    • CountDownLatch使用state表示計(jì)數(shù)次數(shù),state大于0,表示需要加入到同步等待隊(duì)列并阻塞,直到state等于0,才會(huì)逐一喚醒等待隊(duì)列里的線程。

    AQS通過(guò)內(nèi)置的FIFO(first-in-first-out)同步隊(duì)列來(lái)控制獲取共享資源的線程。CLH隊(duì)列是FIFO的雙端雙向隊(duì)列,AQS的同步機(jī)制就是依靠這個(gè)CLH隊(duì)列完成的。隊(duì)列的每個(gè)節(jié)點(diǎn),都有前驅(qū)節(jié)點(diǎn)指針和后繼節(jié)點(diǎn)指針。如下圖:

    4.28 LongAdder解決了什么問(wèn)題,它是如何實(shí)現(xiàn)的?

    參考答案

    高并發(fā)下計(jì)數(shù),一般最先想到的應(yīng)該是AtomicLong/AtomicInt,AtmoicXXX使用硬件級(jí)別的指令 CAS 來(lái)更新計(jì)數(shù)器的值,這樣可以避免加鎖,機(jī)器直接支持的指令,效率也很高。但是AtomicXXX中的 CAS 操作在出現(xiàn)線程競(jìng)爭(zhēng)時(shí),失敗的線程會(huì)白白地循環(huán)一次,在并發(fā)很大的情況下,因?yàn)槊看蜟AS都只有一個(gè)線程能成功,競(jìng)爭(zhēng)失敗的線程會(huì)非常多。失敗次數(shù)越多,循環(huán)次數(shù)就越多,很多線程的CAS操作越來(lái)越接近 自旋鎖(spin lock)。計(jì)數(shù)操作本來(lái)是一個(gè)很簡(jiǎn)單的操作,實(shí)際需要耗費(fèi)的cpu時(shí)間應(yīng)該是越少越好,AtomicXXX在高并發(fā)計(jì)數(shù)時(shí),大量的cpu時(shí)間都浪費(fèi)會(huì)在 自旋 上了,這很浪費(fèi),也降低了實(shí)際的計(jì)數(shù)效率。

    LongAdder是jdk8新增的用于并發(fā)環(huán)境的計(jì)數(shù)器,目的是為了在高并發(fā)情況下,代替AtomicLong/AtomicInt,成為一個(gè)用于高并發(fā)情況下的高效的通用計(jì)數(shù)器。說(shuō)LongAdder比在高并發(fā)時(shí)比AtomicLong更高效,這么說(shuō)有什么依據(jù)呢?LongAdder是根據(jù)鎖分段來(lái)實(shí)現(xiàn)的,它里面維護(hù)一組按需分配的計(jì)數(shù)單元,并發(fā)計(jì)數(shù)時(shí),不同的線程可以在不同的計(jì)數(shù)單元上進(jìn)行計(jì)數(shù),這樣減少了線程競(jìng)爭(zhēng),提高了并發(fā)效率。本質(zhì)上是用空間換時(shí)間的思想,不過(guò)在實(shí)際高并發(fā)情況中消耗的空間可以忽略不計(jì)。

    現(xiàn)在,在處理高并發(fā)計(jì)數(shù)時(shí),應(yīng)該優(yōu)先使用LongAdder,而不是繼續(xù)使用AtomicLong。當(dāng)然,線程競(jìng)爭(zhēng)很低的情況下進(jìn)行計(jì)數(shù),使用Atomic還是更簡(jiǎn)單更直接,并且效率稍微高一些。其他情況,比如序號(hào)生成,這種情況下需要準(zhǔn)確的數(shù)值,全局唯一的AtomicLong才是正確的選擇,此時(shí)不應(yīng)該使用LongAdder。

    4.29 介紹下ThreadLocal和它的應(yīng)用場(chǎng)景

    參考答案

    ThreadLocal顧名思義是線程私有的局部變量存儲(chǔ)容器,可以理解成每個(gè)線程都有自己專屬的存儲(chǔ)容器,它用來(lái)存儲(chǔ)線程私有變量,其實(shí)它只是一個(gè)外殼,內(nèi)部真正存取是一個(gè)Map。每個(gè)線程可以通過(guò)set()和get()存取變量,多線程間無(wú)法訪問(wèn)各自的局部變量,相當(dāng)于在每個(gè)線程間建立了一個(gè)隔板。只要線程處于活動(dòng)狀態(tài),它所對(duì)應(yīng)的ThreadLocal實(shí)例就是可訪問(wèn)的,線程被終止后,它的所有實(shí)例將被垃圾收集。總之記住一句話:ThreadLocal存儲(chǔ)的變量屬于當(dāng)前線程。

    ThreadLocal經(jīng)典的使用場(chǎng)景是為每個(gè)線程分配一個(gè) JDBC 連接 Connection,這樣就可以保證每個(gè)線程的都在各自的 Connection 上進(jìn)行數(shù)據(jù)庫(kù)的操作,不會(huì)出現(xiàn) A 線程關(guān)了 B線程正在使用的 Connection。 另外ThreadLocal還經(jīng)常用于管理Session會(huì)話,將Session保存在ThreadLocal中,使線程處理多次處理會(huì)話時(shí)始終是同一個(gè)Session。

    4.30 請(qǐng)介紹ThreadLocal的實(shí)現(xiàn)原理,它是怎么處理hash沖突的?

    參考答案

    Thread類中有個(gè)變量threadLocals,它的類型為ThreadLocal中的一個(gè)內(nèi)部類ThreadLocalMap,這個(gè)類沒(méi)有實(shí)現(xiàn)map接口,就是一個(gè)普通的Java類,但是實(shí)現(xiàn)的類似map的功能。每個(gè)線程都有自己的一個(gè)map,map是一個(gè)數(shù)組的數(shù)據(jù)結(jié)構(gòu)存儲(chǔ)數(shù)據(jù),每個(gè)元素是一個(gè)Entry,entry的key是ThreadLocal的引用,也就是當(dāng)前變量的副本,value就是set的值。代碼如下所示:

    public class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null; }

    ThreadLocalMap是ThreadLocal的內(nèi)部類,每個(gè)數(shù)據(jù)用Entry保存,其中的Entry繼承與WeakReference,用一個(gè)鍵值對(duì)存儲(chǔ),鍵為ThreadLocal的引用。為什么是WeakReference呢?如果是強(qiáng)引用,即使把ThreadLocal設(shè)置為null,GC也不會(huì)回收,因?yàn)門hreadLocalMap對(duì)它有強(qiáng)引用。代碼如下所示:

    static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;} }

    ThreadLocal中的set方法的實(shí)現(xiàn)邏輯,先獲取當(dāng)前線程,取出當(dāng)前線程的ThreadLocalMap,如果不存在就會(huì)創(chuàng)建一個(gè)ThreadLocalMap,如果存在就會(huì)把當(dāng)前的threadlocal的引用作為鍵,傳入的參數(shù)作為值存入map中。代碼如下所示:

    public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {map.set(this, value);} else {createMap(t, value);} }

    ThreadLocal中g(shù)et方法的實(shí)現(xiàn)邏輯,獲取當(dāng)前線程,取出當(dāng)前線程的ThreadLocalMap,用當(dāng)前的threadlocak作為key在ThreadLocalMap查找,如果存在不為空的Entry,就返回Entry中的value,否則就會(huì)執(zhí)行初始化并返回默認(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(); }

    ThreadLocal中remove方法的實(shí)現(xiàn)邏輯,還是先獲取當(dāng)前線程的ThreadLocalMap變量,如果存在就調(diào)用ThreadLocalMap的remove方法。ThreadLocalMap的存儲(chǔ)就是數(shù)組的實(shí)現(xiàn),因此需要確定元素的位置,找到Entry,把entry的鍵值對(duì)都設(shè)為null,最后也Entry也設(shè)置為null。其實(shí)這其中會(huì)有哈希沖突,具體見下文。代碼如下所示:

    public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {m.remove(this);} }

    ThreadLocal中的hash code非常簡(jiǎn)單,就是調(diào)用AtomicInteger的getAndAdd方法,參數(shù)是個(gè)固定值0x61c88647。上面說(shuō)過(guò)ThreadLocalMap的結(jié)構(gòu)非常簡(jiǎn)單只用一個(gè)數(shù)組存儲(chǔ),并沒(méi)有鏈表結(jié)構(gòu),當(dāng)出現(xiàn)Hash沖突時(shí)采用線性查找的方式,所謂線性查找,就是根據(jù)初始key的hashcode值確定元素在table數(shù)組中的位置,如果發(fā)現(xiàn)這個(gè)位置上已經(jīng)有其他key值的元素被占用,則利用固定的算法尋找一定步長(zhǎng)的下個(gè)位置,依次判斷,直至找到能夠存放的位置。如果產(chǎn)生多次hash沖突,處理起來(lái)就沒(méi)有HashMap的效率高,為了避免哈希沖突,使用盡量少的threadlocal變量。

    4.31 介紹一下線程池

    參考答案

    系統(tǒng)啟動(dòng)一個(gè)新線程的成本是比較高的,因?yàn)樗婕芭c操作系統(tǒng)交互。在這種情形下,使用線程池可以很好地提高性能,尤其是當(dāng)程序中需要?jiǎng)?chuàng)建大量生存期很短暫的線程時(shí),更應(yīng)該考慮使用線程池。

    與數(shù)據(jù)庫(kù)連接池類似的是,線程池在系統(tǒng)啟動(dòng)時(shí)即創(chuàng)建大量空閑的線程,程序?qū)⒁粋€(gè)Runnable對(duì)象或Callable對(duì)象傳給線程池,線程池就會(huì)啟動(dòng)一個(gè)空閑的線程來(lái)執(zhí)行它們的run()或call()方法,當(dāng)run()或call()方法執(zhí)行結(jié)束后,該線程并不會(huì)死亡,而是再次返回線程池中成為空閑狀態(tài),等待執(zhí)行下一個(gè)Runnable對(duì)象的run()或call()方法。

    從Java 5開始,Java內(nèi)建支持線程池。Java 5新增了一個(gè)Executors工廠類來(lái)產(chǎn)生線程池,該工廠類包含如下幾個(gè)靜態(tài)工廠方法來(lái)創(chuàng)建線程池。創(chuàng)建出來(lái)的線程池,都是通過(guò)ThreadPoolExecutor類來(lái)實(shí)現(xiàn)的。

    • newCachedThreadPool():創(chuàng)建一個(gè)具有緩存功能的線程池,系統(tǒng)根據(jù)需要?jiǎng)?chuàng)建線程,這些線程將會(huì)被緩存在線程池中。
    • newFixedThreadPool(int nThreads):創(chuàng)建一個(gè)可重用的、具有固定線程數(shù)的線程池。
    • newSingleThreadExecutor():創(chuàng)建一個(gè)只有單線程的線程池,它相當(dāng)于調(diào)用newFixedThread Pool()方法時(shí)傳入?yún)?shù)為1。
    • newScheduledThreadPool(int corePoolSize):創(chuàng)建具有指定線程數(shù)的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。corePoolSize指池中所保存的線程數(shù),即使線程是空閑的也被保存在線程池內(nèi)。
    • newSingleThreadScheduledExecutor():創(chuàng)建只有一個(gè)線程的線程池,它可以在指定延遲后執(zhí)行線程任務(wù)。
    • ExecutorService newWorkStealingPool(int parallelism):創(chuàng)建持有足夠的線程的線程池來(lái)支持給定的并行級(jí)別,該方法還會(huì)使用多個(gè)隊(duì)列來(lái)減少競(jìng)爭(zhēng)。
    • ExecutorService newWorkStealingPool():該方法是前一個(gè)方法的簡(jiǎn)化版本。如果當(dāng)前機(jī)器有4個(gè)CPU,則目標(biāo)并行級(jí)別被設(shè)置為4,也就是相當(dāng)于為前一個(gè)方法傳入4作為參數(shù)。

    4.32 介紹一下線程池的工作流程

    參考答案

    線程池的工作流程如下圖所示:

  • 判斷核心線程池是否已滿,沒(méi)滿則創(chuàng)建一個(gè)新的工作線程來(lái)執(zhí)行任務(wù)。
  • 判斷任務(wù)隊(duì)列是否已滿,沒(méi)滿則將新提交的任務(wù)添加在工作隊(duì)列。
  • 判斷整個(gè)線程池是否已滿,沒(méi)滿則創(chuàng)建一個(gè)新的工作線程來(lái)執(zhí)行任務(wù),已滿則執(zhí)行飽和(拒絕)策略。
  • 4.33 線程池都有哪些狀態(tài)?

    參考答案

    線程池一共有五種狀態(tài), 分別是:

  • RUNNING :能接受新提交的任務(wù),并且也能處理阻塞隊(duì)列中的任務(wù)。
  • SHUTDOWN:關(guān)閉狀態(tài),不再接受新提交的任務(wù),但卻可以繼續(xù)處理阻塞隊(duì)列中已保存的任務(wù)。在線程池處于 RUNNING 狀態(tài)時(shí),調(diào)用 shutdown()方法會(huì)使線程池進(jìn)入到該狀態(tài)。
  • STOP:不能接受新任務(wù),也不處理隊(duì)列中的任務(wù),會(huì)中斷正在處理任務(wù)的線程。在線程池處于 RUNNING 或 SHUTDOWN 狀態(tài)時(shí),調(diào)用 shutdownNow() 方法會(huì)使線程池進(jìn)入到該狀態(tài)。
  • TIDYING:如果所有的任務(wù)都已終止了,workerCount (有效線程數(shù)) 為0,線程池進(jìn)入該狀態(tài)后會(huì)調(diào)用 terminated() 方法進(jìn)入TERMINATED 狀態(tài)。
  • TERMINATED:在terminated() 方法執(zhí)行完后進(jìn)入該狀態(tài),默認(rèn)terminated()方法中什么也沒(méi)有做。進(jìn)入TERMINATED的條件如下:
    • 線程池不是RUNNING狀態(tài);
    • 線程池狀態(tài)不是TIDYING狀態(tài)或TERMINATED狀態(tài);
    • 如果線程池狀態(tài)是SHUTDOWN并且workerQueue為空;
    • workerCount為0;
    • 設(shè)置TIDYING狀態(tài)成功。
  • 下圖為線程池的狀態(tài)轉(zhuǎn)換過(guò)程:

    4.34 談?wù)劸€程池的拒絕策略

    參考答案

    當(dāng)線程池的任務(wù)緩存隊(duì)列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來(lái)就會(huì)采取任務(wù)拒絕策略,通常有以下四種策略:

  • AbortPolicy:丟棄任務(wù)并拋出RejectedExecutionException異常。
  • DiscardPolicy:也是丟棄任務(wù),但是不拋出異常。
  • DiscardOldestPolicy:丟棄隊(duì)列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)(重復(fù)該過(guò)程)。
  • CallerRunsPolicy:由調(diào)用線程處理該任務(wù)。
  • 4.35 線程池的隊(duì)列大小你通常怎么設(shè)置?

    參考答案

  • CPU密集型任務(wù)

    盡量使用較小的線程池,一般為CPU核心數(shù)+1。 因?yàn)镃PU密集型任務(wù)使得CPU使用率很高,若開過(guò)多的線程數(shù),會(huì)造成CPU過(guò)度切換。

  • IO密集型任務(wù)

    可以使用稍大的線程池,一般為2*CPU核心數(shù)。 IO密集型任務(wù)CPU使用率并不高,因此可以讓CPU在等待IO的時(shí)候有其他線程去處理別的任務(wù),充分利用CPU時(shí)間。

  • 混合型任務(wù)

    可以將任務(wù)分成IO密集型和CPU密集型任務(wù),然后分別用不同的線程池去處理。 只要分完之后兩個(gè)任務(wù)的執(zhí)行時(shí)間相差不大,那么就會(huì)比串行執(zhí)行來(lái)的高效。因?yàn)槿绻麆澐种髢蓚€(gè)任務(wù)執(zhí)行時(shí)間有數(shù)據(jù)級(jí)的差距,那么拆分沒(méi)有意義。因?yàn)橄葓?zhí)行完的任務(wù)就要等后執(zhí)行完的任務(wù),最終的時(shí)間仍然取決于后執(zhí)行完的任務(wù),而且還要加上任務(wù)拆分與合并的開銷,得不償失。

  • 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?

    參考答案

    線程池主要有如下6個(gè)參數(shù):

  • corePoolSize(核心工作線程數(shù)):當(dāng)向線程池提交一個(gè)任務(wù)時(shí),若線程池已創(chuàng)建的線程數(shù)小于corePoolSize,即便此時(shí)存在空閑線程,也會(huì)通過(guò)創(chuàng)建一個(gè)新線程來(lái)執(zhí)行該任務(wù),直到已創(chuàng)建的線程數(shù)大于或等于corePoolSize時(shí)。

  • maximumPoolSize(最大線程數(shù)):線程池所允許的最大線程個(gè)數(shù)。當(dāng)隊(duì)列滿了,且已創(chuàng)建的線程數(shù)小于maximumPoolSize,則線程池會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)。另外,對(duì)于無(wú)界隊(duì)列,可忽略該參數(shù)。

  • keepAliveTime(多余線程存活時(shí)間):當(dāng)線程池中線程數(shù)大于核心線程數(shù)時(shí),線程的空閑時(shí)間如果超過(guò)線程存活時(shí)間,那么這個(gè)線程就會(huì)被銷毀,直到線程池中的線程數(shù)小于等于核心線程數(shù)。

  • workQueue(隊(duì)列):用于傳輸和保存等待執(zhí)行任務(wù)的阻塞隊(duì)列。

  • threadFactory(線程創(chuàng)建工廠):用于創(chuàng)建新線程。threadFactory創(chuàng)建的線程也是采用new Thread()方式,threadFactory創(chuàng)建的線程名都具有統(tǒng)一的風(fēng)格:pool-m-thread-n(m為線程池的編號(hào),n為線程池內(nèi)的線程編號(hào))。
    數(shù)。 IO密集型任務(wù)CPU使用率并不高,因此可以讓CPU在等待IO的時(shí)候有其他線程去處理別的任務(wù),充分利用CPU時(shí)間。

  • 混合型任務(wù)

    可以將任務(wù)分成IO密集型和CPU密集型任務(wù),然后分別用不同的線程池去處理。 只要分完之后兩個(gè)任務(wù)的執(zhí)行時(shí)間相差不大,那么就會(huì)比串行執(zhí)行來(lái)的高效。因?yàn)槿绻麆澐种髢蓚€(gè)任務(wù)執(zhí)行時(shí)間有數(shù)據(jù)級(jí)的差距,那么拆分沒(méi)有意義。因?yàn)橄葓?zhí)行完的任務(wù)就要等后執(zhí)行完的任務(wù),最終的時(shí)間仍然取決于后執(zhí)行完的任務(wù),而且還要加上任務(wù)拆分與合并的開銷,得不償失。

  • 4.36 線程池有哪些參數(shù),各個(gè)參數(shù)的作用是什么?

    參考答案

    線程池主要有如下6個(gè)參數(shù):

  • corePoolSize(核心工作線程數(shù)):當(dāng)向線程池提交一個(gè)任務(wù)時(shí),若線程池已創(chuàng)建的線程數(shù)小于corePoolSize,即便此時(shí)存在空閑線程,也會(huì)通過(guò)創(chuàng)建一個(gè)新線程來(lái)執(zhí)行該任務(wù),直到已創(chuàng)建的線程數(shù)大于或等于corePoolSize時(shí)。
  • maximumPoolSize(最大線程數(shù)):線程池所允許的最大線程個(gè)數(shù)。當(dāng)隊(duì)列滿了,且已創(chuàng)建的線程數(shù)小于maximumPoolSize,則線程池會(huì)創(chuàng)建新的線程來(lái)執(zhí)行任務(wù)。另外,對(duì)于無(wú)界隊(duì)列,可忽略該參數(shù)。
  • keepAliveTime(多余線程存活時(shí)間):當(dāng)線程池中線程數(shù)大于核心線程數(shù)時(shí),線程的空閑時(shí)間如果超過(guò)線程存活時(shí)間,那么這個(gè)線程就會(huì)被銷毀,直到線程池中的線程數(shù)小于等于核心線程數(shù)。
  • workQueue(隊(duì)列):用于傳輸和保存等待執(zhí)行任務(wù)的阻塞隊(duì)列。
  • threadFactory(線程創(chuàng)建工廠):用于創(chuàng)建新線程。threadFactory創(chuàng)建的線程也是采用new Thread()方式,threadFactory創(chuàng)建的線程名都具有統(tǒng)一的風(fēng)格:pool-m-thread-n(m為線程池的編號(hào),n為線程池內(nèi)的線程編號(hào))。
  • handler(拒絕策略):當(dāng)線程池和隊(duì)列都滿了,再加入線程會(huì)執(zhí)行此策略。
  • 總結(jié)

    以上是生活随笔為你收集整理的【2022】多线程并发编程面试真题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。