新聞中心
本文轉(zhuǎn)載自微信公眾號「sowhat1412」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系sowhat1412公眾號。

創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供海倫網(wǎng)站建設(shè)、海倫做網(wǎng)站、海倫網(wǎng)站設(shè)計、海倫網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、海倫企業(yè)網(wǎng)站模板建站服務(wù),十年海倫做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。
1、HashMap
面試第一題必問的 HashMap,挺考驗Javaer的基礎(chǔ)功底的,別問為啥放在這,因為重要!HashMap具有如下特性:
- HashMap 的存取是沒有順序的。
- KV 均允許為 NULL。
- 多線程情況下該類安全,可以考慮用 HashTable。
- JDk8底層是數(shù)組 + 鏈表 + 紅黑樹,JDK7底層是數(shù)組 + 鏈表。
- 初始容量和裝載因子是決定整個類性能的關(guān)鍵點,輕易不要動。
- HashMap是懶漢式創(chuàng)建的,只有在你put數(shù)據(jù)時候才會 build。
- 單向鏈表轉(zhuǎn)換為紅黑樹的時候會先變化為雙向鏈表最終轉(zhuǎn)換為紅黑樹,切記雙向鏈表跟紅黑樹是共存的。
- 對于傳入的兩個key,會強制性的判別出個高低,目的是為了決定向左還是向右放置數(shù)據(jù)。
- 鏈表轉(zhuǎn)紅黑樹后會努力將紅黑樹的root節(jié)點和鏈表的頭節(jié)點 跟table[i]節(jié)點融合成一個。
- 在刪除的時候是先判斷刪除節(jié)點紅黑樹個數(shù)是否需要轉(zhuǎn)鏈表,不轉(zhuǎn)鏈表就跟RBT類似,找個合適的節(jié)點來填充已刪除的節(jié)點。
- 紅黑樹的root節(jié)點不一定跟table[i]也就是鏈表的頭節(jié)點是同一個,三者同步是靠MoveRootToFront實現(xiàn)的。而HashIterator.remove()會在調(diào)用removeNode的時候movable=false。
常見HashMap考點:
- HashMap原理,內(nèi)部數(shù)據(jù)結(jié)構(gòu)。
- HashMap中的put、get、remove大致過程。
- HashMap中 hash函數(shù)實現(xiàn)。
- HashMap如何擴容。
- HashMap幾個重要參數(shù)為什么這樣設(shè)定。
- HashMap為什么線程不安全,如何替換。
- HashMap在JDK7跟JDK8中的區(qū)別。
- HashMap中鏈表跟紅黑樹切換思路。
- JDK7中 HashMap環(huán)產(chǎn)生原理。
2、ConcurrentHashMap
ConcurrentHashMap 是多線程模式下常用的并發(fā)容器,它的實現(xiàn)在JDK7跟JDK8區(qū)別挺大的。
2.1 JDK7
JDK7中的 ConcurrentHashMap 使用 Segment + HashEntry 分段鎖實現(xiàn)并發(fā),它的缺點是并發(fā)程度是由Segment 數(shù)組個數(shù)來決定的,并發(fā)度一旦初始化無法擴容,擴容的話只是HashEntry的擴容。
Segment 繼承自 ReentrantLock,在此扮演鎖的角色??梢岳斫鉃槲覀兊拿總€Segment都是實現(xiàn)了Lock功能的HashMap。如果我們同時有多個Segment形成了Segment數(shù)組那我們就可以實現(xiàn)并發(fā)咯。
大致的put流程如下:
1.ConcurrentHashMap底層大致實現(xiàn)?
ConcurrentHashMap允許多個修改操作并發(fā)進行,其關(guān)鍵在于使用了鎖分離技術(shù)。它使用了多個鎖來控制對hash表的不同部分進行的修改。內(nèi)部使用段(Segment)來表示這些不同的部分,每個段其實就是一個小的HashTable,只要多個修改操作發(fā)生在不同的段上就可以并發(fā)進行。
2.ConcurrentHashMap在并發(fā)下的情況下如何保證取得的元素是最新的?
用于存儲鍵值對數(shù)據(jù)的HashEntry,在設(shè)計上它的成員變量value跟next都是volatile類型的,這樣就保證別的線程對value值的修改,get方法可以馬上看到,并且get的時候是不用加鎖的。
3.ConcurrentHashMap的弱一致性體現(xiàn)在clear和get方法,原因在于沒有加鎖。
比如迭代器在遍歷數(shù)據(jù)的時候是一個Segment一個Segment去遍歷的,如果在遍歷完一個Segment時正好有一個線程在剛遍歷完的Segment上插入數(shù)據(jù),就會體現(xiàn)出不一致性。clear也是一樣。get方法和containsKey方法都是遍歷對應(yīng)索引位上所有節(jié)點,都是不加鎖來判斷的,如果是修改性質(zhì)的因為可見性的存在可以直接獲得最新值,不過如果是新添加值則無法保持一致性。
4.size 統(tǒng)計個數(shù)不準(zhǔn)確
size方法比較有趣,先無鎖的統(tǒng)計所有的數(shù)據(jù)量看下前后兩次是否數(shù)據(jù)一樣,如果一樣則返回數(shù)據(jù),如果不一樣則要把全部的segment進行加鎖,統(tǒng)計,解鎖。并且size方法只是返回一個統(tǒng)計性的數(shù)字。
2.2 JDK8
ConcurrentHashMap 在JDK8中拋棄了分段鎖,轉(zhuǎn)為用 CAS + synchronized,同時將HashEntry改為Node,還加入了紅黑樹的實現(xiàn),主要還是看put的流程(如果看了擴容這塊,絕對可以好好吹逼一番)。
ConcurrentHashMap 是如果來做到高效并發(fā)安全?
1.讀操作
get方法中根本沒有使用同步機制,也沒有使用unsafe方法,所以讀操作是支持并發(fā)操作的。
2.寫操作
基本思路跟HashMap的寫操作類似,只不過用到了CAS + syn 實現(xiàn)加鎖,同時還涉及到擴容的操作。JDK8中鎖已經(jīng)細(xì)化到 table[i] 了,數(shù)組位置不同可并發(fā),位置相同則去幫忙擴容。
3.同步處理主要是通過syn和unsafe的硬件級別原子性這兩種方式完成
當(dāng)我們對某個table[i]操作時候是用syn加鎖的。
取數(shù)據(jù)的時候用的是unsafe硬件級別指令,直接獲取指定內(nèi)存的最新數(shù)據(jù)。
3 、并發(fā)基礎(chǔ)知識
并發(fā)編程的出發(fā)點:充分利用CPU計算資源,多線程并不是一定比單線程快,要不為什么Redis6.0版本的核心操作指令仍然是單線程呢?對吧!
多線程跟單線程的性能都要具體任務(wù)具體分析,talk is cheap, show me the picture。
3.1 進程跟線程
進程:
進程是操作系統(tǒng)調(diào)用的最小單位,是系統(tǒng)進行資源分配和調(diào)度的獨立單位。
線程:
因為進程的創(chuàng)建、銷毀、切換產(chǎn)生大量的時間和空間的開銷,進程的數(shù)量不能太多,而線程是比進程更小的能獨立運行的基本單位,他是進程的一個實體,是CPU調(diào)度的最小單位。線程可以減少程序并發(fā)執(zhí)行時的時間和空間開銷,使得操作系統(tǒng)具有更好的并發(fā)性。
線程基本不擁有系統(tǒng)資源,只有一些運行時必不可少的資源,比如程序計數(shù)器、寄存器和棧,進程則占有堆、棧。線程,Java默認(rèn)有兩個線程 main 跟GC。Java是沒有權(quán)限開線程的,無法操作硬件,都是調(diào)用的 native 的 start0 方法 由 C++ 實現(xiàn)
3.2 并行跟并發(fā)
并發(fā):
concurrency : 多線程操作同一個資源,單核CPU極速的切換運行多個任務(wù)
并行:
parallelism :多個CPU同時使用,CPU多核 真正的同時執(zhí)行
3.3 線程幾個狀態(tài)
Java中線程的狀態(tài)分為6種:
1.初始(New):
新創(chuàng)建了一個線程對象,但還沒有調(diào)用start()方法。
2.可運行(Runnable):
調(diào)用線程的start()方法,此線程進入就緒狀態(tài)。就緒狀態(tài)只是說你資格運行,調(diào)度程序沒有給你CPU資源,你就永遠(yuǎn)是就緒狀態(tài)。
當(dāng)前線程sleep()方法結(jié)束,其他線程join()結(jié)束,等待用戶輸入完畢,某個線程拿到對象鎖,這些線程也將進入就緒狀態(tài)。
當(dāng)前線程時間片用完了,調(diào)用當(dāng)前線程的yield()方法,當(dāng)前線程進入就緒狀態(tài)。
鎖池里的線程拿到對象鎖后,進入就緒狀態(tài)。
3.運行中(Running)
就緒狀態(tài)的線程在獲得CPU時間片后變?yōu)檫\行中狀態(tài)(running)。這也是線程進入運行狀態(tài)的唯一的一種方式。
4.阻塞(Blocked):
阻塞狀態(tài)是線程阻塞在進入synchronized關(guān)鍵字修飾的方法或代碼塊(獲取鎖)時的狀態(tài)。
5.等待(Waiting) 跟 超時等待(Timed_Waiting):
處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間,它們要等待被顯式地喚醒(通知或中斷),否則會處于無限期等待的狀態(tài)。
處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間,不過無須無限期等待被其他線程顯示地喚醒,在達(dá)到一定時間后它們會自動喚醒。
6.終止(Terminated):
當(dāng)線程正常運行結(jié)束或者被異常中斷后就會被終止。線程一旦終止了,就不能復(fù)生。
PS:
調(diào)用 obj.wait 的線程需要先獲取 obj 的 monitor,wait會釋放 obj 的 monitor 并進入等待態(tài)。所以 wait()/notify() 都要與 synchronized 聯(lián)用。
其實線程從阻塞/等待狀態(tài) 到 可運行狀態(tài)都涉及到同步隊列跟等待隊列的,這點在 AQS 有講。
3.4. 阻塞與等待的區(qū)別
阻塞:
當(dāng)一個線程試圖獲取對象鎖(非JUC庫中的鎖,即synchronized),而該鎖被其他線程持有,則該線程進入阻塞狀態(tài)。它的特點是使用簡單,由JVM調(diào)度器來決定喚醒自己,而不需要由另一個線程來顯式喚醒自己,不響應(yīng)中斷。
等待:
當(dāng)一個線程等待另一個線程通知調(diào)度器一個條件時,該線程進入等待狀態(tài)。它的特點是需要等待另一個線程顯式地喚醒自己,實現(xiàn)靈活,語義更豐富,可響應(yīng)中斷。例如調(diào)用:Object.wait()、**Thread.join()**以及等待 Lock 或 Condition。
雖然 synchronized 和 JUC 里的 Lock 都實現(xiàn)鎖的功能,但線程進入的狀態(tài)是不一樣的。synchronized 會讓線程進入阻塞態(tài),而 JUC 里的 Lock是用park()/unpark() 來實現(xiàn)阻塞/喚醒 的,會讓線程進入等待狀態(tài)。雖然等鎖時進入的狀態(tài)不一樣,但被喚醒后又都進入Runnable狀態(tài),從行為效果來看又是一樣的。
3.5 yield 跟 sleep 區(qū)別
- yield 跟 sleep 都能暫停當(dāng)前線程,都不會釋放鎖資源,sleep 可以指定具體休眠的時間,而 yield 則依賴 CPU 的時間片劃分。
- sleep方法給其他線程運行機會時不考慮線程的優(yōu)先級,因此會給低優(yōu)先級的線程以運行的機會。yield方法只會給相同優(yōu)先級或更高優(yōu)先級的線程以運行的機會。
- 調(diào)用 sleep 方法使線程進入等待狀態(tài),等待休眠時間達(dá)到,而調(diào)用我們的 yield方法,線程會進入就緒狀態(tài),也就是sleep需要等待設(shè)置的時間后才會進行就緒狀態(tài),而yield會立即進入就緒狀態(tài)。
- sleep方法聲明會拋出 InterruptedException,而 yield 方法沒有聲明任何異常
- yield 不能被中斷,而 sleep 則可以接受中斷。
- sleep方法比yield方法具有更好的移植性(跟操作系統(tǒng)CPU調(diào)度相關(guān))
3.6 wait 跟 sleep 區(qū)別
1.來源不同
wait 來自O(shè)bject,sleep 來自 Thread
2.是否釋放鎖
wait 釋放鎖,sleep 不釋放
3.使用范圍
wait 必須在同步代碼塊中,sleep 可以任意使用
4.捕捉異常
wait 不需要捕獲異常,sleep 需捕獲異常
3.7 多線程實現(xiàn)方式
- 繼承 Thread,實現(xiàn)run方法
- 實現(xiàn) Runnable接口中的run方法,然后用Thread包裝下。Thread 是線程對象,Runnable 是任務(wù),線程啟動的時候一定是對象。
- 實現(xiàn) Callable接口,F(xiàn)utureTask 包裝實現(xiàn)接口,Thread 包裝 FutureTask。Callable 與Runnable 的區(qū)別在于Callable的call方法有返回值,可以拋出異常,Callable有緩存。
- 通過線程池調(diào)用實現(xiàn)。
- 通過Spring的注解 @Async 實現(xiàn)。
3.8 死鎖
死鎖是指兩個或兩個以上的線程互相持有對方所需要的資源,由于某些鎖的特性,比如syn使用下,一個線程持有一個資源,或者說獲得一個鎖,在該線程釋放這個鎖之前,其它線程是獲取不到這個鎖的,而且會一直死等下去,因此這便造成了死鎖。
面試官:你給我解釋下死鎖是什么,解釋好了我就錄用你。
應(yīng)聘者:先發(fā)Offer,發(fā)了Offer我給你解釋什么是死鎖。
產(chǎn)生條件:
互斥條件:一個資源,或者說一個鎖只能被一個線程所占用,當(dāng)一個線程首先獲取到這個鎖之后,在該線程釋放這個鎖之前,其它線程均是無法獲取到這個鎖的。
占有且等待:一個線程已經(jīng)獲取到一個鎖,再獲取另一個鎖的過程中,即使獲取不到也不會釋放已經(jīng)獲得的鎖。
不可剝奪條件:任何一個線程都無法強制獲取別的線程已經(jīng)占有的鎖
循環(huán)等待條件:線程A拿著線程B的鎖,線程B拿著線程A的鎖。。
檢查:
1、jps -l 定位進程號
2、jstack 進程號找到死鎖問題
避免:
加鎖順序:線程按照相同的順序加鎖。
限時加鎖:線程獲取鎖的過程中限制一定的時間,如果給定時間內(nèi)獲取不到,就算了,這需要用到Lock的一些API。
4、JMM
4.1 JMM由來
隨著CPU、內(nèi)存、磁盤的高速發(fā)展,它們的訪問速度差別很大。為了提速就引入了L1、L2、L3三級緩存。以后程序運行獲取數(shù)據(jù)就是如下的步驟了。
這樣雖然提速了但是會導(dǎo)致緩存一致性問題跟內(nèi)存可見性問題。同時編譯器跟CPU為了加速也引入了指令重排。指令重排的大致意思就是你寫的代碼運行運算結(jié)果會按照你看到的邏輯思維去運行,但是在JVM內(nèi)部系統(tǒng)是智能化的會進行加速排序的。
1、編譯器優(yōu)化的重排序:編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執(zhí)行順序。
2、指令級并行的重排序:現(xiàn)代處理器采用了指令級并行技術(shù)在不影響數(shù)據(jù)依賴性前提下重排。
3、內(nèi)存系統(tǒng)的重排序:處理器使用緩存和讀/寫緩沖區(qū) 進程重排。
指令重排這種機制會導(dǎo)致有序性問題,而在并發(fā)編程時經(jīng)常會涉及到線程之間的通信跟同步問題,一般說是可見性、原子性、有序性。這三個問題對應(yīng)的底層就是 緩存一致性、內(nèi)存可見性、有序性。
原子性:原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。在整個操作過程中不會被線程調(diào)度器中斷的操作,都可認(rèn)為是原子性。比如 a = 1。
可見性:指當(dāng)多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。Java保證可見性可以認(rèn)為通過volatile、synchronized、final來實現(xiàn)。
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行,Java通過volatile、synchronized來保證。
為了保證共享內(nèi)存的正確性(可見性、有序性、原子性),內(nèi)存模型定義了共享內(nèi)存模式下多線程程序讀寫操作行為的規(guī)范,既JMM模型,注意JMM只是一個約定概念,是用來保證效果一致的機制跟規(guī)范。它作用于工作內(nèi)存和主存之間數(shù)據(jù)同步過程,規(guī)定了如何做數(shù)據(jù)同步以及什么時候做數(shù)據(jù)同步。
在JMM中,有兩條規(guī)定:
線程對共享變量的所有操作都必須在自己的工作內(nèi)存中進行,不能直接從主內(nèi)存中讀寫。
不同線程之間無法訪問其他線程工作內(nèi)存中的變量,線程間變量值的傳遞需要通過主內(nèi)存來完成。
共享變量要實現(xiàn)可見性,必須經(jīng)過如下兩個步驟:
把本地內(nèi)存1中更新過的共享變量刷新到主內(nèi)存中。
把主內(nèi)存中最新的共享變量的值更新到本地內(nèi)存2中。
同時人們提出了內(nèi)存屏障、happen-before、af-if-serial這三種概念來保證系統(tǒng)的可見性、原子性、有序性。
4.2 內(nèi)存屏障
內(nèi)存屏障 (Memory Barrier) 是一種CPU指令,用于控制特定條件下的重排序和內(nèi)存可見性問題。Java編譯器也會根據(jù)內(nèi)存屏障的規(guī)則禁止重排序。Java編譯器在生成指令序列的適當(dāng)位置會插入內(nèi)存屏障指令來禁止特定類型的處理器重排序,從而讓程序按我們預(yù)想的流程去執(zhí)行。具有如下功能:
保證特定操作的執(zhí)行順序。
影響某些數(shù)據(jù)(或則是某條指令的執(zhí)行結(jié)果)的內(nèi)存可見性。
在 volatile 中就用到了內(nèi)存屏障,volatile部分已詳細(xì)講述。
4.3 happen-before
因為有指令重排的存在會導(dǎo)致難以理解CPU內(nèi)部運行規(guī)則,JDK用 happens-before 的概念來闡述操作之間的內(nèi)存可見性。在JMM 中如果一個操作執(zhí)行的結(jié)果需要對另一個操作可見,那么這兩個操作之間必須要存在happens-before關(guān)系 。其中CPU的happens-before無需任何同步手段就可以保證的。
- 程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。
- volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。
- 傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。
- start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。
- join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
- 線程中斷規(guī)則:對線程interrupt方法的調(diào)用happens-before于被中斷線程的代碼檢測到中斷事件的發(fā)生。
4.4 af-if-serial
af-if-serial 的含義是不管怎么重排序(編譯器和處理器為了提高并行度),單線程環(huán)境下程序的執(zhí)行結(jié)果不能被改變且必須正確。該語義使單線程環(huán)境下程序員無需擔(dān)心重排序會干擾他們,也無需擔(dān)心內(nèi)存可見性問題。
5、volatile
volatile 關(guān)鍵字的引入可以保證變量的可見性,但是無法保證變量的原子性,比如 a++這樣的是無法保證的。這里其實涉及到JMM 的知識點,Java多線程交互是通過共享內(nèi)存的方式實現(xiàn)的。當(dāng)我們讀寫volatile變量時具有如下規(guī)則:
當(dāng)寫一個volatile變量時,JMM會把該線程對應(yīng)的本地中的共享變量值刷新到主內(nèi)存。
當(dāng)讀一個volatile變量時,JMM會把該線程對應(yīng)的本地內(nèi)存置為無效。線程接下來將從主內(nèi)存中讀取共享變量。
volatile就會用到上面說到的內(nèi)存屏障,目前有四種內(nèi)存屏障:
- StoreStore屏障,保證普通寫不和volatile寫發(fā)生重排序
- StoreLoad屏障,保證volatile寫與后面可能的volatile讀寫不發(fā)生重排序
- LoadLoad屏障,禁止volatile讀與后面的普通讀重排序
- LoadStore屏障,禁止volatile讀和后面的普通寫重排序
volatile原理:用volatile變量修飾的共享變量進行寫操作的時候會使用CPU提供的Lock前綴指令,在CPU級別的功能如下:
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到 系統(tǒng)內(nèi)存。
這個寫回內(nèi)存的操作會告知在其他CPU你們拿到的變量是無效的下一次使用時候要重新共享內(nèi)存拿。
6、單例模式 DCL + volatile
6.1 標(biāo)準(zhǔn)單例模式
高頻考點單例模式:就是將類的構(gòu)造函數(shù)進行private化,然后只留出一個靜態(tài)的Instance 函數(shù)供外部調(diào)用者調(diào)用。單例模式一般標(biāo)準(zhǔn)寫法是 DCL + volatile:
- public class SingleDcl {
- private volatile static SingleDcl singleDcl; //保證可見性
- private SingleDcl(){
- }
- public static SingleDcl getInstance(){
- // 放置進入加鎖代碼,先判斷下是否已經(jīng)初始化好了
- if(singleDcl == null) {
- // 類鎖 可能會出現(xiàn) AB線程都在這卡著,A獲得鎖,B等待獲得鎖。
- synchronized (SingleDcl.class) {
- if(singleDcl == null) {
- // 如果A線程初始化好了,然后通過vloatile 將變量復(fù)雜給住線程。
- // 如果此時沒有singleDel === null,判斷 B進程 進來后還會再次執(zhí)行 new 語句
- singleDcl = new SingleDcl();
- }
- }
- }
- return singleDcl;
- }
- }
6.2 為什么用Volatile修飾
不用Volatile則代碼運行時可能存在指令重排,會導(dǎo)致線程一在運行時執(zhí)行順序是 1-->2--> 4 就賦值給instance變量了,然后接下來再執(zhí)行構(gòu)造方法初始化。問題是如果構(gòu)造方法初始化執(zhí)行沒完成前 線程二進入發(fā)現(xiàn)instance != null,直接給線程二個半成品,加入volatile后底層會使用內(nèi)存屏障強制按照你以為的執(zhí)行。
單例模式幾乎是面試必考點,,一般有如下特性:
懶漢式:在需要用到對象時才實例化對象,正確的實現(xiàn)方式是 Double Check + Lock + volatile,解決了并發(fā)安全和性能低下問題,對內(nèi)存要求非常高,那么使用懶漢式寫法。
餓漢式:在類加載時已經(jīng)創(chuàng)建好該單例對象,在獲取單例對象時直接返回對象即可,對內(nèi)存要求不高使用餓漢式寫法,因為簡單不易出錯,且沒有任何并發(fā)安全和性能問題。
枚舉式:Effective Java 這本書也列舉了使用枚舉,其代碼精簡,沒有線程安全問題,且 Enum 類內(nèi)部防止反射和反序列化時破壞單例。
7、線程池
7.1 五分鐘了解線程池
老王是個深耕在帝都的一線碼農(nóng),辛苦一年掙了點錢,想把錢存儲到銀行卡里,拿錢去銀行辦理遇到了如下的遭遇
老王銀行門口取號后發(fā)現(xiàn)有柜臺營業(yè)ing 但是沒人辦理業(yè)務(wù)就直接辦理了。
老王取號后發(fā)現(xiàn)柜臺上都有人在辦理,等待席有空地,去坐著等辦理去了。
老王取號后發(fā)現(xiàn)柜臺都有人辦理,等待席也人坐滿了,這個時候銀行經(jīng)理看到老王是老實人本著關(guān)愛老實人的態(tài)度,新開一個臨時窗口給他辦理了。
老王取號后發(fā)現(xiàn)柜臺都滿了,等待座位席也滿了,臨時窗口也人滿了。這個時候銀行經(jīng)理給出了若干解決策略。
- 直接告知人太多不給你辦理了。
- 采用冷暴力模式,也不給不辦理也不讓他走。
- 經(jīng)理讓老王取嘗試跟座位席中最前面的人聊一聊看是否可以加塞,可以就辦理,不可以還是被踢走。
- 經(jīng)理直接跟老王說誰讓你來的你找誰去我這辦理不了。
上面的這個流程幾乎就跟JDK線程池的大致流程類似,其中7大參數(shù):
- 營業(yè)中的3個窗口對應(yīng)核心線程池數(shù):corePoolSize
- 銀行總的營業(yè)窗口數(shù)對應(yīng):maximumPoolSize
- 打開的臨時窗口在多少時間內(nèi)無人辦理則關(guān)閉對應(yīng):keepAliveTime
- 臨時窗口存貨時間單位:TimeUnit
- 銀行里的等待座椅就是等待隊列:BlockingQueue
- threadFactory 該參數(shù)在JDK中是 線程工廠,用來創(chuàng)建線程對象,一般不會動。
- 無法辦理的時候銀行給出的解決方法對應(yīng):RejectedExecutionHandler
當(dāng)線程池的任務(wù)緩存隊列已滿并且線程池中的線程數(shù)目達(dá)到maximumPoolSize,如果還有任務(wù)到來就會采取任務(wù)拒絕策略,一般有四大拒絕策略:
- ThreadPoolExecutor.AbortPolicy :丟棄任務(wù),并拋出 RejectedExecutionException 異常。
- ThreadPoolExecutor.CallerRunsPolicy:該任務(wù)被線程池拒絕,由調(diào)用 execute方法的線程執(zhí)行該任務(wù)。
- ThreadPoolExecutor.DiscardOldestPolicy :拋棄隊列最前面的任務(wù),然后重新嘗試執(zhí)行任務(wù)。
- ThreadPoolExecutor.DiscardPolicy:丟棄任務(wù),也不拋出異常。
7.2 正確創(chuàng)建方式
使用Executors創(chuàng)建線程池可能會導(dǎo)致OOM。原因在于線程池中的BlockingQueue主要有兩種實現(xiàn),分別是ArrayBlockingQueue 和 LinkedBlockingQueue。
- ArrayBlockingQueue 是一個用數(shù)組實現(xiàn)的有界阻塞隊列,必須設(shè)置容量。
- LinkedBlockingQueue 是一個用鏈表實現(xiàn)的有界阻塞隊列,容量可以選擇進行設(shè)置,不設(shè)置的話,將是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE,極易容易導(dǎo)致線程池OOM。
正確創(chuàng)建線程池的方式就是自己直接調(diào)用ThreadPoolExecutor的構(gòu)造函數(shù)來自己創(chuàng)建線程池。在創(chuàng)建的同時,給BlockQueue指定容量就可以了。
- private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
- 60L, TimeUnit.SECONDS,
- new ArrayBlockingQueue(10));
7.3 常見線程池
羅列幾種常見的線程池創(chuàng)建方式。
1.Executors.newFixedThreadPool
定長的線程池,有核心線程,核心線程的即為最大的線程數(shù)量,沒有非核心線程。 使用的無界的等待隊列是LinkedBlockingQueue。使用時候小心堵滿等待隊列。
2.Executors.newSingleThreadExecutor
創(chuàng)建單個線程數(shù)的線程池,它可以保證先進先出的執(zhí)行順序
3.Executors.newCachedThreadPool
創(chuàng)建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。
4.Executors.newScheduledThreadPool
創(chuàng)建一個定長的線程池,而且支持定時的以及周期性的任務(wù)執(zhí)行,支持定時及周期性任務(wù)執(zhí)行
5.ThreadPoolExecutor
最原始跟常見的創(chuàng)建線程池的方式,它包含了 7 個參數(shù)、4種拒絕策略 可用。
7.4 線程池核心點
線程池 在工作中常用,面試也是必考點。關(guān)于線程池的細(xì)節(jié)跟使用在以前舉例過一個 銀行排隊 辦業(yè)務(wù)的例子了。線程池一般主要也無非就是下面幾個考點了:
- 為什么用線程池。
- 線程池的作用。
- 7大重要參數(shù)。
- 4大拒絕策略。
- 常見線程池任務(wù)隊列,如何理解有界跟無界。
- 常用的線程池模版。
- 如何分配線程池個數(shù),IO密集型 還是 CPU密集型。
- 設(shè)定一個線程池優(yōu)先級隊列,Runable 類要實現(xiàn)可對比功能,任務(wù)隊列使用優(yōu)先級隊列。
8、ThreadLocal
ThreadLocal 可以簡單理解為線程本地變量,相比于 synchronized 是用空間來換時間的思想。他會在每個線程都創(chuàng)建一個副本,在線程之間通過訪問內(nèi)部副本變量的形式做到了線程之間互相隔離。這里用到了 弱引用 知識點:
如果一個對象只具有弱引用,那么GC回收器在掃描到該對象時,無論內(nèi)存充足與否,都會回收該對象的內(nèi)存。
8.1 核心點
每個Thread內(nèi)部都維護一個ThreadLocalMap字典數(shù)據(jù)結(jié)構(gòu),字典的Key值是ThreadLocal,那么當(dāng)某個ThreadLocal對象不再使用(沒有其它地方再引用)時,每個已經(jīng)關(guān)聯(lián)了此ThreadLocal的線程怎么在其內(nèi)部的ThreadLocalMap里做清除此資源呢?JDK中的ThreadLocalMap沒有繼承java.util.Map類,而是自己實現(xiàn)了一套專門用來定時清理無效資源的字典結(jié)構(gòu)。其內(nèi)部存儲實體結(jié)構(gòu)Entry
接著分析底層代碼會發(fā)現(xiàn)在調(diào)用ThreadLocal.get() 或者 ThreadLocal.set() 都會 定期回收無效的Entry 操作。
9、CAS
Compare And Swap:比較并交換,主要是通過處理器的指令來保證操作的原子性,它包含三個操作數(shù):
V:變量內(nèi)存地址
A:舊的預(yù)期值
B:準(zhǔn)備設(shè)置的新值
當(dāng)執(zhí)行CAS指令時,只有當(dāng) V 對應(yīng)的值等于 A 時才會用 B 去更新V的值,否則就不會執(zhí)行更新操作。CAS可能會帶來ABA問題、循環(huán)開銷過大問題、一個共享變量原子性操作的局限性。如何解決以前寫過,在此不再重復(fù)。
10、Synchronized
10.1 Synchronized 講解
Synchronized 是 JDK自帶的線程安全關(guān)鍵字,該關(guān)鍵字可以修飾實例方法、靜態(tài)方法、代碼塊三部分。該關(guān)鍵字可以保證互斥性、可見性、有序性(不解決重排)但保證有序性。
Syn的底層其實是C++代碼寫的,JDK6前是重量級鎖,調(diào)用的時候涉及到用戶態(tài)跟內(nèi)核態(tài)的切換,挺耗時的。JDK6之前 Doug Lea寫出了JUC包,可以方便的讓用于在用戶態(tài)實現(xiàn)鎖的使用,Syn的開發(fā)者被激發(fā)了斗志所以在JDK6后對Syn進行了各種性能升級。
10.2 Synchronized 底層
Syn里涉及到了 對象頭 包含對象頭、填充數(shù)據(jù)、實例變量。這里可以看一個美團面試題:
問題一:new Object()占多少字節(jié)
- markword 8字節(jié) + classpointer 4字節(jié)(默認(rèn)用calssPointer壓縮) + padding 4字節(jié) = 16字節(jié)
- 如果沒開啟classpointer壓縮:markword 8字節(jié) + classpointer 8字節(jié) = 16字節(jié)
問題二:User (int id,String name) User u = new User(1,"李四")
markword 8字節(jié) + 開啟classPointer壓縮后classpointer 4字節(jié) + instance data int 4字節(jié) + 開啟普通對象指針壓縮后String4字節(jié) + padding 4 = 24字節(jié)
10.3 Synchronized 鎖升級
synchronized 鎖在JDK6以后有四種狀態(tài),無鎖、偏向鎖、輕量級鎖、重量級鎖。這幾個狀態(tài)會隨著競爭狀態(tài)逐漸升級,鎖可以升級但不能降級,但是偏向鎖狀態(tài)可以被重置為無鎖狀態(tài)。大致升級過程如下
鎖對比:
| 鎖狀態(tài) | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖解鎖無需額外消耗,跟非同步方法時間相差納秒級別 | 如果競爭線程多,會帶來額外的鎖撤銷的消耗 | 基本沒有其他線程競爭的同步場景 |
| 輕量級鎖 | 競爭的線程不會阻塞而是在自旋,可提高程序響應(yīng)速度 | 如果一直無法獲得會自旋消耗CPU | 少量線程競爭,持有鎖時間不長,追求響應(yīng)速度 |
| 重量級鎖 | 線程競爭不會導(dǎo)致CPU自旋跟消耗CPU資源 | 線程阻塞,響應(yīng)時間長 | 很多線程競爭鎖,切鎖持有時間長,追求吞吐量時候 |
10.4 Synchronized 無法禁止指令重排,卻能保證有序性
指令重排是程序運行時 解釋器 跟 CPU 自帶的加速手段,可能導(dǎo)致語句執(zhí)行順序跟預(yù)想不一樣情況,但是無論如何重排 也必須遵循 as-if-serial。
避免重排的最簡單方法就是禁止處理器優(yōu)化跟指令重排,比如volatile中用內(nèi)存屏障實現(xiàn),syn是關(guān)鍵字級別的排他且可重入鎖,當(dāng)某個線程執(zhí)行到一段被syn修飾的代碼之前,會先進行加鎖,執(zhí)行完之后再進行解鎖。
當(dāng)某段代碼被syn加鎖后跟解鎖前,其他線程是無法再次獲得鎖的,只有這條加鎖線程可以重復(fù)獲得該鎖。所以代碼在執(zhí)行的時候是單線程執(zhí)行的,這就滿足了as-if-serial語義,正是因為有了as-if-serial語義保證,單線程的有序性就天然存在了。
10.5 wait 虛假喚醒
虛假喚醒定義:
當(dāng)一個條件滿足時,很多線程都被喚醒了,但只有其中部分是有用的喚醒,其它的喚醒是不對的,
比如說買賣貨物,如果商品本來沒有貨物,所有消費者線程都在wait狀態(tài)卡頓呢。這時突然生產(chǎn)者進了一件商品,喚醒了所有掛起的消費者??赡軐?dǎo)致所有的消費者都繼續(xù)執(zhí)行wait下面的代碼,出現(xiàn)錯誤調(diào)用。
虛假喚醒原因:
因為 if 只會執(zhí)行一次,執(zhí)行完會接著向下執(zhí)行 if 下面的。而 while 不會,直到條件滿足才會向下執(zhí)行 while下面的。
虛假喚醒 解決辦法:
在調(diào)用 wait 的時候要用 while 不能用 if。
10.6 notify()底層
1.為何wait跟notify必須要加synchronized鎖
synchronized 代碼塊通過 javap 生成的字節(jié)碼中包含monitorenter 和 monitorexit 指令線程,執(zhí)行 monitorenter 指令可以獲取對象的 monitor,而wait 方法通過調(diào)用 native 方法 wait(0) 實現(xiàn),該注釋說:The current thread must own this object's monitor。
2.notify 執(zhí)行后立馬喚醒線程嗎?
notify/notifyAll 調(diào)用時并不會真正釋放對象鎖,只是把等待中的線程喚醒然后放入到對象的鎖池中,但是鎖池中的所有線程都不會立馬運行,只有擁有鎖的線程運行完代碼塊釋放鎖,別的線程拿到鎖才可以運行。
- public void test()
- {
- Object object = new Object();
- synchronized (object){
- object.notifyAll();
- while (true){
- // TODO 死循環(huán)會導(dǎo)致 無法釋放鎖。
- }
- }
- }
11、AQS
11.1 高頻考點線程交替打印
目標(biāo)是實現(xiàn)兩個線程交替打印,實現(xiàn)字母在前數(shù)字在后。你可以用信號量、Synchronized關(guān)鍵字跟Lock實現(xiàn),這里用 ReentrantLock簡單實現(xiàn):
- import java.util.concurrent.CountDownLatch;
- import java.util.concurrent.locks.Condition;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- public class Main {
- private static Lock lock = new ReentrantLock();
- private static Condition c1 = lock.newCondition();
- private static Condition c2 = lock.newCondition();
- private static CountDownLatch count = new CountDownLatch(1);
- public static void main(String[] args) {
- String c = "ABCDEFGHI";
- char[] ca = c.toCharArray();
- String n = "123456789";
- char[] na = n.toCharArray();
- Thread t1 = new Thread(() -> {
- try {
- lock.lock();
- count.countDown();
- for(char caa : ca) {
- c1.signal();
- System.out.print(caa);
- c2.await();
- }
- c1.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- });
- Thread t2 = new Thread(() -> {
- try {
- count.await();
- lock.lock();
- for(char naa : na) {
- c2.signal();
- System.out.print(naa);
- c1.await();
- }
- c2.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- });
- t1.start();
- t2.start();
- }
- }
11.2 AQS底層
上題我們用到了ReentrantLock、Condition ,但是它們的底層是如何實現(xiàn)的呢?其實他們是基于AQS的 同步隊列 跟 等待隊列 實現(xiàn)的!
11.2.1 AQS 同步隊列
學(xué)AQS 前 CAS + 自旋 + LockSupport + 模板模式 必須會,目的是方便理解源碼,感覺比 Synchronized 簡單,因為是單純的 Java 代碼。個人理解AQS具有如下幾個特點:
- 在AQS 同步隊列中 -1 表示線程在睡眠狀態(tài)
- 當(dāng)前Node節(jié)點線程會把前一個Node.ws = -1。當(dāng)前節(jié)點把前面節(jié)點ws設(shè)置為-1,你可以理解為:你自己能知道自己睡著了嗎?只能是別人看到了發(fā)現(xiàn)你睡眠了!
- 持有鎖的線程永遠(yuǎn)不在隊列中。
- 在AQS隊列中第二個才是最先排隊的線程。
- 如果是交替型任務(wù)或者單線程任務(wù),即使用了Lock也不會涉及到AQS 隊列。
- 不到萬不得已不要輕易park線程,很耗時的!所以排隊的頭線程會自旋的嘗試幾個獲取鎖。
- 并不是說 CAS 一定比SYN好,如果高并發(fā)執(zhí)行時間久 ,用SYN好, 因為SYN底層用了wait() 阻塞后是不消耗CPU資源的。如果鎖競爭不激烈說明自旋不嚴(yán)重 此時用CAS。
- 在AQS中也要盡可能避免調(diào)用CLH隊列,因為CLH可能會調(diào)用到park,相對來耗時。
ReentrantLock底層:
11.2.2 AQS 等待隊列
當(dāng)我們調(diào)用 Condition 里的 await 跟 signal 時候底層其實是這樣走的。
12、線程思考
12.1. 變量建議使用棧封閉
所有的變量都是在方法內(nèi)部聲明的,這些變量都處于棧封閉狀態(tài)。方法調(diào)用的時候會有一個棧楨,這是一個獨立的空間。在這個獨立空間創(chuàng)建跟使用則絕對是安全的,但是注意不要返回該變量哦!
12.2. 防止線程饑餓
優(yōu)先級低的線程總是得不到執(zhí)行機會,一般要保證資源充足、公平的分配資源、防止持有鎖的線程長時間執(zhí)行。
12.3 開發(fā)步驟
多線程編程不要為了用而用,引入多線程后會引入額外的開銷。量應(yīng)用程序性能一般:服務(wù)時間、延遲時間、吞吐量、可伸縮性。做應(yīng)用的時候可以一般按照如下步驟:
- 先確保保證程序的正確性跟健壯性,確實達(dá)不到性能要求再想如何提速。
- 一定要以測試為基準(zhǔn)。
- 一個程序中串行的部分永遠(yuǎn)是有的.
- 裝逼利器:阿姆達(dá)爾定律 S=1/(1-a+a/n)
阿姆達(dá)爾定律中 a為并行計算部分所占比例,n為并行處理結(jié)點個數(shù):
- 當(dāng)1-a=0時,(即沒有串行,只有并行)最大加速比s=n;
- 當(dāng)a=0時(即只有串行,沒有并行),最小加速比s=1;
- 當(dāng)n無窮大時,極限加速比s→ 1/(1-a),這就是加速比的上限。例如,若串行代碼占整個代碼的25%,則并行處理的總體性能不可能超過4。
12.4 影響性能因素
- 縮小鎖的范圍,能鎖方法塊盡量不要鎖函數(shù)
- 減少鎖的粒度跟鎖分段,比如ConcurrentHashMap的實現(xiàn)。
- 讀多寫少時候用讀寫鎖,可提高十倍性能。
- 用CAS操作來替換重型鎖。
- 盡量用JDK自帶的常見并發(fā)容器,底層已經(jīng)足夠優(yōu)化了。
當(dāng)前名稱:Java并發(fā)十二連招,你能接住嗎?
網(wǎng)頁鏈接:http://www.dlmjj.cn/article/djegjjh.html


咨詢
建站咨詢
