多线程知识(转)
但凡面試官考“多線程”,一定會問這10個問題!
原創: Daisy IT技術思維 3月7日
↑↑↑點上方藍字關注并星標?「IT技術思維」
一起培養頂尖技術思維
來源:IT技術思維原創,轉載請注明原出處
內容提供:張雷,新浪微博資深技術專家
隨著技術人才大幅增長以及公司招聘更加嚴苛,程序員的職場正面臨著前所未有的激烈競爭。以Java為例,不僅要了解操作系統、掌握JVM等知識點,還要深耕數據結構與算法,掌握Spring全家桶等框架。
而在這其中,對于并發與多線程的處理,也是一個優秀的技術工程師成長過程中必須攻下的難關。它貫穿著日常工作,也是入職面試重點考察的重點。
我們用5分鐘復習一下并發與多線程。
01
知識點匯總
多線程協作時,因為對資源的鎖定與等待會產生死鎖,需要了解產生死鎖的四個基本條件,要明白競爭條件與臨界區的概念,知道通過破壞造成死鎖的4個條件來防止死鎖。
除了了解進程間的通信方式,還要知道線程的通信方式,通信主要指線程之間的協作機制,例如Wait、Notify
另外需要知道Java為多線程提供的一些機制,例如Threadlocal用來保存線程獨享的數據,Fork/Foin機制用于大任務的分割與匯總,Volatile對多線程數據可見性的保證以及線程的中斷機制。
其他還有: Threadlocal的實現機制。Fork/Join的工作竊取算法等內容。
02
知識點詳解
1、詳解-線程的狀態轉換
先介紹線程狀態轉換。
線程是Jvm執行任務的最小單元,理解線程的狀態轉換是理解后續多線程問題的基礎。
第一個詳解知識點介紹線程狀態轉換。
在Jvm運行中,線程一共有New、Runnable、Blocked、Waiting、Timed_waiting、Terminated六種狀態,這些狀態對應Thread.State枚舉類中的狀態。
當創建一個線程的時候,線程處在New狀態,運行Thread的Start方法后,線程進入Runnable可運行狀態。
這個時候,所有可運行狀態的線程并不能馬上運行,而是需要先進入就緒狀態等待線程調度,如圖中間的Ready狀態。在獲取到Cpu后才能進入運行狀態,如圖中的Running。運行狀態可以隨著不同條件轉換成除New以外的其他狀態。
先看左邊,在運行態中的線程進入Synchronized同步塊或者同步方法時,如果獲取鎖失敗,則會進入到Blocked狀態。當獲取到鎖后,會從Blocked狀態恢復到就緒狀態。
再看右邊,運行中的線程還會進入等待狀態,這兩個等待一個是有超時時間的等待,例如調用Object.wait、Thread.join等。另外一個時無超時的等待,例如調用Thread.join或者Locksupport.park。
這兩種等待都可以通過Notify或Unpark結束等待狀態恢復到就緒狀態。
最后是線程運行完成結束時,如圖下方,線程狀態變成Terminated
2、詳解-CAS與ABA問題
解決線程同步與互斥的主要方式是Cas、Synchronized、和Lock。
Cas是屬于樂觀鎖的一種實現,是一種輕量級鎖,Juc中很多工具類的實現就是基于Cas。
Cas操作是線程在讀取數據時不進行加鎖,在準備寫回數據時,比較原值是否修改,若未被其他線程修改則寫回,若已被修改,則重新執行讀取流程。這是一種樂觀策略,認為并發操作并不總會發生。
比較并寫回的操作是通過操作系統原語實現的,保證執行過程中不會被中斷。
Cas容易出現Aba問題,如果線程T1讀取值A之后,發生過兩次寫入,先由線程T2寫回了b,又由T3寫回了A,此時T1在寫回比較時,值還是A,就無法判斷是否發生過修改。
Aba問題不一定會影響結果,但還是需要防范,解決的辦法可以增加額外的標志位或者時間戳。Juc工具包中提供了這樣的類。
3、詳解-Synchronized
Synchronized是最常用的線程同步手段之一,它是如何保證同一時刻只有一個線程可以進入臨界區呢?
我們知道Synchronized是對對象進行加鎖,在Jvm中,對象在內存中分為三塊區域:對象頭、實例數據和對齊填充。在對象頭中保存了鎖標志位和指向Monitor對象的起始地址。當Monitor被某個線程持有后,就會處于鎖定狀態,Owner部分會指向持有Monitor對象的線程。另外Monitor中還有兩個隊列,用來存放進入及等待獲取鎖的線程。
Synchronized應用在方法上時,在字節碼中是通過方法的AccCC_Synchronized標志來實現的,Synchronized應用在同步塊上時,在字節碼中是通過Monitorenter和Monitorexit實現的。
針對Synchronized獲取鎖的方式,Jvm使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程再次獲取鎖,如果失敗,就升級為Cas輕量級鎖,如果再失敗會短暫自旋,防止線程被系統掛起。最后如果以上都失敗就是升級為重量級鎖。
4、詳解-Aqs與Lock
在介紹Lock前,先介紹Aqs,也就是隊列同步器,這是實現Lock的基礎。
Aqs有一個State標記位,值為1時表示有線程占用,其他線程需要進入到同步隊列等待。同步隊列是一個雙向鏈表。
當獲得鎖的線程需要等待某個條件時,會進入Condition的等待隊列,等待隊列可以有多個。
當Condition條件滿足時,線程會從等待隊列重新進入到同步隊列進行獲取鎖的競爭。
Reentrantlock就是基于Aqs實現的,Reentrantlock內部有公平鎖和非公平鎖兩種實現,差別就在于新來的線程會不會比已經在同步隊列中的等待線程更早獲得鎖。
和Reentrantlock實現方式類似,Semaphore也是基于aqs,差別在于Reentrantlock是獨占鎖,Semaphore是共享鎖。
5、詳解-線程池
線程池通過復用線程,避免線程頻繁創建和銷毀。
Java的Executors工具類中,提供了5種類型線程池的創建方法,它們的特點和適用場景如下:
第1種是:固定大小線程池,特點是線程數固定,使用無界隊列,適用于任務數量不均勻的場景、對內存壓力不敏感,但系統負載比較敏感的場景;
第2種是:Cached線程池,特點是不限制線程數,適用于要求低延遲的短期任務場景;
第3種是:單線程線程池,也就是一個線程的固定線程池,適用于需要異步執行但需要保證任務順序的場景;
第4種是:Scheduled線程池,適用于定期執行任務場景,支持按固定頻率定期執行和按固定延時定期執行兩種方式;
第5種是:工作竊取線程池,使用的ForkJoinPool,是固定并行度的多任務隊列,適合任務執行時長不均勻的場景。
6、詳解-線程池參數介紹
前面提到的線程池,除了工作竊取線程池外,都是通過ThreadPoolExecutor的不同初始化參數來創建的。
第1個參數:設置核心線程數。默認情況下核心線程會一直存活。
第2個參數:設置最大線程數。決定線程池最多可以創建的多少線程。
第3個參數和第4個參數:用來設置線程空閑時間,和空閑時間的單位,當線程閑置超過空閑時間就會被銷毀。可以通過AllowCoreThreadTimeOut方法來允許核心線程被回收。
第5個參數:設置緩沖隊列,圖中左下方的三個隊列是設置線程池時常使用的緩沖隊列。其中Array Blocking Queue是一個有界隊列,就是指隊列有最大容量限制。Linked Blocking Queue是無界隊列,就是隊列不限制容量。最后一個是Synchronous Queue,是一個同步隊列,內部沒有緩沖區。
第6個參數:設置線程池工廠方法,線程工廠用來創建新線程,可以用來對線程的一些屬性進行定制,例如線程的Group、線程名、優先級等。一般使用默認工廠類即可。
第7個參數:設置線程池滿時的拒絕策略。如右下角所示有四種策略,abort策略在線程池滿后,提交新任務時會拋出Rejected Execution Exception,這個也是默認的拒絕策略。
Discard策略會在提交失敗時對任務直接進行丟棄。CallerRuns策略會在提交失敗時,由提交任務的線程直接執行提交的任務。Discard Oldest策略會丟棄最早提交的任務。
■前面的5種線程池都是使用怎樣的參數來創建的呢?
固定大小線程池創建時核心和最大線程數都設置成指定的線程數,這樣線程池中就只會使用固定大小的線程數。隊列使用無界隊列Linked Blocking Queue。
Single線程池就是線程數設置為1的固定線程池。Cached線程池的核心線程數設置為0,最大線程數是Integer.Max_Value,主要是通過把緩沖隊列設置成SynchronousQueue,這樣只要沒有空閑線程就會新建。scheduled線程池與前幾種不同的是使用了Delayed Work Queue,這是一種按延遲時間獲取任務的優先級隊列。
7、詳解-線程池執行流程
我們向線程提交任務時可以使用Execute和Submit,區別就是Submit可以返回一個Future對象,通過Future對象可以了解任務執行情況,可以取消任務的執行,還可獲取執行結果或執行異常。Submit最終也是通過Execute執行的。
線程池提交任務時的執行順序如下:
向線程池提交任務時,會首先判斷線程池中的線程數是否大于設置的核心線程數,如果不大于,就創建一個核心線程來執行任務。
如果大于核心線程數,就會判斷緩沖隊列是否滿了,如果沒有滿,則放入隊列,等待線程空閑時執行任務。
如果隊列已經滿了,則判斷是否達到了線程池設置的最大線程數,如果沒有達到,就創建新線程來執行任務。
如果已經達到了最大線程數,則執行指定的拒絕策略。這里需要注意隊列的判斷與最大線程數判斷的順序,不要搞反。
8、詳解-juc工具類
前面基礎知識部分已經提到過,Juc是Java提供的用于多線程處理的工具類庫,其中的常用工具類的作用如下:
第一行的類都是基本數據類型的原子類:包括Atomicboolean、Atomiclong、Atomicinteger類。
AtomicLong通過Unsafe類實現,基于Cas。Unsafe類是底層工具類,Juc中很多類的底層都使用到了Unsafe包中的功能。Unsafe類提供了類似C的指針操作,提供Cas等功能。Unsafe類中的所有方法都是Native修飾的;
另外Longadder等四個類是Jdk1.8中提供的更高效的操作類。LongAdder基于Cell實現,使用分段鎖思想,是一種空間換時間的策略,更適合高并發場景;
LongAccumulator提供了比LongAdder更強大的功能,能夠指定對數據的操作規則,例如可以把對數據的相加操作改成相乘操作。
第二行中的類提供了對對象的原子讀寫功能,后兩個類Atomic Stamped Reference和Atomic Markable Reference是用來解決我們前面提到的Abs問題,分別基于時間戳和標記位來解決。
9、詳解-juc2
這一頁表格中,第一行的類主要是鎖相關的類,例如我們前面介紹過的Reentrant重入鎖。
與Reentrant Lock的獨占鎖不同,Semaphore是共享鎖,允許多個線程共享資源,適用于限制使用共享資源線程數量的場景,例如100個車輛要使用20個停車位,那么最多允許20個車占用停車位。
Stamped Lock是1.8改進的讀寫鎖,是使用一種Clh的樂觀鎖,能夠有效防止寫饑餓。所謂寫饑餓就是在多線程讀寫時,讀線程訪問非常頻繁,導致總是有讀線程占用資源,寫線程很難加上寫鎖。
第二行中主要是異步執行相關的類,這里可以重點了解jdk1.8中提供的CompletableFuture,可以支持流式調用,可以方便的進行多Future的組合使用,例如可以同時執行兩個異步任務,然后對執行結果進行合并處理。還可以很方便的設置完成時間。
另外一個是1.7中提供的ForkJoinPool,采用分治思想,將大任務分解成多個小任務處理,然后在合并處理結果。ForkJoinPool的特點是使用工作竊取算法,可以有效平衡多任務時間長短不一的場景。
10、詳情-juc3
表格中第一行是常用的阻塞隊列,剛才講解線程池時已經簡單介紹過了,這里在補充一點,Linked Blocking Deque是雙端隊列,也就是可以分別從隊頭和隊尾操作入隊、出隊。
而Array Blocking Queue單端隊列,只能從隊尾入隊,隊頭出隊。
第二行是控制多線程協作時使用的類。其中Count Down Latch實現計數器功能,可以用來控制等待多個線程執行任務后進行匯總。
Cyclic Barrier可以讓一組線程等待至某個狀態之后,再全部同時執行,一般在測試時使用,可以讓多線程更好的并發執行。
Semaphore前面已經介紹過,用來控制對共享資源的訪問并發度。
最后一行是比較常用的兩個集合類,可以了解一下Copy On Write ArrayList,Cow通過在寫入數據時進行Copy修改,然后在更新引用的方式,來消除并行讀寫中的鎖使用,比較適合讀多寫少,數據量比較小,但是并發非常高的場景。
03
面試考察點
第1點:是要理解線程的同步與互斥的原理,包括臨界資源、臨界區的概念,知道重量級鎖、輕量級鎖、自旋鎖、偏向鎖、重入鎖、讀寫鎖的概念。
第2點:要掌握線程安全相關機制,例如 Cas、Synchronized、Lock三種同步方式的實現原理、要明白Threadlocal是每個線程獨享的局部變量,了解Threadlocal使用弱引用的ThreadLocalMap保存不同的Threadlocal變量。
第3點:要了解Juc中的工具類的使用場景與主要的幾種工具類的實現原理,例如Reentrantlock,Concurrenthashmap、Longadder等實現方式
第4點:要熟悉線程池的原理、使用場景、常用配置,例如大量短期任務的場景適合使用Cached線程池;系統資源比較緊張時,可以選擇固定線程池。
另外注意慎用無界隊列,可能會有Oom的風險。
第5點:要深刻理解線程的同步與異步、阻塞與非阻塞,同步和異步的區別是任務是否是同一個線程執行,阻塞與非阻塞的區別是異步執行任務時,線程是不是會阻塞等待結果,還是會繼續執行后續邏輯。
04
面試加分項
掌握了上面這些內容,如果能做到這幾點加分項,一定會給面試官留下更好的印象。
第1點:是可以結合實際項目經驗或者實際案例介紹原理,例如介紹線程池設置時,可以提到自己的項目中有一個需要高吞吐量的場景,使用了Cached的線程池。
第2點:如果有過解決多線程問題的經驗或者排查思路的話會獲得面試加分
第3點:能夠熟悉常用的線程分析工具與方法,例如會用Jstack分析線程的運行狀態,查找鎖對象持有狀況等
第4點:了解Java8對Juc工具類做了哪些增強,例如提供了Longadder來替換Atomiclong,更適合并發度比較高的場景。
最后,可以了解一下Reactive異步編程思想,了解Back Pressure背壓的概念與應用場景。
相信研究完以上內容,你會對并發與多線程有一定了解,從容準備面試,祝你拿到心儀Offer。
以上內容摘取自
《32個Java面試必考點》
第04講:并發與多線程
總結
- 上一篇: D6 PRO充电器使用方法
- 下一篇: 汉王 决战电子书