隨着技術人才大幅增加以及公司招聘更加嚴苛,程序員的職場正面臨着史無前例的激烈競爭。以Java爲例,不只要了解操做系統、掌握JVM等知識點,還要深耕數據結構與算法,掌握Spring全家桶等框架。java
而在這其中,對於併發與多線程的處理,也是一個優秀的技術工程師成長過程當中必須攻下的難關。它貫穿着平常工做,也是入職面試重點考察的重點。程序員
咱們用5分鐘複習一下併發與多線程。面試
多線程協做時,由於對資源的鎖定與等待會產生死鎖,須要瞭解產生死鎖的四個基本條件,要明白競爭條件與臨界區的概念,知道經過破壞形成死鎖的4個條件來防止死鎖。算法
除了瞭解進程間的通訊方式,還要知道線程的通訊方式,通訊主要指線程之間的協做機制,例如wait、notify編程
另外須要知道java爲多線程提供的一些機制,例如threadlocal用來保存線程獨享的數據,fork/join機制用於大任務的分割與彙總,volatile對多線程數據可見性的保證以及線程的中斷機制。安全
其餘還有: threadlocal的實現機制。fork/join的工做竊取算法等內容。數據結構
一、要理解線程的同步與互斥的原理,包括臨界資源、臨界區的概念,知道重量級鎖、輕量級鎖、自旋鎖、偏向鎖、重入鎖、讀寫鎖的概念。多線程
二、要掌握線程安全相關機制,例如 cas、synchronized、lock三種同步方式的實現原理、要明白threadlocal是每一個線程獨享的局部變量,瞭解threadlocal使用弱引用的ThreadLocalMap保存不一樣的threadlocal變量。併發
三、要了解JUC中的工具類的使用場景與主要的幾種工具類的實現原理,例如reentrantlock,concurrenthashmap、longadder等實現方式框架
四、要熟悉線程池的原理、使用場景、經常使用配置,例如大量短時間任務的場景適合使用cached線程池;系統資源比較緊張時,能夠選擇固定線程池。
另外注意慎用無界隊列,可能會有oom的風險。
五、要深入理解線程的同步與異步、阻塞與非阻塞,同步和異步的區別是任務是不是同一個線程執行,阻塞與非阻塞的區別是異步執行任務時,線程是否是會阻塞等待結果,仍是會繼續執行後續邏輯。
一、詳解-線程的狀態轉換
先介紹線程狀態轉換。
線程是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
二、詳解-CAS與ABA問題
解決線程同步與互斥的主要方式是cas、synchronized、和lock。
cas是屬於樂觀鎖的一種實現,是一種輕量級鎖,juc中不少工具類的實現就是基於cas。
cas操做是線程在讀取數據時不進行加鎖,在準備寫回數據時,比較原值是否修改,若未被其餘線程修改則寫回,若已被修改,則從新執行讀取流程。這是一種樂觀策略,認爲併發操做並不總會發生。
比較並寫回的操做是經過操做系統原語實現的,保證執行過程當中不會被中斷。
CAS容易出現ABA問題,若是線程T1讀取值A以後,發生過兩次寫入,先由線程T2寫回了b,又由t3寫回了a,此時t1在寫回比較時,值仍是a,就沒法判斷是否發生過修改。
aba問題不必定會影響結果,但仍是須要防範,解決的辦法能夠增長額外的標誌位或者時間戳。juc工具包中提供了這樣的類。
三、詳解-synchronized
synchronized是最經常使用的線程同步手段之一,它是如何保證同一時刻只有一個線程能夠進入臨界區呢?
咱們知道synchronized是對對象進行加鎖,在JVM中,對象在內存中分爲三塊區域:對象頭、實例數據和對齊填充。在對象頭中保存了鎖標誌位和指向monitor對象的起始地址。當monitor被某個線程持有後,就會處於鎖定狀態,owner部分會指向持有monitor對象的線程。另外monitor中還有兩個隊列,用來存放進入及等待獲取鎖的線程。
synchronized應用在方法上時,在字節碼中是經過方法的ACC_SYNCHRONIZED標誌來實現的,synchronized應用在同步塊上時,在字節碼中是經過monitorenter和monitorexit實現的。
針對synchronized獲取鎖的方式,jvm使用了鎖升級的優化方式,就是先使用偏向鎖優先同一線程再次獲取鎖,若是失敗,就升級爲CAS輕量級鎖,若是再失敗會短暫自旋,防止線程被系統掛起。最後若是以上都失敗就是升級爲重量級鎖。
四、詳解-aqs與lock
在介紹lock前,先介紹aqs,也就是隊列同步器,這是實現lock的基礎。
aqs有一個state標記位,值爲1時表示有線程佔用,其餘線程須要進入到同步隊列等待。同步隊列是一個雙向鏈表。
當得到鎖的線程須要等待某個條件時,會進入condition的等待隊列,等待隊列能夠有多個。
當condition條件知足時,線程會從等待隊列從新進入到同步隊列進行獲取鎖的競爭。
reentrantlock就是基於aqs實現的,reentrantlock內部有公平鎖和非公平鎖兩種實現,差異就在於新來的線程會不會比已經在同步隊列中的等待線程更早得到鎖。
和reentrantlock實現方式相似,semaphore也是基於aqs,差異在於reentrantlock是獨佔鎖,semaphore是共享鎖。
五、詳解-線程池
線程池經過複用線程,避免線程頻繁建立和銷燬。
java的Executors工具類中,提供了5種類型線程池的建立方法,它們的特色和適用場景以下:
第1種是:固定大小線程池,特色是線程數固定,使用無界隊列,適用於任務數量不均勻的場景、對內存壓力不敏感,但系統負載比較敏感的場景;
第2種是:cached線程池,特色是不限制線程數,適用於要求低延遲的短時間任務場景;
第3種是:單線程線程池,也就是一個線程的固定線程池,適用於須要異步執行但須要保證任務順序的場景;
第4種是:scheduled線程池,適用於按期執行任務場景,支持按固定頻率按期執行和按固定延時按期執行兩種方式;
第5種是:工做竊取線程池,使用的ForkJoinPool,是固定並行度的多任務隊列,適合任務執行時長不均勻的場景。
六、詳解-線程池參數介紹
前面提到的線程池,除了工做竊取線程池外,都是經過ThreadPoolExecutor的不一樣初始化參數來建立的。
第一個參數設置核心線程數。默認狀況下核心線程會一直存活。
第二個參數設置最大線程數。決定線程池最多能夠建立的多少線程。
第三個參數和第四個參數用來設置線程空閒時間,和空閒時間的單位,當線程閒置超過空閒時間就會被銷燬。能夠經過allowCoreThreadTimeOut方法來容許核心線程被回收。
第五個參數設置緩衝隊列,圖中左下方的三個隊列是設置線程池時常使用的緩衝隊列。其中ArrayBlockingQueue是一個有界隊列,就是指隊列有最大容量限制。LinkedBlockingQueue是無界隊列,就是隊列不限制容量。最後一個是SynchronousQueue,是一個同步隊列,內部沒有緩衝區。
第六個設置線程池工廠方法,線程工廠用來建立新線程,能夠用來對線程的一些屬性進行定製,例如線程的group、線程名、優先級等。通常使用默認工廠類便可。
第七個設置線程池滿時的拒絕策略。如右下角所示有四種策略,abort策略在線程池滿後,提交新任務時會拋出RejectedExecutionException,這個也是默認的拒絕策略。
Discard策略會在提交失敗時對任務直接進行丟棄。CallerRuns策略會在提交失敗時,由提交任務的線程直接執行提交的任務。DiscardOldest策略會丟棄最先提交的任務。
前面的5種線程池都是使用怎樣的參數來建立的呢?
固定大小線程池建立時核心和最大線程數都設置成指定的線程數,這樣線程池中就只會使用固定大小的線程數。隊列使用無界隊列linkedblockingqueue。
single線程池就是線程數設置爲1的固定線程池。cached線程池的核心線程數設置爲0,最大線程數是Integer.MAX_VALUE,主要是經過把緩衝隊列設置成SynchronousQueue,這樣只要沒有空閒線程就會新建。scheduled線程池與前幾種不一樣的是使用了DelayedWorkQueue,這是一種按延遲時間獲取任務的優先級隊列。
七、詳解-線程池執行流程
咱們向線程提交任務時可使用execute和submit,區別就是submit能夠返回一個future對象,經過Future對象能夠了解任務執行狀況,能夠取消任務的執行,還可獲取執行結果或執行異常。submit最終也是經過execute執行的。
線程池提交任務時的執行順序以下:
向線程池提交任務時,會首先判斷線程池中的線程數是否大於設置的核心線程數,若是不大於,就建立一個核心線程來執行任務。
若是大於核心線程數,就會判斷緩衝隊列是否滿了,若是沒有滿,則放入隊列,等待線程空閒時執行任務。
若是隊列已經滿了,則判斷是否達到了線程池設置的最大線程數,若是沒有達到,就建立新線程來執行任務。
若是已經達到了最大線程數,則執行指定的拒絕策略。這裏須要注意隊列的判斷與最大線程數判斷的順序,不要搞反。
八、詳解-juc工具類
前面基礎知識部分已經提到過,juc是java提供的用於多線程處理的工具類庫,其中的經常使用工具類的做用以下:
第一行的類都是基本數據類型的原子類:包括atomicboolean、atomiclong、atomicinteger類。
AtomicLong經過unsafe類實現,基於CAS。unsafe類是底層工具類,juc中不少類的底層都使用到了unsafe包中的功能。unsafe類提供了相似c的指針操做,提供CAS等功能。Unsafe類中的全部方法都是native修飾的;
另外longadder等四個類是jdk1.8中提供的更高效的操做類。LongAdder基於Cell實現,使用分段鎖思想,是一種空間換時間的策略,更適合高併發場景;
LongAccumulator提供了比LongAdder更強大的功能,可以指定對數據的操做規則,例如能夠把對數據的相加操做改爲相乘操做。
第二行中的類提供了對對象的原子讀寫功能,後兩個類AtomicStampedReference和AtomicMarkableReference是用來解決咱們前面提到的ABA問題,分別基於時間戳和標記位來解決。
九、詳解-juc2
這一頁表格中,第一行的類主要是鎖相關的類,例如咱們前面介紹過的reentrant重入鎖。
與ReentrantLock的獨佔鎖不一樣,Semaphore是共享鎖,容許多個線程共享資源,適用於限制使用共享資源線程數量的場景,例如100個車輛要使用20個停車位,那麼最多容許20個車佔用停車位。
StampedLock是1.8改進的讀寫鎖,是使用一種CLH的樂觀鎖,可以有效防止寫飢餓。所謂寫飢餓就是在多線程讀寫時,讀線程訪問很是頻繁,致使老是有讀線程佔用資源,寫線程很難加上寫鎖。
第二行中主要是異步執行相關的類,這裏能夠重點了解jdk1.8中提供的CompletableFuture,能夠支持流式調用,能夠方便的進行多future的組合使用,例如能夠同時執行兩個異步任務,而後對執行結果進行合併處理。還能夠很方便的設置完成時間。
另一個是1.7中提供的ForkJoinPool,採用分治思想,將大任務分解成多個小任務處理,而後在合併處理結果。ForkJoinPool的特色是使用工做竊取算法,能夠有效平衡多任務時間長短不一的場景。
十、詳情-juc3
表格中第一行是經常使用的阻塞隊列,剛纔講解線程池時已經簡單介紹過了,這裏在補充一點,LinkedBlockingDeque是雙端隊列,也就是能夠分別從隊頭和隊尾操做入隊、出隊。
而ArrayBlockingQueue單端隊列,只能從隊尾入隊,隊頭出隊。
第二行是控制多線程協做時使用的類。其中CountDownLatch實現計數器功能,能夠用來控制等待多個線程執行任務後進行彙總。
CyclicBarrier可讓一組線程等待至某個狀態以後,再所有同時執行,通常在測試時使用,可讓多線程更好的併發執行。
Semaphore前面已經介紹過,用來控制對共享資源的訪問併發度。
最後一行是比較經常使用的兩個集合類,能夠了解一下CopyOnWriteArrayList,COW經過在寫入數據時進行copy修改,而後在更新引用的方式,來消除並行讀寫中的鎖使用,比較適合讀多寫少,數據量比較小,可是併發很是高的場景。
掌握了上面這些內容,若是能作到這幾點加分項,必定會給面試官留下更好的印象。
一、能夠結合實際項目經驗或者實際案例介紹原理,例如介紹線程池設置時,能夠提到本身的項目中有一個須要高吞吐量的場景,使用了cached的線程池;
二、若是有過解決多線程問題的經驗或者排查思路的話會得到面試加分;
三、可以熟悉經常使用的線程分析工具與方法,例如會用jstack分析線程的運行狀態,查找鎖對象持有情況等;
四、瞭解Java8對JUC工具類作了哪些加強,例如提供了longadder來替換atomiclong,更適合併發度比較高的場景;
五、能夠了解Reactive異步編程思想,瞭解back pressure背壓的概念與應用場景。
以上內容摘取自《32個Java面試必考點》第04講:併發與多線程,點此學習更多