面試題-Java多線程基礎(chǔ)、實(shí)現(xiàn)工具和可見性保證

前言

Java多線程部分的題目,是我根據(jù)Java Guide的面試突擊版本V3.0再整理出來的,其中,我選擇了一些比較重要的問題,并重新做出相應(yīng)回答,并添加了一些比較重要的問題,希望對(duì)大家起到一定的幫助。

系列文章:

面試題-Java基礎(chǔ)

面試題-Java集合

Java多線程

多線程基礎(chǔ)

  1. 編寫多線程程序可能會(huì)存在的一些問題(重要)
  • 安全性問題:由于編譯器、硬件和運(yùn)行時(shí)的機(jī)制是不可預(yù)測(cè)的,假如沒有正確的同步機(jī)制,可能會(huì)產(chǎn)生安全性問題。
    • 原子性:一組操作未必如我們所想的是原子的,中間可能被其他線程干擾。
    • 可見性:線程對(duì)共享變量的修改,未必對(duì)其他線程是可見的。
  • 活躍性問題:某個(gè)操作無法繼續(xù)執(zhí)行下去時(shí),就會(huì)出現(xiàn)活躍性問題。比如:死鎖饑餓活鎖等等
  • 性能問題:上下文切換開銷;同步機(jī)制帶來的其他開銷等等
  1. 線程生命周期中有哪些狀態(tài)?(核查線程問題時(shí),了解每種狀態(tài)代表的含義是基礎(chǔ))
    1. NEW:一個(gè)線程被創(chuàng)建出來時(shí),start()被調(diào)用之前,狀態(tài)為NEW
    2. Runnable:調(diào)用start后,線程狀態(tài)變?yōu)镽unnable
      1. Running:上CPU執(zhí)行中,外部不可見
      2. Ready狀態(tài):排隊(duì)等待執(zhí)行,外部不可見
      3. IO wait:等待IO操作,外部不可見
    3. Blocked:線程等待獲取鎖
    4. Waiting:
      1. TIMED_WAITING:有超時(shí)時(shí)間的等待
      2. WAITING:沒有超時(shí)時(shí)間的等地啊
    5. Terminated:執(zhí)行完畢,進(jìn)入Terminated狀態(tài)
線程狀態(tài).png
  1. 線程的中斷機(jī)制:如何正確的干涉線程的執(zhí)行過程

    首先要明確,中斷并不是停止,中斷可以理解為一種通知機(jī)制。

    線程在不同的狀態(tài)時(shí),對(duì)中斷的響應(yīng)也不同。

    • Runnable狀態(tài):線程處于Runnable狀態(tài)時(shí),調(diào)用interrupt方法只會(huì)設(shè)置線程的中斷標(biāo)記位為true,需要線程在執(zhí)行代碼中自己判斷中斷標(biāo)記位來得到通知。
      • 注:判斷中斷標(biāo)記位的方法有兩種:
        • isInterrupted:實(shí)例方法
        • interrupted:靜態(tài)方法,會(huì)清空標(biāo)記位
    • Blocked狀態(tài):線程處于Blocked狀態(tài)時(shí),調(diào)用interrupt方法只會(huì)設(shè)置線程的中斷標(biāo)記位,線程什么都不會(huì)做,會(huì)繼續(xù)Blocked。
    • Timed_wating或Waiting:線程處于Waiting相關(guān)狀態(tài)時(shí),調(diào)用interrupt方法會(huì)設(shè)置線程的中斷標(biāo)記位,并且線程內(nèi)部會(huì)自動(dòng)監(jiān)測(cè)這個(gè)中斷狀態(tài),當(dāng)監(jiān)測(cè)到為true,會(huì)拋出InterruptedException,清空標(biāo)記位,線程會(huì)重新變?yōu)镽unnable狀態(tài)

    總結(jié):中斷線程對(duì)Blocked方法無效,不會(huì)立即響應(yīng),線程會(huì)一直阻塞。已經(jīng)在運(yùn)行中的線程需要自己監(jiān)測(cè)中斷標(biāo)記位;在Waiting狀態(tài)中的線程可以自動(dòng)監(jiān)測(cè)標(biāo)記位,通過拋出異常中斷。

    特殊情況:

    當(dāng)線程處于等待IO(IO wait狀態(tài))時(shí),調(diào)用interrupt方法會(huì)設(shè)置標(biāo)記位

    • 如果Channel是可中斷的,會(huì)拋出ClosedByInterruptException異常
    • 如果是不可中斷的,比如selector選擇器,會(huì)從阻塞中立即返回

    附,討論具體方法代碼的文章:[java 中斷線程的幾種方式 interrupt()]

Java中的保證線程安全性的相關(guān)模型與工具

為了解決多線程編程中的線程安全性問題,Java提供了JMM內(nèi)存模型和多種工具供我們選擇。

synchronized關(guān)鍵字

可以解決什么問題?

synchronized關(guān)鍵字,可以保證原子性和可見性。

如何使用

synchronized關(guān)鍵字,可以使用在實(shí)例方法,靜態(tài)方法和代碼塊中。

  • 實(shí)例方法鎖定的是this
  • 靜態(tài)方法鎖定的是當(dāng)前的Class對(duì)象。
  • 代碼塊中可以自定義一個(gè)對(duì)象。
JVM對(duì)synchronized的性能優(yōu)化
  1. 偏向鎖

    • 機(jī)制:如果一直是同一個(gè)線程反復(fù)申請(qǐng)鎖,那么可以直接進(jìn)入代碼塊,不需要做其他同步操作。

    • 適用場(chǎng)景:某一個(gè)線程會(huì)反復(fù)進(jìn)入同步代碼塊,就省略了加鎖操作。

  2. 輕量級(jí)鎖

    • 機(jī)制:當(dāng)申請(qǐng)偏向鎖失敗時(shí)(有其他線程在占用),會(huì)升級(jí),申請(qǐng)輕量級(jí)鎖
    • 適用場(chǎng)景:多個(gè)線程交替執(zhí)行同步代碼塊內(nèi)容,并且有較短時(shí)間的重疊(如果沒有重疊,就會(huì)一直保持偏向鎖,如果重疊較長(zhǎng),就會(huì)導(dǎo)致鎖升級(jí))
  3. 重量級(jí)鎖

    • 機(jī)制:申請(qǐng)輕量級(jí)鎖失敗,會(huì)升級(jí)為重量級(jí)鎖,首先會(huì)先自旋若干個(gè)空循環(huán),如果循環(huán)之后還是無法獲取鎖,只能等待操作系統(tǒng)掛起
    • 適用場(chǎng)景:多個(gè)線程同時(shí)執(zhí)行同步代碼塊,并且每個(gè)線程占用鎖時(shí)間短(自旋一陣大部分可以成功申請(qǐng)到鎖)。
  4. 鎖消除

    • 機(jī)制:JIT會(huì)通過逃逸分析自動(dòng)去除不可能出現(xiàn)競(jìng)爭(zhēng)的鎖

Reentrant Lock

可以解決什么問題?

和synchronized一樣,Reentrant Lock也可以保證原子性和可見性。

Reentrant Lock 和synchronized的區(qū)別

首先要明確,synchronized是在JVM層面實(shí)現(xiàn)的,而Reentrant Lock是在Java代碼層面實(shí)現(xiàn)的。

Reentrant Lock的性能在jdk1.5之前是要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized,但是Jdk1.6版本虛擬機(jī)對(duì)synchronized做了優(yōu)化(就是上面講過的偏向鎖等等),使得他們的性能幾乎差不多了。

既然如此,為什么還需要使用Reentrant Lock呢?因?yàn)镽eentrant Lock和synchronized相比,提供了更豐富的功能。

可中斷鎖等待

在前面多線程基礎(chǔ)部分,提到過中斷。當(dāng)線程處于BLOCKED狀態(tài)時(shí)(僅指使用synchronized關(guān)鍵字),interrupt函數(shù)只能設(shè)置中斷標(biāo)記位為true,但是線程依舊保持在阻塞狀態(tài)。如果使用Reentrant Lock,在interrupt調(diào)用后,可以拋出InterruptedException響應(yīng)通知,并且線程從阻塞狀態(tài)中返回。

鎖申請(qǐng)支持設(shè)置超時(shí)時(shí)間

可以使用tryLock或者待超時(shí)參數(shù)的tryLock,成功會(huì)返回true,失敗返回false。不會(huì)像synchronized,如果一直申請(qǐng)不到鎖,就會(huì)持續(xù)阻塞。

公平鎖

synchronized的實(shí)現(xiàn)是非公平的,當(dāng)鎖可用時(shí),會(huì)從等待隊(duì)列中隨機(jī)選擇一個(gè)線程。而Reentrant Lock可以設(shè)置公平模式,保證不會(huì)出現(xiàn)饑餓現(xiàn)象。

可等待不同條件

synchronized的wait方法的實(shí)現(xiàn)機(jī)制是,把該線程放入鎖對(duì)象的等待隊(duì)列中,這個(gè)隊(duì)列只有一個(gè)。如果需要區(qū)分等待的條件,就無法實(shí)現(xiàn)了。Reentrant Lock可以配合Condition使用,區(qū)分不同的等待條件,在需要等待的條件上wait就可以。

Volatile

可以解決什么問題?

Volatile只能保證可見性。

Volatile

可以解決什么問題?

Volatile只能保證可見性。

可見性保證的實(shí)現(xiàn)原理

上面提到了鎖和volatile工具都可以保證可見性,那么可見性究竟是如何實(shí)現(xiàn)的呢?

JMM內(nèi)存模型

主內(nèi)存和線程的本地內(nèi)存以及通信過程

JMM定義,線程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線程在自己的本地內(nèi)存中持有一個(gè)共享變量的副本。主內(nèi)存不僅僅是普通意義上的內(nèi)存,而是一個(gè)抽象的概念,主要包括了編譯器緩存、寄存器等硬件相關(guān)的優(yōu)化。當(dāng)兩個(gè)線程想要通信,線程A需要把自己本地內(nèi)存中的共享變量刷新到主內(nèi)存中,線程B從主內(nèi)存中獲取共享變量的值。示意圖如下:

內(nèi)存模型.png

總結(jié):JMM通過控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互,來為java程序員提供內(nèi)存可見性保證

JMM具體是如何控制主內(nèi)存和本地內(nèi)存的交互呢?

我們知道,無法保證可見性的根源源于重排序,在現(xiàn)代PC中,存在著兩種重排序。

  1. 編譯器重排序

  2. 處理器重排序

那么要保證可靠性,禁止重排序就可以了。

對(duì)于編譯器重排序,JMM的編譯器重排序規(guī)則會(huì)直接禁止編譯器重排序。對(duì)于處理器重排序,JMM會(huì)在某些指令處,插入內(nèi)存屏障來禁止處理器重排序。

Happens-before-更友好的方式來描述操作之間的內(nèi)存可見性

假如沒有Happens-before原則,那么作為程序員的我們,需要去詳細(xì)了解JMM的編譯器重排序規(guī)則和內(nèi)存屏障相關(guān)的細(xì)節(jié),Happens-before原則,給程序員提供了一個(gè)更加友好易懂的視圖,方便理解。

具體來說,如果A存在對(duì)于B的Happens-before關(guān)系,那么A操作一定對(duì)B操作可見。下面是幾個(gè)比較重要的明細(xì)原則:

  • 程序順序規(guī)則:一個(gè)線程中的每個(gè)操作,happens- before 于該線程中的任意后續(xù)操作。

  • 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器鎖的解鎖,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖。

  • volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀。

  • 傳遞性:如果A happens- before B,且B happens- before C,那么A happens- before C。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容