全網(wǎng)最全!2021年BATJ一線名企java面試題總結(jié),助你備戰(zhàn)金三銀四!

前言

金三銀四將至,最近收到太多粉絲朋友要求我整理出一套面試題了,由于年關(guān)將至,自己手里面的事情也比較多,最近是真的忙,所以更新也給大家落下了!在此先說一句抱歉。話不多說,下面整理了多家一線名企的java面試題合集給大家放在下面了,希望對(duì)大家有幫助!

類加載機(jī)制

Java 程序是怎樣運(yùn)行的?

  • 首先通過 Javac 編譯器將 .java 轉(zhuǎn)為 JVM 可加載的 .class 字節(jié)碼文件。

    Javac 是由 Java 編寫的程序,編譯過程可以分為: ① 詞法解析,通過空格分割出單詞、操作符、控制符等信息,形成 token 信息流,傳遞給語法解析器。② 語法解析,把 token 信息流按照 Java 語法規(guī)則組裝成語法樹。③ 語義分析,檢查關(guān)鍵字使用是否合理、類型是否匹配、作用域是否正確等。④ 字節(jié)碼生成,將前面各個(gè)步驟的信息轉(zhuǎn)換為字節(jié)碼。

    字節(jié)碼必須通過類加載過程加載到 JVM 后才可以執(zhí)行,執(zhí)行有三種模式,解釋執(zhí)行、JIT 編譯執(zhí)行、JIT 編譯與解釋器混合執(zhí)行(主流 JVM 默認(rèn)執(zhí)行的方式)。混合模式的優(yōu)勢(shì)在于解釋器在啟動(dòng)時(shí)先解釋執(zhí)行,省去編譯時(shí)間。

  • 之后通過即時(shí)編譯器 JIT 把字節(jié)碼文件編譯成本地機(jī)器碼。

    Java 程序最初都是通過解釋器進(jìn)行解釋執(zhí)行的,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁,就會(huì)認(rèn)定其為"熱點(diǎn)代碼",熱點(diǎn)代碼的檢測(cè)主要有基于采樣和基于計(jì)數(shù)器兩種方式,為了提高熱點(diǎn)代碼的執(zhí)行效率,虛擬機(jī)會(huì)把它們編譯成本地機(jī)器碼,盡可能對(duì)代碼優(yōu)化,在運(yùn)行時(shí)完成這個(gè)任務(wù)的后端編譯器被稱為即時(shí)編譯器。

  • 還可以通過靜態(tài)的提前編譯器 AOT 直接把程序編譯成與目標(biāo)機(jī)器指令集相關(guān)的二進(jìn)制代碼。

類加載是什么?

Class 文件中描述的各類信息都需要加載到虛擬機(jī)后才能使用。JVM 把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型,這個(gè)過程稱為虛擬機(jī)的類加載機(jī)制。

與編譯時(shí)需要連接的語言不同,Java 中類型的加載、連接和初始化都是在運(yùn)行期間完成的,這增加了性能開銷,但卻提供了極高的擴(kuò)展性,Java 動(dòng)態(tài)擴(kuò)展的語言特性就是依賴運(yùn)行期動(dòng)態(tài)加載和連接實(shí)現(xiàn)的。

一個(gè)類型從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,整個(gè)生命周期經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段,其中驗(yàn)證、解析和初始化三個(gè)部分稱為連接。加載、驗(yàn)證、準(zhǔn)備、初始化階段的順序是確定的,解析則不一定:可能在初始化之后再開始,這是為了支持 Java 的動(dòng)態(tài)綁定。

類初始化的情況有哪些?

① 遇到 newgetstaticputstaticinvokestatic 字節(jié)碼指令時(shí),還未初始化。典型場(chǎng)景包括 new 實(shí)例化對(duì)象、讀取或設(shè)置靜態(tài)字段、調(diào)用靜態(tài)方法。

② 對(duì)類反射調(diào)用時(shí),還未初始化。

③ 初始化類時(shí),父類還未初始化。

④ 虛擬機(jī)啟動(dòng)時(shí),會(huì)先初始化包含 main 方法的主類。

⑤ 使用 JDK7 的動(dòng)態(tài)語言支持時(shí),如果 MethodHandle 實(shí)例的解析結(jié)果為指定類型的方法句柄且句柄對(duì)應(yīng)的類還未初始化。

⑥ 接口定義了默認(rèn)方法,如果接口的實(shí)現(xiàn)類初始化,接口要在其之前初始化。

其余所有引用類型的方式都不會(huì)觸發(fā)初始化,稱為被動(dòng)引用。被動(dòng)引用實(shí)例:① 子類使用父類的靜態(tài)字段時(shí),只有父類被初始化。② 通過數(shù)組定義使用類。③ 常量在編譯期會(huì)存入調(diào)用類的常量池,不會(huì)初始化定義常量的類。

接口和類加載過程的區(qū)別:初始化類時(shí)如果父類沒有初始化需要初始化父類,但接口初始化時(shí)不要求父接口初始化,只有在真正使用父接口時(shí)(如引用接口中定義的常量)才會(huì)初始化。

類加載的過程是什么?

加載

該階段虛擬機(jī)需要完成三件事:① 通過一個(gè)類的全限定類名獲取定義類的二進(jìn)制字節(jié)流。② 將字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)區(qū)。③ 在內(nèi)存中生成對(duì)應(yīng)該類的 Class 實(shí)例,作為方法區(qū)這個(gè)類的數(shù)據(jù)訪問入口。

驗(yàn)證

確保 Class 文件的字節(jié)流符合約束。如果虛擬機(jī)不檢查輸入的字節(jié)流,可能因?yàn)檩d入有錯(cuò)誤或惡意企圖的字節(jié)流而導(dǎo)致系統(tǒng)受攻擊。驗(yàn)證主要包含四個(gè)階段:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證、符號(hào)引用驗(yàn)證。

驗(yàn)證重要但非必需,因?yàn)橹挥型ㄟ^與否的區(qū)別,通過后對(duì)程序運(yùn)行期沒有任何影響。如果代碼已被反復(fù)使用和驗(yàn)證過,在生產(chǎn)環(huán)境就可以考慮關(guān)閉大部分驗(yàn)證縮短類加載時(shí)間。

準(zhǔn)備

為類靜態(tài)變量分配內(nèi)存并設(shè)置零值,該階段進(jìn)行的內(nèi)存分配僅包括類變量,不包括實(shí)例變量。如果變量被 final 修飾,編譯時(shí) Javac 會(huì)為變量生成 ConstantValue 屬性,準(zhǔn)備階段虛擬機(jī)會(huì)將變量值設(shè)為代碼值。

解析

將常量池內(nèi)的符號(hào)引用替換為直接引用。

符號(hào)引用以一組符號(hào)描述引用目標(biāo),可以是任何形式的字面量,只要使用時(shí)能無歧義地定位目標(biāo)即可。與虛擬機(jī)內(nèi)存布局無關(guān),引用目標(biāo)不一定已經(jīng)加載到虛擬機(jī)內(nèi)存。

直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或能間接定位到目標(biāo)的句柄。和虛擬機(jī)的內(nèi)存布局相關(guān),引用目標(biāo)必須已在虛擬機(jī)的內(nèi)存中存在。

初始化

直到該階段 JVM 才開始執(zhí)行類中編寫的代碼。準(zhǔn)備階段時(shí)變量賦過零值,初始化階段會(huì)根據(jù)程序員的編碼去初始化類變量和其他資源。初始化階段就是執(zhí)行類構(gòu)造方法中的 <client> 方法,該方法是 Javac 自動(dòng)生成的。

有哪些類加載器?

自 JDK1.2 起 Java 一直保持三層類加載器:

  • 啟動(dòng)類加載器

    在 JVM 啟動(dòng)時(shí)創(chuàng)建,負(fù)責(zé)加載最核心的類,例如 Object、System 等。無法被程序直接引用,如果需要把加載委派給啟動(dòng)類加載器,直接使用 null 代替即可,因?yàn)閱?dòng)類加載器通常由操作系統(tǒng)實(shí)現(xiàn),并不存在于 JVM 體系。

  • 平臺(tái)類加載器

    從 JDK9 開始從擴(kuò)展類加載器更換為平臺(tái)類加載器,負(fù)載加載一些擴(kuò)展的系統(tǒng)類,比如 XML、加密、壓縮相關(guān)的功能類等。

  • 應(yīng)用類加載器

    也稱系統(tǒng)類加載器,負(fù)責(zé)加載用戶類路徑上的類庫,可以直接在代碼中使用。如果沒有自定義類加載器,一般情況下應(yīng)用類加載器就是默認(rèn)的類加載器。自定義類加載器通過繼承 ClassLoader 并重寫 findClass 方法實(shí)現(xiàn)。

雙親委派模型是什么?

類加載器具有等級(jí)制度但非繼承關(guān)系,以組合的方式復(fù)用父加載器的功能。雙親委派模型要求除了頂層的啟動(dòng)類加載器外,其余類加載器都應(yīng)該有自己的父加載器。

一個(gè)類加載器收到了類加載請(qǐng)求,它不會(huì)自己去嘗試加載,而將該請(qǐng)求委派給父加載器,每層的類加載器都是如此,因此所有加載請(qǐng)求最終都應(yīng)該傳送到啟動(dòng)類加載器,只有當(dāng)父加載器反饋無法完成請(qǐng)求時(shí),子加載器才會(huì)嘗試。

類跟隨它的加載器一起具備了有優(yōu)先級(jí)的層次關(guān)系,確保某個(gè)類在各個(gè)類加載器環(huán)境中都是同一個(gè),保證程序的穩(wěn)定性。

如何判斷兩個(gè)類是否相等?

任意一個(gè)類都必須由類加載器和這個(gè)類本身共同確立其在虛擬機(jī)中的唯一性。

兩個(gè)類只有由同一類加載器加載才有比較意義,否則即使兩個(gè)類來源于同一個(gè) Class 文件,被同一個(gè) JVM 加載,只要類加載器不同,這兩個(gè)類就必定不相等。

JMM

JMM 的作用是什么?

Java 線程的通信由 JMM 控制,JMM 的主要目的是定義程序中各種變量的訪問規(guī)則。變量包括實(shí)例字段、靜態(tài)字段,但不包括局部變量與方法參數(shù),因?yàn)樗鼈兪蔷€程私有的,不存在多線程競(jìng)爭(zhēng)。JMM 遵循一個(gè)基本原則:只要不改變程序執(zhí)行結(jié)果,編譯器和處理器怎么優(yōu)化都行。例如編譯器分析某個(gè)鎖只會(huì)單線程訪問就消除鎖,某個(gè) volatile 變量只會(huì)單線程訪問就把它當(dāng)作普通變量。

JMM 規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,每條線程有自己的工作內(nèi)存,工作內(nèi)存中保存被該線程使用的變量的主內(nèi)存副本,線程對(duì)變量的所有操作都必須在工作空間進(jìn)行,不能直接讀寫主內(nèi)存數(shù)據(jù)。不同線程間無法直接訪問對(duì)方工作內(nèi)存中的變量,線程通信必須經(jīng)過主內(nèi)存。

關(guān)于主內(nèi)存與工作內(nèi)存的交互,即變量如何從主內(nèi)存拷貝到工作內(nèi)存、從工作內(nèi)存同步回主內(nèi)存,JMM 定義了 8 種原子操作:

操作 作用變量范圍 作用
lock 主內(nèi)存 把變量標(biāo)識(shí)為線程獨(dú)占狀態(tài)
unlock 主內(nèi)存 釋放處于鎖定狀態(tài)的變量
read 主內(nèi)存 把變量值從主內(nèi)存?zhèn)鞯焦ぷ鲀?nèi)存
load 工作內(nèi)存 把 read 得到的值放入工作內(nèi)存的變量副本
user 工作內(nèi)存 把工作內(nèi)存中的變量值傳給執(zhí)行引擎
assign 工作內(nèi)存 把從執(zhí)行引擎接收的值賦給工作內(nèi)存變量
store 工作內(nèi)存 把工作內(nèi)存的變量值傳到主內(nèi)存
write 主內(nèi)存 把 store 取到的變量值放入主內(nèi)存變量中

as-if-serial 是什么?

不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能改變,編譯器和處理器必須遵循 as-if-serial 語義。

為了遵循 as-if-serial,編譯器和處理器不會(huì)對(duì)存在數(shù)據(jù)依賴關(guān)系的操作重排序,因?yàn)檫@種重排序會(huì)改變執(zhí)行結(jié)果。但是如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序

as-if-serial 把單線程程序保護(hù)起來,給程序員一種幻覺:?jiǎn)尉€程程序是按程序的順序執(zhí)行的。

happens-before 是什么?

先行發(fā)生原則,JMM 定義的兩項(xiàng)操作間的偏序關(guān)系,是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)的重要手段。

JMM 將 happens-before 要求禁止的重排序按是否會(huì)改變程序執(zhí)行結(jié)果分為兩類。對(duì)于會(huì)改變結(jié)果的重排序 JMM 要求編譯器和處理器必須禁止,對(duì)于不會(huì)改變結(jié)果的重排序,JMM 不做要求。

JMM 存在一些天然的 happens-before 關(guān)系,無需任何同步器協(xié)助就已經(jīng)存在。如果兩個(gè)操作的關(guān)系不在此列,并且無法從這些規(guī)則推導(dǎo)出來,它們就沒有順序性保障,虛擬機(jī)可以對(duì)它們隨意進(jìn)行重排序

  • 程序次序規(guī)則:一個(gè)線程內(nèi)寫在前面的操作先行發(fā)生于后面的。
  • 管程鎖定規(guī)則: unlock 操作先行發(fā)生于后面對(duì)同一個(gè)鎖的 lock 操作。
  • volatile 規(guī)則:對(duì) volatile 變量的寫操作先行發(fā)生于后面的讀操作。
  • 線程啟動(dòng)規(guī)則:線程的 start 方法先行發(fā)生于線程的每個(gè)動(dòng)作。
  • 線程終止規(guī)則:線程中所有操作先行發(fā)生于對(duì)線程的終止檢測(cè)。
  • 對(duì)象終結(jié)規(guī)則:對(duì)象的初始化先行發(fā)生于 finalize 方法。
  • 傳遞性:如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那么操作 A 先行發(fā)生于操作 C 。

as-if-serial 和 happens-before 有什么區(qū)別?

as-if-serial 保證單線程程序的執(zhí)行結(jié)果不變,happens-before 保證正確同步的多線程程序的執(zhí)行結(jié)果不變。

這兩種語義的目的都是為了在不改變程序執(zhí)行結(jié)果的前提下盡可能提高程序執(zhí)行并行度。

什么是指令重排序

為了提高性能,編譯器和處理器通常會(huì)對(duì)指令進(jìn)行重排序,重排序指從源代碼到指令序列的重排序,分為三種:① 編譯器優(yōu)化的重排序,編譯器在不改變單線程程序語義的前提下可以重排語句的執(zhí)行順序。② 指令級(jí)并行的重排序,如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序。③ 內(nèi)存系統(tǒng)的重排序

原子性、可見性、有序性分別是什么?

原子性

基本數(shù)據(jù)類型的訪問都具備原子性,例外就是 long 和 double,虛擬機(jī)將沒有被 volatile 修飾的 64 位數(shù)據(jù)操作劃分為兩次 32 位操作。

如果應(yīng)用場(chǎng)景需要更大范圍的原子性保證,JMM 還提供了 lock 和 unlock 操作滿足需求,盡管 JVM 沒有把這兩種操作直接開放給用戶使用,但是提供了更高層次的字節(jié)碼指令 monitorenter 和 monitorexit,這兩個(gè)字節(jié)碼指令反映到 Java 代碼中就是 synchronized。

可見性

可見性指當(dāng)一個(gè)線程修改了共享變量時(shí),其他線程能夠立即得知修改。JMM 通過在變量修改后將值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新的方式實(shí)現(xiàn)可見性,無論普通變量還是 volatile 變量都是如此,區(qū)別是 volatile 保證新值能立即同步到主內(nèi)存以及每次使用前立即從主內(nèi)存刷新。

除了 volatile 外,synchronized 和 final 也可以保證可見性。同步塊可見性由"對(duì)一個(gè)變量執(zhí)行 unlock 前必須先把此變量同步回主內(nèi)存,即先執(zhí)行 store 和 write"這條規(guī)則獲得。final 的可見性指:被 final 修飾的字段在構(gòu)造方法中一旦初始化完成,并且構(gòu)造方法沒有把 this 引用傳遞出去,那么其他線程就能看到 final 字段的值。

有序性

有序性可以總結(jié)為:在本線程內(nèi)觀察所有操作是有序的,在一個(gè)線程內(nèi)觀察另一個(gè)線程,所有操作都是無序的。前半句指 as-if-serial 語義,后半句指指令重排序和工作內(nèi)存與主內(nèi)存延遲現(xiàn)象。

Java 提供 volatile 和 synchronized 保證有序性,volatile 本身就包含禁止指令重排序的語義,而 synchronized 保證一個(gè)變量在同一時(shí)刻只允許一條線程對(duì)其進(jìn)行 lock 操作,確保持有同一個(gè)鎖的兩個(gè)同步塊只能串行進(jìn)入。

談一談 volatile

JMM 為 volatile 定義了一些特殊訪問規(guī)則,當(dāng)變量被定義為 volatile 后具備兩種特性:

  • 保證變量對(duì)所有線程可見

    當(dāng)一條線程修改了變量值,新值對(duì)于其他線程來說是立即可以得知的。volatile 變量在各個(gè)線程的工作內(nèi)存中不存在一致性問題,但 Java 的運(yùn)算操作符并非原子操作,導(dǎo)致 volatile 變量運(yùn)算在并發(fā)下仍不安全。

  • 禁止指令重排序優(yōu)化

    使用 volatile 變量進(jìn)行寫操作,匯編指令帶有 lock 前綴,相當(dāng)于一個(gè)內(nèi)存屏障,后面的指令不能重排到內(nèi)存屏障之前。

    使用 lock 前綴引發(fā)兩件事:① 將當(dāng)前處理器緩存行的數(shù)據(jù)寫回系統(tǒng)內(nèi)存。②使其他處理器的緩存無效。相當(dāng)于對(duì)緩存變量做了一次 store 和 write 操作,讓 volatile 變量的修改對(duì)其他處理器立即可見。

靜態(tài)變量 i 執(zhí)行多線程 i++ 的不安全問題

自增語句由 4 條字節(jié)碼指令構(gòu)成的,依次為 getstaticiconst_1iaddputstatic,當(dāng) getstatic 把 i 的值取到操作棧頂時(shí),volatile 保證了 i 值在此刻正確,但在執(zhí)行 iconst_1iadd 時(shí),其他線程可能已經(jīng)改變了 i 值,操作棧頂?shù)闹稻妥兂闪诉^期數(shù)據(jù),所以 putstatic 執(zhí)行后就可能把較小的 i 值同步回了主內(nèi)存。

適用場(chǎng)景

① 運(yùn)算結(jié)果并不依賴變量的當(dāng)前值。② 一寫多讀,只有單一的線程修改變量值。

內(nèi)存語義

寫一個(gè) volatile 變量時(shí),把該線程工作內(nèi)存中的值刷新到主內(nèi)存。

讀一個(gè) volatile 變量時(shí),把該線程工作內(nèi)存值置為無效,從主內(nèi)存讀取。

指令重排序特點(diǎn)

第二個(gè)操作是 volatile 寫,不管第一個(gè)操作是什么都不能重排序,確保寫之前的操作不會(huì)被重排序到寫之后。

第一個(gè)操作是 volatile 讀,不管第二個(gè)操作是什么都不能重排序,確保讀之后的操作不會(huì)被重排序到讀之前。

第一個(gè)操作是 volatile 寫,第二個(gè)操作是 volatile 讀不能重排序

JSR-133 增強(qiáng) volatile 語義的原因

在舊的內(nèi)存模型中,雖然不允許 volatile 變量間重排序,但允許 volatile 變量與普通變量重排序,可能導(dǎo)致內(nèi)存不可見問題。JSR-133 嚴(yán)格限制編譯器和處理器對(duì) volatile 變量與普通變量的重排序,確保 volatile 的寫-讀和鎖的釋放-獲取具有相同的內(nèi)存語義。

final 可以保證可見性嗎?

final 可以保證可見性,被 final 修飾的字段在構(gòu)造方法中一旦被初始化完成,并且構(gòu)造方法沒有把 this 引用傳遞出去,在其他線程中就能看見 final 字段值。

在舊的 JMM 中,一個(gè)嚴(yán)重缺陷是線程可能看到 final 值改變。比如一個(gè)線程看到一個(gè) int 類型 final 值為 0,此時(shí)該值是未初始化前的零值,一段時(shí)間后該值被某線程初始化,再去讀這個(gè) final 值會(huì)發(fā)現(xiàn)值變?yōu)?1。

為修復(fù)該漏洞,JSR-133 為 final 域增加重排序規(guī)則:只要對(duì)象是正確構(gòu)造的(被構(gòu)造對(duì)象的引用在構(gòu)造方法中沒有逸出),那么不需要使用同步就可以保證任意線程都能看到這個(gè) final 域初始化后的值。

寫 final 域重排序規(guī)則

禁止把 final 域的寫重排序到構(gòu)造方法之外,編譯器會(huì)在 final 域的寫后,構(gòu)造方法的 return 前,插入一個(gè) Store Store 屏障。確保在對(duì)象引用為任意線程可見之前,對(duì)象的 final 域已經(jīng)初始化過。

讀 final 域重排序規(guī)則

在一個(gè)線程中,初次讀對(duì)象引用和初次讀該對(duì)象包含的 final 域,JMM 禁止處理器重排序這兩個(gè)操作。編譯器在讀 final 域操作的前面插入一個(gè) Load Load 屏障,確保在讀一個(gè)對(duì)象的 final 域前一定會(huì)先讀包含這個(gè) final 域的對(duì)象引用。


談一談 synchronized

每個(gè) Java 對(duì)象都有一個(gè)關(guān)聯(lián)的 monitor,使用 synchronized 時(shí) JVM 會(huì)根據(jù)使用環(huán)境找到對(duì)象的 monitor,根據(jù) monitor 的狀態(tài)進(jìn)行加解鎖的判斷。如果成功加鎖就成為該 monitor 的唯一持有者,monitor 在被釋放前不能再被其他線程獲取。

同步代碼塊使用 monitorenter 和 monitorexit 這兩個(gè)字節(jié)碼指令獲取和釋放 monitor。這兩個(gè)字節(jié)碼指令都需要一個(gè)引用類型的參數(shù)指明要鎖定和解鎖的對(duì)象,對(duì)于同步普通方法,鎖是當(dāng)前實(shí)例對(duì)象;對(duì)于靜態(tài)同步方法,鎖是當(dāng)前類的 Class 對(duì)象;對(duì)于同步方法塊,鎖是 synchronized 括號(hào)里的對(duì)象。

執(zhí)行 monitorenter 指令時(shí),首先嘗試獲取對(duì)象鎖。如果這個(gè)對(duì)象沒有被鎖定,或當(dāng)前線程已經(jīng)持有鎖,就把鎖的計(jì)數(shù)器加 1,執(zhí)行 monitorexit 指令時(shí)會(huì)將鎖計(jì)數(shù)器減 1。一旦計(jì)數(shù)器為 0 鎖隨即就被釋放。

例如有兩個(gè)線程 A、B 競(jìng)爭(zhēng) monitor,當(dāng) A 競(jìng)爭(zhēng)到鎖時(shí)會(huì)將 monitor 中的 owner 設(shè)置為 A,把 B 阻塞并放到等待資源的 ContentionList 隊(duì)列。ContentionList 中的部分線程會(huì)進(jìn)入 EntryList,EntryList 中的線程會(huì)被指定為 OnDeck 競(jìng)爭(zhēng)候選者,如果獲得了鎖資源將進(jìn)入 Owner 狀態(tài),釋放鎖后進(jìn)入 !Owner 狀態(tài)。被阻塞的線程會(huì)進(jìn)入 WaitSet。

被 synchronized 修飾的同步塊對(duì)一條線程來說是可重入的,并且同步塊在持有鎖的線程釋放鎖前會(huì)阻塞其他線程進(jìn)入。從執(zhí)行成本的角度看,持有鎖是一個(gè)重量級(jí)的操作。Java 線程是映射到操作系統(tǒng)的內(nèi)核線程上的,如果要阻塞或喚醒一條線程,需要操作系統(tǒng)幫忙完成,不可避免用戶態(tài)到核心態(tài)的轉(zhuǎn)換。

不公平的原因

所有收到鎖請(qǐng)求的線程首先自旋,如果通過自旋也沒有獲取鎖將被放入 ContentionList,該做法對(duì)于已經(jīng)進(jìn)入隊(duì)列的線程不公平。

為了防止 ContentionList 尾部的元素被大量線程進(jìn)行 CAS 訪問影響性能,Owner 線程會(huì)在釋放鎖時(shí)將 ContentionList 的部分線程移動(dòng)到 EntryList 并指定某個(gè)線程為 OnDeck 線程,該行為叫做競(jìng)爭(zhēng)切換,犧牲了公平性但提高了性能。


鎖優(yōu)化有哪些策略?

JDK 6 對(duì) synchronized 做了很多優(yōu)化,引入了自適應(yīng)自旋、鎖消除、鎖粗化、偏向鎖和輕量級(jí)鎖等提高鎖的效率,鎖一共有 4 個(gè)狀態(tài),級(jí)別從低到高依次是:無鎖、偏向鎖、輕量級(jí)鎖和重量級(jí)鎖,狀態(tài)會(huì)隨競(jìng)爭(zhēng)情況升級(jí)。鎖可以升級(jí)但不能降級(jí),這種只能升級(jí)不能降級(jí)的鎖策略是為了提高鎖獲得和釋放的效率。


自旋鎖是什么?

同步對(duì)性能最大的影響是阻塞,掛起和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)完成。許多應(yīng)用上共享數(shù)據(jù)的鎖定只會(huì)持續(xù)很短的時(shí)間,為了這段時(shí)間去掛起和恢復(fù)線程并不值得。如果機(jī)器有多個(gè)處理器核心,我們可以讓后面請(qǐng)求鎖的線程稍等一會(huì),但不放棄處理器的執(zhí)行時(shí)間,看看持有鎖的線程是否很快會(huì)釋放鎖。為了讓線程等待只需讓線程執(zhí)行一個(gè)忙循環(huán),這項(xiàng)技術(shù)就是自旋鎖。

自旋鎖在 JDK1.4 就已引入,默認(rèn)關(guān)閉,在 JDK6 中改為默認(rèn)開啟。自旋不能代替阻塞,雖然避免了線程切換開銷,但要占用處理器時(shí)間,如果鎖被占用的時(shí)間很短,自旋的效果就會(huì)非常好,反之只會(huì)白白消耗處理器資源。如果自旋超過了限定的次數(shù)仍然沒有成功獲得鎖,就應(yīng)掛起線程,自旋默認(rèn)限定次數(shù)是 10。

什么是自適應(yīng)自旋?

JDK6 對(duì)自旋鎖進(jìn)行了優(yōu)化,自旋時(shí)間不再固定,而是由前一次的自旋時(shí)間及鎖擁有者的狀態(tài)決定。

如果在同一個(gè)鎖上,自旋剛剛成功獲得過鎖且持有鎖的線程正在運(yùn)行,虛擬機(jī)會(huì)認(rèn)為這次自旋也很可能成功,進(jìn)而允許自旋持續(xù)更久。如果自旋很少成功,以后獲取鎖時(shí)將可能直接省略掉自旋,避免浪費(fèi)處理器資源。

有了自適應(yīng)自旋,隨著程序運(yùn)行時(shí)間的增長,虛擬機(jī)對(duì)程序鎖的狀況預(yù)測(cè)就會(huì)越來越精準(zhǔn)。

鎖消除是什么?

鎖消除指即時(shí)編譯器對(duì)檢測(cè)到不可能存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖進(jìn)行消除。

主要判定依據(jù)來源于逃逸分析,如果判斷一段代碼中堆上的所有數(shù)據(jù)都只被一個(gè)線程訪問,就可以當(dāng)作棧上的數(shù)據(jù)對(duì)待,認(rèn)為它們是線程私有的而無須同步。

鎖粗化是什么?

原則需要將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實(shí)際作用域中進(jìn)行同步,這是為了使等待鎖的線程盡快拿到鎖。

但如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體之外的,即使沒有線程競(jìng)爭(zhēng)也會(huì)導(dǎo)致不必要的性能消耗。因此如果虛擬機(jī)探測(cè)到有一串零碎的操作都對(duì)同一個(gè)對(duì)象加鎖,將會(huì)把同步的范圍擴(kuò)展到整個(gè)操作序列的外部。

偏向鎖是什么?

偏向鎖是為了在沒有競(jìng)爭(zhēng)的情況下減少鎖開銷,鎖會(huì)偏向于第一個(gè)獲得它的線程,如果在執(zhí)行過程中鎖一直沒有被其他線程獲取,則持有偏向鎖的線程將不需要進(jìn)行同步。

當(dāng)鎖對(duì)象第一次被線程獲取時(shí),虛擬機(jī)會(huì)將對(duì)象頭中的偏向模式設(shè)為 1,同時(shí)使用 CAS 把獲取到鎖的線程 ID 記錄在對(duì)象的 Mark Word 中。如果 CAS 成功,持有偏向鎖的線程以后每次進(jìn)入鎖相關(guān)的同步塊都不再進(jìn)行任何同步操作。

一旦有其他線程嘗試獲取鎖,偏向模式立即結(jié)束,根據(jù)鎖對(duì)象是否處于鎖定狀態(tài)決定是否撤銷偏向,后續(xù)同步按照輕量級(jí)鎖那樣執(zhí)行。

輕量級(jí)鎖是什么?

輕量級(jí)鎖是為了在沒有競(jìng)爭(zhēng)的前提下減少重量級(jí)鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。

在代碼即將進(jìn)入同步塊時(shí),如果同步對(duì)象沒有被鎖定,虛擬機(jī)將在當(dāng)前線程的棧幀中建立一個(gè)鎖記錄空間,存儲(chǔ)鎖對(duì)象目前 Mark Word 的拷貝。然后虛擬機(jī)使用 CAS 嘗試把對(duì)象的 Mark Word 更新為指向鎖記錄的指針,如果更新成功即代表該線程擁有了鎖,鎖標(biāo)志位將轉(zhuǎn)變?yōu)?00,表示處于輕量級(jí)鎖定狀態(tài)。

如果更新失敗就意味著至少存在一條線程與當(dāng)前線程競(jìng)爭(zhēng)。虛擬機(jī)檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是則說明當(dāng)前線程已經(jīng)擁有了鎖,直接進(jìn)入同步塊繼續(xù)執(zhí)行,否則說明鎖對(duì)象已經(jīng)被其他線程搶占。如果出現(xiàn)兩條以上線程爭(zhēng)用同一個(gè)鎖,輕量級(jí)鎖就不再有效,將膨脹為重量級(jí)鎖,鎖標(biāo)志狀態(tài)變?yōu)?10,此時(shí)Mark Word 存儲(chǔ)的就是指向重量級(jí)鎖的指針,后面等待鎖的線程也必須阻塞。

解鎖同樣通過 CAS 進(jìn)行,如果對(duì)象 Mark Word 仍然指向線程的鎖記錄,就用 CAS 把對(duì)象當(dāng)前的 Mark Word 和線程復(fù)制的 Mark Word 替換回來。假如替換成功同步過程就順利完成了,如果失敗則說明有其他線程嘗試過獲取該鎖,就要在釋放鎖的同時(shí)喚醒被掛起的線程。

偏向鎖、輕量級(jí)鎖和重量級(jí)鎖的區(qū)別?

偏向鎖的優(yōu)點(diǎn)是加解鎖不需要額外消耗,和執(zhí)行非同步方法比僅存在納秒級(jí)差距,缺點(diǎn)是如果存在鎖競(jìng)爭(zhēng)會(huì)帶來額外鎖撤銷的消耗,適用只有一個(gè)線程訪問同步代碼塊的場(chǎng)景。

輕量級(jí)鎖的優(yōu)點(diǎn)是競(jìng)爭(zhēng)線程不阻塞,程序響應(yīng)速度快,缺點(diǎn)是如果線程始終得不到鎖會(huì)自旋消耗 CPU,適用追求響應(yīng)時(shí)間、同步代碼塊執(zhí)行快的場(chǎng)景。

重量級(jí)鎖的優(yōu)點(diǎn)是線程競(jìng)爭(zhēng)不使用自旋不消耗CPU,缺點(diǎn)是線程會(huì)阻塞,響應(yīng)時(shí)間慢,適應(yīng)追求吞吐量、同步代碼塊執(zhí)行慢的場(chǎng)景。

Lock 和 synchronized 有什么區(qū)別?

Lock 接是 juc 包的頂層接口,基于Lock 接口,用戶能夠以非塊結(jié)構(gòu)來實(shí)現(xiàn)互斥同步,擺脫了語言特性束縛,在類庫層面實(shí)現(xiàn)同步。Lock 并未用到 synchronized,而是利用了 volatile 的可見性。

重入鎖 ReentrantLock 是 Lock 最常見的實(shí)現(xiàn),與 synchronized 一樣可重入,不過它增加了一些高級(jí)功能:

  • **等待可中斷: **持有鎖的線程長期不釋放鎖時(shí),正在等待的線程可以選擇放棄等待而處理其他事情。
  • 公平鎖: 公平鎖指多個(gè)線程在等待同一個(gè)鎖時(shí),必須按照申請(qǐng)鎖的順序來依次獲得鎖,而非公平鎖不保證這一點(diǎn),在鎖被釋放時(shí),任何線程都有機(jī)會(huì)獲得鎖。synchronized 是非公平的,ReentrantLock 在默認(rèn)情況下是非公平的,可以通過構(gòu)造方法指定公平鎖。一旦使用了公平鎖,性能會(huì)急劇下降,影響吞吐量。
  • 鎖綁定多個(gè)條件: 一個(gè) ReentrantLock 可以同時(shí)綁定多個(gè) Condition。synchronized 中鎖對(duì)象的 waitnotify 可以實(shí)現(xiàn)一個(gè)隱含條件,如果要和多個(gè)條件關(guān)聯(lián)就不得不額外添加鎖,而 ReentrantLock 可以多次調(diào)用 newCondition 創(chuàng)建多個(gè)條件。

一般優(yōu)先考慮使用 synchronized:① synchronized 是語法層面的同步,足夠簡(jiǎn)單。② Lock 必須確保在 finally 中釋放鎖,否則一旦拋出異常有可能永遠(yuǎn)不會(huì)釋放鎖。使用 synchronized 可以由 JVM 來確保即使出現(xiàn)異常鎖也能正常釋放。③ 盡管 JDK5 時(shí) ReentrantLock 的性能優(yōu)于 synchronized,但在 JDK6 進(jìn)行鎖優(yōu)化后二者的性能基本持平。從長遠(yuǎn)來看 JVM 更容易針對(duì)synchronized 優(yōu)化,因?yàn)?JVM 可以在線程和對(duì)象的元數(shù)據(jù)中記錄 synchronized 中鎖的相關(guān)信息,而使用 Lock 的話 JVM 很難得知具體哪些鎖對(duì)象是由特定線程持有的。

ReentrantLock 的可重入是怎么實(shí)現(xiàn)的?

以非公平鎖為例,通過 nonfairTryAcquire 方法獲取鎖,該方法增加了再次獲取同步狀態(tài)的處理邏輯:判斷當(dāng)前線程是否為獲取鎖的線程來決定獲取是否成功,如果是獲取鎖的線程再次請(qǐng)求則將同步狀態(tài)值增加并返回 true,表示獲取同步狀態(tài)成功。

成功獲取鎖的線程再次獲取鎖將增加同步狀態(tài)值,釋放同步狀態(tài)時(shí)將減少同步狀態(tài)值。如果鎖被獲取了 n 次,那么前 n-1 次 tryRelease 方法必須都返回 fasle,只有同步狀態(tài)完全釋放才能返回 true,該方法將同步狀態(tài)是否為 0 作為最終釋放條件,釋放時(shí)將占有線程設(shè)置為null 并返回 true。

對(duì)于非公平鎖只要 CAS 設(shè)置同步狀態(tài)成功則表示當(dāng)前線程獲取了鎖,而公平鎖則不同。公平鎖使用 tryAcquire 方法,該方法與nonfairTryAcquire 的唯一區(qū)別就是判斷條件中多了對(duì)同步隊(duì)列中當(dāng)前節(jié)點(diǎn)是否有前驅(qū)節(jié)點(diǎn)的判斷,如果該方法返回 true 表示有線程比當(dāng)前線程更早請(qǐng)求鎖,因此需要等待前驅(qū)線程獲取并釋放鎖后才能獲取鎖。

什么是讀寫鎖?

ReentrantLock 是排他鎖,同一時(shí)刻只允許一個(gè)線程訪問,讀寫鎖在同一時(shí)刻允許多個(gè)讀線程訪問,在寫線程訪問時(shí),所有的讀寫線程均阻塞。讀寫鎖維護(hù)了一個(gè)讀鎖和一個(gè)寫鎖,通過分離讀寫鎖使并發(fā)性相比排他鎖有了很大提升。

讀寫鎖依賴 AQS 來實(shí)現(xiàn)同步功能,讀寫狀態(tài)就是其同步器的同步狀態(tài)。讀寫鎖的自定義同步器需要在同步狀態(tài),即一個(gè) int 變量上維護(hù)多個(gè)讀線程和一個(gè)寫線程的狀態(tài)。讀寫鎖將變量切分成了兩個(gè)部分,高 16 位表示讀,低 16 位表示寫。

寫鎖是可重入排他鎖,如果當(dāng)前線程已經(jīng)獲得了寫鎖則增加寫狀態(tài),如果當(dāng)前線程在獲取寫鎖時(shí),讀鎖已經(jīng)被獲取或者該線程不是已經(jīng)獲得寫鎖的線程則進(jìn)入等待。寫鎖的釋放與 ReentrantLock 的釋放類似,每次釋放減少寫狀態(tài),當(dāng)寫狀態(tài)為 0 時(shí)表示寫鎖已被釋放。

讀鎖是可重入共享鎖,能夠被多個(gè)線程同時(shí)獲取,在沒有其他寫線程訪問時(shí),讀鎖總會(huì)被成功獲取。如果當(dāng)前線程已經(jīng)獲取了讀鎖,則增加讀狀態(tài)。如果當(dāng)前線程在獲取讀鎖時(shí),寫鎖已被其他線程獲取則進(jìn)入等待。讀鎖每次釋放會(huì)減少讀狀態(tài),減少的值是(1<<16),讀鎖的釋放是線程安全的。

鎖降級(jí)指把持住當(dāng)前擁有的寫鎖,再獲取讀鎖,隨后釋放先前擁有的寫鎖。

鎖降級(jí)中讀鎖的獲取是必要的,這是為了保證數(shù)據(jù)可見性,如果當(dāng)前線程不獲取讀鎖而直接釋放寫鎖,假設(shè)此刻另一個(gè)線程 A 獲取寫鎖修改了數(shù)據(jù),當(dāng)前線程無法感知線程 A 的數(shù)據(jù)更新。如果當(dāng)前線程獲取讀鎖,遵循鎖降級(jí)的步驟,A 將被阻塞,直到當(dāng)前線程使用數(shù)據(jù)并釋放讀鎖之后,線程 A 才能獲取寫鎖進(jìn)行數(shù)據(jù)更新。

AQS 了解嗎?

AQS 隊(duì)列同步器是用來構(gòu)建鎖或其他同步組件的基礎(chǔ)框架,它使用一個(gè) volatile int state 變量作為共享資源,如果線程獲取資源失敗,則進(jìn)入同步隊(duì)列等待;如果獲取成功就執(zhí)行臨界區(qū)代碼,釋放資源時(shí)會(huì)通知同步隊(duì)列中的等待線程。

同步器的主要使用方式是繼承,子類通過繼承同步器并實(shí)現(xiàn)它的抽象方法來管理同步狀態(tài),對(duì)同步狀態(tài)進(jìn)行更改需要使用同步器提供的 3個(gè)方法 getStatesetStatecompareAndSetState ,它們保證狀態(tài)改變是安全的。子類推薦被定義為自定義同步組件的靜態(tài)內(nèi)部類,同步器自身沒有實(shí)現(xiàn)任何同步接口,它僅僅定義若干同步狀態(tài)獲取和釋放的方法,同步器既支持獨(dú)占式也支持共享式。

同步器是實(shí)現(xiàn)鎖的關(guān)鍵,在鎖的實(shí)現(xiàn)中聚合同步器,利用同步器實(shí)現(xiàn)鎖的語義。鎖面向使用者,定義了使用者與鎖交互的接口,隱藏實(shí)現(xiàn)細(xì)節(jié);同步器面向鎖的實(shí)現(xiàn)者,簡(jiǎn)化了鎖的實(shí)現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程排隊(duì)、等待與喚醒等底層操作。

每當(dāng)有新線程請(qǐng)求資源時(shí)都會(huì)進(jìn)入一個(gè)等待隊(duì)列,只有當(dāng)持有鎖的線程釋放鎖資源后該線程才能持有資源。等待隊(duì)列通過雙向鏈表實(shí)現(xiàn),線程被封裝在鏈表的 Node 節(jié)點(diǎn)中,Node 的等待狀態(tài)包括:CANCELLED(線程已取消)、SIGNAL(線程需要喚醒)、CONDITION (線程正在等待)、PROPAGATE(后繼節(jié)點(diǎn)會(huì)傳播喚醒操作,只在共享模式下起作用)。

AQS 有哪兩種模式?

獨(dú)占模式表示鎖只會(huì)被一個(gè)線程占用,其他線程必須等到持有鎖的線程釋放鎖后才能獲取鎖,同一時(shí)間只能有一個(gè)線程獲取到鎖。

共享模式表示多個(gè)線程獲取同一個(gè)鎖有可能成功,ReadLock 就采用共享模式。

獨(dú)占模式通過 acquire 和 release 方法獲取和釋放鎖,共享模式通過 acquireShared 和 releaseShared 方法獲取和釋放鎖。

:AQS 獨(dú)占式獲取/釋放鎖的原理?

獲取同步狀態(tài)時(shí),調(diào)用 acquire 方法,維護(hù)一個(gè)同步隊(duì)列,使用 tryAcquire 方法安全地獲取線程同步狀態(tài),獲取失敗的線程會(huì)被構(gòu)造同步節(jié)點(diǎn)并通過 addWaiter 方法加入到同步隊(duì)列的尾部,在隊(duì)列中自旋。之后調(diào)用 acquireQueued 方法使得該節(jié)點(diǎn)以死循環(huán)的方式獲取同步狀態(tài),如果獲取不到則阻塞,被阻塞線程的喚醒主要依靠前驅(qū)節(jié)點(diǎn)的出隊(duì)或被中斷實(shí)現(xiàn),移出隊(duì)列或停止自旋的條件是前驅(qū)節(jié)點(diǎn)是頭結(jié)點(diǎn)且成功獲取了同步狀態(tài)。

釋放同步狀態(tài)時(shí),同步器調(diào)用 tryRelease 方法釋放同步狀態(tài),然后調(diào)用 unparkSuccessor 方法喚醒頭節(jié)點(diǎn)的后繼節(jié)點(diǎn),使后繼節(jié)點(diǎn)重新嘗試獲取同步狀態(tài)。

為什么只有前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)時(shí)才能嘗試獲取同步狀態(tài)?

頭節(jié)點(diǎn)是成功獲取到同步狀態(tài)的節(jié)點(diǎn),后繼節(jié)點(diǎn)的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點(diǎn)是否是頭節(jié)點(diǎn)。

目的是維護(hù)同步隊(duì)列的 FIFO 原則,節(jié)點(diǎn)和節(jié)點(diǎn)在循環(huán)檢查的過程中基本不通信,而是簡(jiǎn)單判斷自己的前驅(qū)是否為頭節(jié)點(diǎn),這樣就使節(jié)點(diǎn)的釋放規(guī)則符合 FIFO,并且也便于對(duì)過早通知的處理,過早通知指前驅(qū)節(jié)點(diǎn)不是頭節(jié)點(diǎn)的線程由于中斷被喚醒。

AQS 共享式式獲取/釋放鎖的原理?

獲取同步狀態(tài)時(shí),調(diào)用 acquireShared 方法,該方法調(diào)用 tryAcquireShared 方法嘗試獲取同步狀態(tài),返回值為 int 類型,返回值不小于于 0 表示能獲取同步狀態(tài)。因此在共享式獲取鎖的自旋過程中,成功獲取同步狀態(tài)并退出自旋的條件就是該方法的返回值不小于0。

釋放同步狀態(tài)時(shí),調(diào)用 releaseShared 方法,釋放后會(huì)喚醒后續(xù)處于等待狀態(tài)的節(jié)點(diǎn)。它和獨(dú)占式的區(qū)別在于 tryReleaseShared 方法必須確保同步狀態(tài)安全釋放,通過循環(huán) CAS 保證,因?yàn)獒尫磐綘顟B(tài)的操作會(huì)同時(shí)來自多個(gè)線程。

線程

線程的生命周期有哪些狀態(tài)?

NEW:新建狀態(tài),線程被創(chuàng)建且未啟動(dòng),此時(shí)還未調(diào)用 start 方法。

RUNNABLE:Java 將操作系統(tǒng)中的就緒和運(yùn)行兩種狀態(tài)統(tǒng)稱為 RUNNABLE,此時(shí)線程有可能在等待時(shí)間片,也有可能在執(zhí)行。

BLOCKED:阻塞狀態(tài),可能由于鎖被其他線程占用、調(diào)用了 sleepjoin 方法、執(zhí)行了 wait方法等。

WAITING:等待狀態(tài),該狀態(tài)線程不會(huì)被分配 CPU 時(shí)間片,需要其他線程通知或中斷。可能由于調(diào)用了無參的 waitjoin 方法。

TIME_WAITING:限期等待狀態(tài),可以在指定時(shí)間內(nèi)自行返回。導(dǎo)可能由于調(diào)用了帶參的 waitjoin 方法。

TERMINATED:終止?fàn)顟B(tài),表示當(dāng)前線程已執(zhí)行完畢或異常退出。

線程的創(chuàng)建方式有哪些?

① 繼承 Thread 類并重寫 run 方法。實(shí)現(xiàn)簡(jiǎn)單,但不符合里氏替換原則,不可以繼承其他類。

② 實(shí)現(xiàn) Runnable 接口并重寫 run 方法。避免了單繼承局限性,編程更加靈活,實(shí)現(xiàn)解耦。

③實(shí)現(xiàn) Callable 接口并重寫 call 方法。可以獲取線程執(zhí)行結(jié)果的返回值,并且可以拋出異常。

線程有哪些方法?

sleep 方***導(dǎo)致當(dāng)前線程進(jìn)入休眠狀態(tài),與 wait 不同的是該方法不會(huì)釋放鎖資源,進(jìn)入的是 TIMED-WAITING 狀態(tài)。

yiled 方法使當(dāng)前線程讓出 CPU 時(shí)間片給優(yōu)先級(jí)相同或更高的線程,回到 RUNNABLE 狀態(tài),與其他線程一起重新競(jìng)爭(zhēng)CPU時(shí)間片。

join 方法用于等待其他線程運(yùn)行終止,如果當(dāng)前線程調(diào)用了另一個(gè)線程的 join 方法,則當(dāng)前線程進(jìn)入阻塞狀態(tài),當(dāng)另一個(gè)線程結(jié)束時(shí)當(dāng)前線程才能從阻塞狀態(tài)轉(zhuǎn)為就緒態(tài),等待獲取CPU時(shí)間片。底層使用的是wait,也會(huì)釋放鎖。

什么是守護(hù)線程?

守護(hù)線程是一種支持型線程,可以通過 setDaemon(true) 將線程設(shè)置為守護(hù)線程,但必須在線程啟動(dòng)前設(shè)置。

守護(hù)線程被用于完成支持性工作,但在 JVM 退出時(shí)守護(hù)線程中的 finally 塊不一定執(zhí)行,因?yàn)?JVM 中沒有非守護(hù)線程時(shí)需要立即退出,所有守護(hù)線程都將立即終止,不能靠在守護(hù)線程使用 finally 確保關(guān)閉資源。

線程通信的方式有哪些?

命令式編程中線程的通信機(jī)制有兩種,共享內(nèi)存和消息傳遞。在共享內(nèi)存的并發(fā)模型里線程間共享程序的公共狀態(tài),通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信。在消息傳遞的并發(fā)模型里線程間沒有公共狀態(tài),必須通過發(fā)送消息來顯式通信。Java 并發(fā)采用共享內(nèi)存模型,線程之間的通信總是隱式進(jìn)行,整個(gè)通信過程對(duì)程序員完全透明。

volatile 告知程序任何對(duì)變量的讀需要從主內(nèi)存中獲取,寫必須同步刷新回主內(nèi)存,保證所有線程對(duì)變量訪問的可見性。

synchronized 確保多個(gè)線程在同一時(shí)刻只能有一個(gè)處于方法或同步塊中,保證線程對(duì)變量訪問的原子性、可見性和有序性。

等待通知機(jī)制指一個(gè)線程 A 調(diào)用了對(duì)象的 wait 方法進(jìn)入等待狀態(tài),另一線程 B 調(diào)用了對(duì)象的 notify/notifyAll 方法,線程 A 收到通知后結(jié)束阻塞并執(zhí)行后序操作。對(duì)象上的 waitnotify/notifyAll 如同開關(guān)信號(hào),完成等待方和通知方的交互。

如果一個(gè)線程執(zhí)行了某個(gè)線程的 join 方法,這個(gè)線程就會(huì)阻塞等待執(zhí)行了 join 方法的線程終止,這里涉及等待/通知機(jī)制。join 底層通過 wait 實(shí)現(xiàn),線程終止時(shí)會(huì)調(diào)用自身的 notifyAll 方法,通知所有等待在該線程對(duì)象上的線程。

管道 IO 流用于線程間數(shù)據(jù)傳輸,媒介為內(nèi)存。PipedOutputStream 和 PipedWriter 是輸出流,相當(dāng)于生產(chǎn)者,PipedInputStream 和 PipedReader 是輸入流,相當(dāng)于消費(fèi)者。管道流使用一個(gè)默認(rèn)大小為 1KB 的循環(huán)緩沖數(shù)組。輸入流從緩沖數(shù)組讀數(shù)據(jù),輸出流往緩沖數(shù)組中寫數(shù)據(jù)。當(dāng)數(shù)組已滿時(shí),輸出流所在線程阻塞;當(dāng)數(shù)組首次為空時(shí),輸入流所在線程阻塞。

ThreadLocal 是線程共享變量,但它可以為每個(gè)線程創(chuàng)建單獨(dú)的副本,副本值是線程私有的,互相之間不影響。

線程池有什么好處?

降低資源消耗,復(fù)用已創(chuàng)建的線程,降低開銷、控制最大并發(fā)數(shù)。

隔離線程環(huán)境,可以配置獨(dú)立線程池,將較慢的線程與較快的隔離開,避免相互影響。

實(shí)現(xiàn)任務(wù)線程隊(duì)列緩沖策略和拒絕機(jī)制。

實(shí)現(xiàn)某些與時(shí)間相關(guān)的功能,如定時(shí)執(zhí)行、周期執(zhí)行等。

線程池處理任務(wù)的流程?

① 核心線程池未滿,創(chuàng)建一個(gè)新的線程執(zhí)行任務(wù),此時(shí) workCount < corePoolSize。

② 如果核心線程池已滿,工作隊(duì)列未滿,將線程存儲(chǔ)在工作隊(duì)列,此時(shí) workCount >= corePoolSize。

③ 如果工作隊(duì)列已滿,線程數(shù)小于最大線程數(shù)就創(chuàng)建一個(gè)新線程處理任務(wù),此時(shí) workCount < maximumPoolSize,這一步也需要獲取全局鎖。

④ 如果超過大小線程數(shù),按照拒絕策略來處理任務(wù),此時(shí) workCount > maximumPoolSize。

線程池創(chuàng)建線程時(shí),會(huì)將線程封裝成工作線程 Worker,Worker 在執(zhí)行完任務(wù)后還會(huì)循環(huán)獲取工作隊(duì)列中的任務(wù)來執(zhí)行。

有哪些創(chuàng)建線程池的方法?

可以通過 Executors 的靜態(tài)工廠方法創(chuàng)建線程池:

newFixedThreadPool,固定大小的線程池,核心線程數(shù)也是最大線程數(shù),不存在空閑線程,keepAliveTime = 0。該線程池使用的工作隊(duì)列是無界阻塞隊(duì)列 LinkedBlockingQueue,適用于負(fù)載較重的服務(wù)器。

newSingleThreadExecutor,使用單線程,相當(dāng)于單線程串行執(zhí)行所有任務(wù),適用于需要保證順序執(zhí)行任務(wù)的場(chǎng)景。

newCachedThreadPool,maximumPoolSize 設(shè)置為 Integer 最大值,是高度可伸縮的線程池。該線程池使用的工作隊(duì)列是沒有容量的 SynchronousQueue,如果主線程提交任務(wù)的速度高于線程處理的速度,線程池會(huì)不斷創(chuàng)建新線程,極端情況下會(huì)創(chuàng)建過多線程而耗盡CPU 和內(nèi)存資源。適用于執(zhí)行很多短期異步任務(wù)的小程序或負(fù)載較輕的服務(wù)器。

newScheduledThreadPool:線程數(shù)最大為 Integer 最大值,存在 OOM 風(fēng)險(xiǎn)。支持定期及周期性任務(wù)執(zhí)行,適用需要多個(gè)后臺(tái)線程執(zhí)行周期任務(wù),同時(shí)需要限制線程數(shù)量的場(chǎng)景。相比 Timer 更安全,功能更強(qiáng),與 newCachedThreadPool 的區(qū)別是不回收工作線程。

newWorkStealingPool:JDK8 引入,創(chuàng)建持有足夠線程的線程池支持給定的并行度,通過多個(gè)隊(duì)列減少競(jìng)爭(zhēng)。

創(chuàng)建線程池有哪些參數(shù)?

① corePoolSize:常駐核心線程數(shù),如果為 0,當(dāng)執(zhí)行完任務(wù)沒有任何請(qǐng)求時(shí)會(huì)消耗線程池;如果大于 0,即使本地任務(wù)執(zhí)行完,核心線程也不會(huì)被銷毀。該值設(shè)置過大會(huì)浪費(fèi)資源,過小會(huì)導(dǎo)致線程的頻繁創(chuàng)建與銷毀。

② maximumPoolSize:線程池能夠容納同時(shí)執(zhí)行的線程最大數(shù),必須大于等于 1,如果與核心線程數(shù)設(shè)置相同代表固定大小線程池。

keepAliveTime:線程空閑時(shí)間,線程空閑時(shí)間達(dá)到該值后會(huì)被銷毀,直到只剩下 corePoolSize 個(gè)線程為止,避免浪費(fèi)內(nèi)存資源。

④ unit:keepAliveTime 的時(shí)間單位。

⑤ workQueue:工作隊(duì)列,當(dāng)線程請(qǐng)求數(shù)大于等于 corePoolSize 時(shí)線程會(huì)進(jìn)入阻塞隊(duì)列。

⑥ threadFactory:線程工廠,用來生產(chǎn)一組相同任務(wù)的線程。可以給線程命名,有利于分析錯(cuò)誤。

⑦ handler:拒絕策略,默認(rèn)使用 AbortPolicy 丟棄任務(wù)并拋出異常,CallerRunsPolicy 表示重新嘗試提交該任務(wù),DiscardOldestPolicy 表示拋棄隊(duì)列里等待最久的任務(wù)并把當(dāng)前任務(wù)加入隊(duì)列,DiscardPolicy 表示直接拋棄當(dāng)前任務(wù)但不拋出異常。

如何關(guān)閉線程池?

可以調(diào)用 shutdownshutdownNow 方法關(guān)閉線程池,原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的 interrupt 方法中斷線程,無法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無法終止。

區(qū)別是 shutdownNow 首先將線程池的狀態(tài)設(shè)為 STOP,然后嘗試停止正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表。而 shutdown 只是將線程池的狀態(tài)設(shè)為 SHUTDOWN,然后中斷沒有正在執(zhí)行任務(wù)的線程。

通常調(diào)用 shutdown 來關(guān)閉線程池,如果任務(wù)不一定要執(zhí)行完可調(diào)用 shutdownNow


Q11:線程池的選擇策略有什么?

可以從以下角度分析:①任務(wù)性質(zhì):CPU 密集型、IO 密集型和混合型。②任務(wù)優(yōu)先級(jí)。③任務(wù)執(zhí)行時(shí)間。④任務(wù)依賴性:是否依賴其他資源,如數(shù)據(jù)庫連接。

性質(zhì)不同的任務(wù)可用不同規(guī)模的線程池處理,CPU 密集型任務(wù)應(yīng)配置盡可能小的線程,如配置 Ncpu+1 個(gè)線程的線程池。由于 IO 密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),應(yīng)配置盡可能多的線程,如 2*Ncpu。混合型的任務(wù),如果可以拆分,將其拆分為一個(gè) CPU 密集型任務(wù)和一個(gè) IO 密集型任務(wù),只要兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不大那么分解后的吞吐量將高于串行執(zhí)行的吞吐量,如果相差太大則沒必要分解。

優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列 PriorityBlockingQueue 處理。

執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池處理,或者使用優(yōu)先級(jí)隊(duì)列讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。

依賴數(shù)據(jù)庫連接池的任務(wù),由于線程提交 SQL 后需要等待數(shù)據(jù)庫返回的結(jié)果,等待的時(shí)間越長 CPU 空閑的時(shí)間就越長,因此線程數(shù)應(yīng)該盡可能地設(shè)置大一些,提高 CPU 的利用率。

建議使用有界隊(duì)列,能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)置的稍微大一些。


阻塞隊(duì)列有哪些選擇?

阻塞隊(duì)列支持阻塞插入和移除,當(dāng)隊(duì)列滿時(shí),阻塞插入元素的線程直到隊(duì)列不滿。當(dāng)隊(duì)列為空時(shí),獲取元素的線程會(huì)被阻塞直到隊(duì)列非空。阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場(chǎng)景,阻塞隊(duì)列就是生產(chǎn)者用來存放元素,消費(fèi)者用來獲取元素的容器。

Java 中的阻塞隊(duì)列

ArrayBlockingQueue,由數(shù)組組成的有界阻塞隊(duì)列,默認(rèn)情況下不保證線程公平,有可能先阻塞的線程最后才訪問隊(duì)列。

LinkedBlockingQueue,由鏈表結(jié)構(gòu)組成的有界阻塞隊(duì)列,隊(duì)列的默認(rèn)和最大長度為 Integer 最大值。

PriorityBlockingQueue,支持優(yōu)先級(jí)的無界阻塞隊(duì)列,默認(rèn)情況下元素按照升序排序。可自定義 compareTo 方法指定排序規(guī)則,或者初始化時(shí)指定 Comparator 排序,不能保證同優(yōu)先級(jí)元素的順序。

DelayQueue,支持延時(shí)獲取元素的無界阻塞隊(duì)列,使用優(yōu)先級(jí)隊(duì)列實(shí)現(xiàn)。創(chuàng)建元素時(shí)可以指定多久才能從隊(duì)列中獲取當(dāng)前元素,只有延遲期滿時(shí)才能從隊(duì)列中獲取元素,適用于緩存和定時(shí)調(diào)度。

SynchronousQueue,不存儲(chǔ)元素的阻塞隊(duì)列,每一個(gè) put 必須等待一個(gè) take。默認(rèn)使用非公平策略,也支持公平策略,適用于傳遞性場(chǎng)景,吞吐量高。

LinkedTransferQueue,鏈表組成的無界阻塞隊(duì)列,相對(duì)于其他阻塞隊(duì)列多了 tryTransfertransfer 方法。transfer方法:如果當(dāng)前有消費(fèi)者正等待接收元素,可以把生產(chǎn)者傳入的元素立刻傳輸給消費(fèi)者,否則會(huì)將元素放在隊(duì)列的尾節(jié)點(diǎn)并等到該元素被消費(fèi)者消費(fèi)才返回。tryTransfer 方法用來試探生產(chǎn)者傳入的元素能否直接傳給消費(fèi)者,如果沒有消費(fèi)者等待接收元素則返回 false,和 transfer 的區(qū)別是無論消費(fèi)者是否消費(fèi)都會(huì)立即返回。

LinkedBlockingDeque,鏈表組成的雙向阻塞隊(duì)列,可從隊(duì)列的兩端插入和移出元素,多線程同時(shí)入隊(duì)時(shí)減少了競(jìng)爭(zhēng)。

實(shí)現(xiàn)原理

使用通知模式實(shí)現(xiàn),生產(chǎn)者往滿的隊(duì)列里添加元素時(shí)會(huì)阻塞,當(dāng)消費(fèi)者消費(fèi)后,會(huì)通知生產(chǎn)者當(dāng)前隊(duì)列可用。當(dāng)往隊(duì)列里插入一個(gè)元素,如果隊(duì)列不可用,阻塞生產(chǎn)者主要通過 LockSupport 的 park 方法實(shí)現(xiàn),不同操作系統(tǒng)中實(shí)現(xiàn)方式不同,在 Linux 下使用的是系統(tǒng)方法 pthread_cond_wait 實(shí)現(xiàn)。

談一談 ThreadLocal

ThreadLoacl 是線程共享變量,主要用于一個(gè)線程內(nèi)跨類、方法傳遞數(shù)據(jù)。ThreadLoacl 有一個(gè)靜態(tài)內(nèi)部類 ThreadLocalMap,其 Key 是 ThreadLocal 對(duì)象,值是 Entry 對(duì)象,Entry 中只有一個(gè) Object 類的 vaule 值。ThreadLocal 是線程共享的,但 ThreadLocalMap 是每個(gè)線程私有的。ThreadLocal 主要有 set、get 和 remove 三個(gè)方法。

set 方法

首先獲取當(dāng)前線程,然后再獲取當(dāng)前線程對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 map。如果 map 存在就直接設(shè)置值,key 是當(dāng)前的 ThreadLocal 對(duì)象,value 是傳入的參數(shù)。

如果 map 不存在就通過 createMap 方法為當(dāng)前線程創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象再設(shè)置值。

get 方法

首先獲取當(dāng)前線程,然后再獲取當(dāng)前線程對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 map。如果 map 存在就以當(dāng)前 ThreadLocal 對(duì)象作為 key 獲取 Entry 類型的對(duì)象 e,如果 e 存在就返回它的 value 屬性。

如果 e 不存在或者 map 不存在,就調(diào)用 setInitialValue 方法先為當(dāng)前線程創(chuàng)建一個(gè) ThreadLocalMap 對(duì)象然后返回默認(rèn)的初始值 null。

remove 方法

首先通過當(dāng)前線程獲取其對(duì)應(yīng)的 ThreadLocalMap 類型的對(duì)象 m,如果 m 不為空,就解除 ThreadLocal 這個(gè) key 及其對(duì)應(yīng)的 value 值的聯(lián)系。

存在的問題

線程復(fù)用會(huì)產(chǎn)生臟數(shù)據(jù),由于線程池會(huì)重用 Thread 對(duì)象,因此與 Thread 綁定的 ThreadLocal 也會(huì)被重用。如果沒有調(diào)用 remove 清理與線程相關(guān)的 ThreadLocal 信息,那么假如下一個(gè)線程沒有調(diào)用 set 設(shè)置初始值就可能 get 到重用的線程信息。

ThreadLocal 還存在內(nèi)存泄漏的問題,由于 ThreadLocal 是弱引用,但 Entry 的 value 是強(qiáng)引用,因此當(dāng) ThreadLocal 被垃圾回收后,value 依舊不會(huì)被釋放。因此需要及時(shí)調(diào)用 remove 方法進(jìn)行清理操作。

JUC

什么是 CAS?

CAS 表示 Compare And Swap,比較并交換,CAS 需要三個(gè)操作數(shù),分別是內(nèi)存位置 V、舊的預(yù)期值 A 和準(zhǔn)備設(shè)置的新值 B。CAS 指令執(zhí)行時(shí),當(dāng)且僅當(dāng) V 符合 A 時(shí),處理器才會(huì)用 B 更新 V 的值,否則它就不執(zhí)行更新。但不管是否更新都會(huì)返回 V 的舊值,這些處理過程是原子操作,執(zhí)行期間不會(huì)被其他線程打斷。

在 JDK 5 后,Java 類庫中才開始使用 CAS 操作,該操作由 Unsafe 類里的 compareAndSwapInt 等幾個(gè)方法包裝提供。HotSpot 在內(nèi)部對(duì)這些方法做了特殊處理,即時(shí)編譯的結(jié)果是一條平臺(tái)相關(guān)的處理器 CAS 指令。Unsafe 類不是給用戶程序調(diào)用的類,因此 JDK9 前只有 Java 類庫可以使用 CAS,譬如 juc 包里的 AtomicInteger類中 compareAndSet 等方法都使用了Unsafe 類的 CAS 操作實(shí)現(xiàn)。

CAS 有什么問題?

CAS 從語義上來說存在一個(gè)邏輯漏洞:如果 V 初次讀取時(shí)是 A,并且在準(zhǔn)備賦值時(shí)仍為 A,這依舊不能說明它沒有被其他線程更改過,因?yàn)檫@段時(shí)間內(nèi)假設(shè)它的值先改為 B 又改回 A,那么 CAS 操作就會(huì)誤認(rèn)為它從來沒有被改變過。

這個(gè)漏洞稱為 ABA 問題,juc 包提供了一個(gè) AtomicStampedReference,原子更新帶有版本號(hào)的引用類型,通過控制變量值的版本來解決 ABA 問題。大部分情況下 ABA 不會(huì)影響程序并發(fā)的正確性,如果需要解決,傳統(tǒng)的互斥同步可能會(huì)比原子類更高效。

有哪些原子類?

JDK 5 提供了 java.util.concurrent.atomic 包,這個(gè)包中的原子操作類提供了一種用法簡(jiǎn)單、性能高效、線程安全地更新一個(gè)變量的方式。到 JDK 8 該包共有17個(gè)類,依據(jù)作用分為四種:原子更新基本類型類、原子更新數(shù)組類、原子更新引用類以及原子更新字段類,atomic 包里的類基本都是使用 Unsafe 實(shí)現(xiàn)的包裝類。

AtomicInteger 原子更新整形、 AtomicLong 原子更新長整型、AtomicBoolean 原子更新布爾類型。

AtomicIntegerArray,原子更新整形數(shù)組里的元素、 AtomicLongArray 原子更新長整型數(shù)組里的元素、 AtomicReferenceArray 原子更新引用類型數(shù)組里的元素。

AtomicReference 原子更新引用類型、AtomicMarkableReference 原子更新帶有標(biāo)記位的引用類型,可以綁定一個(gè) boolean 標(biāo)記、 AtomicStampedReference 原子更新帶有版本號(hào)的引用類型,關(guān)聯(lián)一個(gè)整數(shù)值作為版本號(hào),解決 ABA 問題。

AtomicIntegerFieldUpdater 原子更新整形字段的更新器、 AtomicLongFieldUpdater 原子更新長整形字段的更新器AtomicReferenceFieldUpdater 原子更新引用類型字段的更新器。

AtomicIntger 實(shí)現(xiàn)原子更新的原理是什么?

AtomicInteger 原子更新整形、 AtomicLong 原子更新長整型、AtomicBoolean 原子更新布爾類型。

getAndIncrement 以原子方式將當(dāng)前的值加 1,首先在 for 死循環(huán)中取得 AtomicInteger 里存儲(chǔ)的數(shù)值,第二步對(duì) AtomicInteger 當(dāng)前的值加 1 ,第三步調(diào)用 compareAndSet 方法進(jìn)行原子更新,先檢查當(dāng)前數(shù)值是否等于 expect,如果等于則說明當(dāng)前值沒有被其他線程修改,則將值更新為 next,否則會(huì)更新失敗返回 false,程序會(huì)進(jìn)入 for 循環(huán)重新進(jìn)行 compareAndSet 操作。

atomic 包中只提供了三種基本類型的原子更新,atomic 包里的類基本都是使用 Unsafe 實(shí)現(xiàn)的,Unsafe 只提供三種 CAS 方法:compareAndSwapIntcompareAndSwapLongcompareAndSwapObject,例如原子更新 Boolean 是先轉(zhuǎn)成整形再使用 compareAndSwapInt

CountDownLatch 是什么?

CountDownLatch 是基于執(zhí)行時(shí)間的同步類,允許一個(gè)或多個(gè)線程等待其他線程完成操作,構(gòu)造方法接收一個(gè) int 參數(shù)作為計(jì)數(shù)器,如果要等待 n 個(gè)點(diǎn)就傳入 n。每次調(diào)用 countDown 方法時(shí)計(jì)數(shù)器減 1,await 方***阻塞當(dāng)前線程直到計(jì)數(shù)器變?yōu)?,由于 countDown 方法可用在任何地方,所以 n 個(gè)點(diǎn)既可以是 n 個(gè)線程也可以是一個(gè)線程里的 n 個(gè)執(zhí)行步驟。

CyclicBarrier 是什么?

循環(huán)屏障是基于同步到達(dá)某個(gè)點(diǎn)的信號(hào)量觸發(fā)機(jī)制,作用是讓一組線程到達(dá)一個(gè)屏障時(shí)被阻塞,直到最后一個(gè)線程到達(dá)屏障才會(huì)解除。構(gòu)造方法中的參數(shù)表示攔截線程數(shù)量,每個(gè)線程調(diào)用 await 方法告訴 CyclicBarrier 自己已到達(dá)屏障,然后被阻塞。還支持在構(gòu)造方法中傳入一個(gè) Runnable 任務(wù),當(dāng)線程到達(dá)屏障時(shí)會(huì)優(yōu)先執(zhí)行該任務(wù)。適用于多線程計(jì)算數(shù)據(jù),最后合并計(jì)算結(jié)果的應(yīng)用場(chǎng)景。

CountDownLacth 的計(jì)數(shù)器只能用一次,而 CyclicBarrier 的計(jì)數(shù)器可使用 reset 方法重置,所以 CyclicBarrier 能處理更為復(fù)雜的業(yè)務(wù)場(chǎng)景,例如計(jì)算錯(cuò)誤時(shí)可用重置計(jì)數(shù)器重新計(jì)算。

Semaphore 是什么?

信號(hào)量用來控制同時(shí)訪問特定資源的線程數(shù)量,通過協(xié)調(diào)各個(gè)線程以保證合理使用公共資源。信號(hào)量可以用于流量控制,特別是公共資源有限的應(yīng)用場(chǎng)景,比如數(shù)據(jù)庫連接。

Semaphore 的構(gòu)造方法參數(shù)接收一個(gè) int 值,表示可用的許可數(shù)量即最大并發(fā)數(shù)。使用 acquire 方法獲得一個(gè)許可證,使用 release 方法歸還許可,還可以用 tryAcquire 嘗試獲得許可。

Exchanger 是什么?

交換者是用于線程間協(xié)作的工具類,用于進(jìn)行線程間的數(shù)據(jù)交換。它提供一個(gè)同步點(diǎn),在這個(gè)同步點(diǎn)兩個(gè)線程可以交換彼此的數(shù)據(jù)。

兩個(gè)線程通過 exchange 方法交換數(shù)據(jù),第一個(gè)線程執(zhí)行 exchange 方法后會(huì)阻塞等待第二個(gè)線程執(zhí)行該方法,當(dāng)兩個(gè)線程都到達(dá)同步點(diǎn)時(shí)這兩個(gè)線程就可以交換數(shù)據(jù),將本線程生產(chǎn)出的數(shù)據(jù)傳遞給對(duì)方。應(yīng)用場(chǎng)景包括遺傳算法、校對(duì)工作等。

JDK7 的 ConcurrentHashMap 原理?

ConcurrentHashMap 用于解決 HashMap 的線程不安全和 HashTable 的并發(fā)效率低,HashTable 之所以效率低是因?yàn)樗芯€程都必須競(jìng)爭(zhēng)同一把鎖,假如容器里有多把鎖,每一把鎖用于鎖容器的部分?jǐn)?shù)據(jù),那么多線程訪問容器不同數(shù)據(jù)段的數(shù)據(jù)時(shí),線程間就不會(huì)存在鎖競(jìng)爭(zhēng),從而有效提高并發(fā)效率,這就是 ConcurrentHashMap 的鎖分段技術(shù)。首先將數(shù)據(jù)分成 Segment 數(shù)據(jù)段,然后給每一個(gè)數(shù)據(jù)段配一把鎖,當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段的數(shù)據(jù)時(shí),其他段的數(shù)據(jù)也能被其他線程訪問。

get 實(shí)現(xiàn)簡(jiǎn)單高效,先經(jīng)過一次再散列,再用這個(gè)散列值通過散列運(yùn)算定位到 Segment,最后通過散列算法定位到元素。get 的高效在于不需要加鎖,除非讀到空值才會(huì)加鎖重讀。get 方法中將共享變量定義為 volatile,在 get 操作里只需要讀所以不用加鎖。

put 必須加鎖,首先定位到 Segment,然后進(jìn)行插入操作,第一步判斷是否需要對(duì) Segment 里的 HashEntry 數(shù)組進(jìn)行擴(kuò)容,第二步定位添加元素的位置,然后將其放入數(shù)組。

size 操作用于統(tǒng)計(jì)元素的數(shù)量,必須統(tǒng)計(jì)每個(gè) Segment 的大小然后求和,在統(tǒng)計(jì)結(jié)果累加的過程中,之前累加過的 count 變化幾率很小,因此先嘗試兩次通過不加鎖的方式統(tǒng)計(jì)結(jié)果,如果統(tǒng)計(jì)過程中容器大小發(fā)生了變化,再加鎖統(tǒng)計(jì)所有 Segment 大小。判斷容器是否發(fā)生變化根據(jù) modCount 確定。

ArrayList 的線程安全集合是什么?

可以使用 CopyOnWriteArrayList 代替 ArrayList,它實(shí)現(xiàn)了讀寫分離。寫操作復(fù)制一個(gè)新的集合,在新集合內(nèi)添加或刪除元素,修改完成后再將原集合的引用指向新集合。這樣做的好處是可以高并發(fā)地進(jìn)行讀寫操作而不需要加鎖,因?yàn)楫?dāng)前集合不會(huì)添加任何元素。使用時(shí)注意盡量設(shè)置容量初始值,并且可以使用批量添加或刪除,避免多次擴(kuò)容,比如只增加一個(gè)元素卻復(fù)制整個(gè)集合。

適合讀多寫少,單個(gè)添加時(shí)效率極低。CopyOnWriteArrayList 是 fail-safe 的,并發(fā)包的集合都是這種機(jī)制,fail-safe 在安全的副本上遍歷,集合修改與副本遍歷沒有任何關(guān)系,缺點(diǎn)是無法讀取最新數(shù)據(jù)。這也是 CAP 理論中 C 和 A 的矛盾,即一致性與可用性的矛盾。

整理了1000道多家公司java面試題400多頁pdf文檔,關(guān)注公種浩【前程有光】,獲取這些整理的資料。



針對(duì)于上面的面試問到的知識(shí)點(diǎn)我總結(jié)出了互聯(lián)網(wǎng)公司Java程序員面試涉及到的絕大部分面試題及答案做成了文檔和架構(gòu)資料分享給大家,家希望能幫助到您面試前的復(fù)習(xí)且找到一個(gè)好的工作,也節(jié)省大家在網(wǎng)上搜索資料的時(shí)間來學(xué)習(xí)。

最后

歡迎大家一起交流,整理資料不易,喜歡文章記得點(diǎn)個(gè)贊喲,感謝支持!!!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評(píng)論 3 421
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,935評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,534評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,892評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,322評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,800評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,084評(píng)論 2 375

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

  • 1、多線程有什么用? 1)揮多核CPU 的優(yōu)勢(shì)隨著工業(yè)的進(jìn)步,現(xiàn)在的筆記本、臺(tái)式機(jī)乃至商用的應(yīng)用服務(wù)器至少也都是雙...
    風(fēng)平浪靜如碼閱讀 348評(píng)論 0 1
  • 久違的晴天,家長會(huì)。 家長大會(huì)開好到教室時(shí),離放學(xué)已經(jīng)沒多少時(shí)間了。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,544評(píng)論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,594評(píng)論 0 11
  • 可愛進(jìn)取,孤獨(dú)成精。努力飛翔,天堂翱翔。戰(zhàn)爭(zhēng)美好,孤獨(dú)進(jìn)取。膽大飛翔,成就輝煌。努力進(jìn)取,遙望,和諧家園。可愛游走...
    趙原野閱讀 2,752評(píng)論 1 1
  • 在妖界我有個(gè)名頭叫胡百曉,無論是何事,只要找到胡百曉即可有解決的辦法。因?yàn)槭侵缓偞蠹乙杂瀭饔灲形摇皟A城百曉”,...
    貓九0110閱讀 3,303評(píng)論 7 3