面经——多线程
面經(jīng)——多線程
注:題目從牛客 Java部門面經(jīng)整理而來。
2020秋招面經(jīng)大匯總!(崗位劃分)
1. 創(chuàng)建線程和終止線程方式
創(chuàng)建線程有四種方式:
中斷線程方式:
2. Runnable和callable區(qū)別
3. synchronize問題詳解
synchronized 問題新開了一遍筆記:synchronized面試五連擊
4. 樂觀鎖和悲觀鎖
1. 樂觀鎖和悲觀鎖區(qū)別
-
悲觀鎖:總是假設(shè)最壞的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。Java中synchronized和ReentrantLock等獨占鎖就是悲觀鎖思想的實現(xiàn)。
-
樂觀鎖:總是假設(shè)最好的情況,每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號機(jī)制和CAS算法實現(xiàn)。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量,像數(shù)據(jù)庫提供的類似于write_condition機(jī)制,其實都是提供的樂觀鎖。在Java中java.util.concurrent.atomic包下面的原子變量類就是使用了樂觀鎖的一種實現(xiàn)方式CAS實現(xiàn)的。
2. 兩種鎖的使用場景
從上面對兩種鎖的介紹,我們知道兩種鎖各有優(yōu)缺點,不可認(rèn)為一種好于另一種,像樂觀鎖適用于寫比較少的情況下(多讀場景),即沖突真的很少發(fā)生的時候,這樣可以省去了鎖的開銷,加大了系統(tǒng)的整個吞吐量。但如果是多寫的情況,一般會經(jīng)常產(chǎn)生沖突,這就會導(dǎo)致上層應(yīng)用會不斷的進(jìn)行retry,這樣反倒是降低了性能,所以一般多寫的場景下用悲觀鎖就比較合適。
5. 線程安全和非線程安全區(qū)別
線程安全就是多線程訪問時,采用了加鎖機(jī)制,當(dāng)一個線程訪問該類的某個數(shù)據(jù)時,進(jìn)行保護(hù),其他線程不能進(jìn)行訪問直到該線程讀取完,其他線程才可使用。不會出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)污染。
線程不安全就是不提供數(shù)據(jù)訪問保護(hù),有可能出現(xiàn)多個線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù)
比如ArrayList是非線程安全的,Vector是線程安全的;HashMap是非線程安全的,HashTable是線程安全的;StringBuilder是非線程安全的,StringBuffer是線程安全的。
6. JMM 內(nèi)存模型
Java 內(nèi)存模型試圖屏蔽各種硬件和操作系統(tǒng)的內(nèi)存訪問差異,以實現(xiàn)讓 Java 程序在各種平臺下都能達(dá)到一致的內(nèi)存
訪問效果。
主內(nèi)存與工作內(nèi)存
處理器上的寄存器的讀寫的速度比內(nèi)存快幾個數(shù)量級,為了解決這種速度矛盾,在它們之間加入了高速緩存。
加入高速緩存帶來了一個新的問題:緩存一致性。如果多個緩存共享同一塊主內(nèi)存區(qū)域,那么多個緩存的數(shù)據(jù)可能會不一致,需要一些協(xié)議來解決這個問題。
解決緩存一致性方案有兩種:
但是方案1存在一個問題,它是采用一種獨占的方式來實現(xiàn)的,即總線加LOCK#鎖的話,只能有一個CPU能夠運行,其他CPU都得阻塞,效率較為低下。
方案二 緩存一致性協(xié)議(MESI協(xié)議)它確保每個緩存中使用的共享變量的副本是一致的。所以JMM就解決這個問題。
JMM(Java內(nèi)存模型Java Memory Model,簡稱JMM)本身是一種抽象的概念并不真實存在,它描述的是一組規(guī)則或規(guī)范,通過這組規(guī)范定義了程序中各個變量(包括實例字段,靜態(tài)字段和構(gòu)成數(shù)組對象的元素)的訪問方式。
JMM關(guān)于同步的規(guī)定:
- 線程解鎖前,必須把共享變量的值刷新回主內(nèi)存
- 線程加鎖前,必須讀取主內(nèi)存的最新值到自己的工作內(nèi)存
- 加鎖解鎖是同一把鎖
由于JVM運行程序的實體是線程,而每個線程創(chuàng)建時JVM都會為其創(chuàng)建一個工作內(nèi)存(有些地方稱為棧空間),工作內(nèi)存是每個線程的私有數(shù)據(jù)區(qū)域,而Java內(nèi)存模型中規(guī)定所有變量都存儲在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問,但線程對變量 的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行,首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對變量進(jìn)行操作,操作完成 后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量,各個線程中的工作內(nèi)存中存儲著主內(nèi)存中的變量副本拷貝,因此不同的線程間無法去訪問對方的工作內(nèi)存,線程間的通信(傳值)必須通過主內(nèi)存來完成
內(nèi)存間交互操作
Java 內(nèi)存模型定義了 8 個操作來完成主內(nèi)存和工作內(nèi)存的交互操作。
- read:把一個變量的值從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中
- load:在 read 之后執(zhí)行,把 read 得到的值放入工作內(nèi)存的變量副本中
- use:把工作內(nèi)存中一個變量的值傳遞給執(zhí)行引擎
- assign:把一個從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量
- store:把工作內(nèi)存的一個變量的值傳送到主內(nèi)存中
- write:在 store 之后執(zhí)行,把 store 得到的值放入主內(nèi)存的變量中
- lock:作用于主內(nèi)存的變量
- unlock
內(nèi)存模型三大特性
1. 原子性
Java 內(nèi)存模型保證了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如對一個 int 類型的變量執(zhí)行 assign 賦值操作,這個操作就是原子性的。但是 Java 內(nèi)存模型允許虛擬機(jī)將沒有被 volatile 修飾的64 位數(shù)據(jù)(long,double)的讀寫操作劃分為兩次 32 位的操作來進(jìn)行,即 load、store、read 和 write 操作可以不具備原子性。
有一個錯誤認(rèn)識就是,int 等原子性的類型在多線程環(huán)境中不會出現(xiàn)線程安全問題。前面的線程不安全示例代碼中,
cnt 屬于 int 類型變量,1000 個線程對它進(jìn)行自增操作之后,得到的值為 997 而不是 1000。
為了方便討論,將內(nèi)存間的交互操作簡化為 3 個:load、assign、store。
下圖演示了兩個線程同時對 cnt 進(jìn)行操作,load、assign、store 這一系列操作整體上看不具備原子性,那么在 T1修改 cnt 并且還沒有將修改后的值寫入主內(nèi)存,T2 依然可以讀入舊值。可以看出,這兩個線程雖然執(zhí)行了兩次自增運算,但是主內(nèi)存中 cnt 的值最后為 1 而不是 2。因此對 int 類型讀寫操作滿足原子性只是說明 load、assign、store 這些單個操作具備原子性。
AtomicInteger 能保證多個線程修改的原子性。
除了使用原子類之外,也可以使用 synchronized 互斥鎖來保證操作的原子性。它對應(yīng)的內(nèi)存間交互操作為:lock 和
unlock,在虛擬機(jī)實現(xiàn)上對應(yīng)的字節(jié)碼指令為 monitorenter 和 monitorexit。
2. 可見性
可見性指當(dāng)一個線程修改了共享變量的值,其它線程能夠立即得知這個修改。Java 內(nèi)存模型是通過在變量修改后將新值同步回主內(nèi)存,在變量讀取前從主內(nèi)存刷新變量值來實現(xiàn)可見性的。
主要有三種實現(xiàn)可見性的方式:
對前面的線程不安全示例中的 cnt 變量使用 volatile 修飾,不能解決線程不安全問題,因為 volatile 并不能保證操作的原子性
3. 有序性
有序性是指:在本線程內(nèi)觀察,所有操作都是有序的。在一個線程觀察另一個線程,所有操作都是無序的,無序是因為發(fā)生了指令重排序。在 Java 內(nèi)存模型中,允許編譯器和處理器對指令進(jìn)行重排序,重排序過程不會影響到單線程程序的執(zhí)行,卻會影響到多線程并發(fā)執(zhí)行的正確性。
volatile 關(guān)鍵字通過添加內(nèi)存屏障的方式來禁止指令重排,即重排序時不能把后面的指令放到內(nèi)存屏障之前。
也可以通過 synchronized 來保證有序性,它保證每個時刻只有一個線程執(zhí)行同步代碼,相當(dāng)于是讓線程順序執(zhí)行同步代碼。
擴(kuò)展:內(nèi)存屏障(Memory Barrier)又稱內(nèi)存柵欄,是一個CPU指令,它的作用有兩個:
由于編譯器和處理器都能執(zhí)行指令重排優(yōu)化。如果在指令間插入一條Memory Barrier則會告訴編譯器和CPU,不管什么指令都不能
和這條MemoryBarrier指令重排序,也就是說通過插入內(nèi)存屏障禁止在內(nèi)存屏障前后的指令執(zhí)行重排序優(yōu)化。內(nèi)存屏障另外一個作
用是強制刷出各種CPU的緩存數(shù)據(jù),因此任何CPU上的線程都能讀取到這些數(shù)據(jù)的最新版本。
7. volatile解析
volatile是 java虛擬機(jī) 提供的輕量級的同步機(jī)制(可以理解成乞丐版的synchronized)
特性有:
1. 保證可見性
理解volatile特性之一保證可見性之前要先理解什么是JMM內(nèi)存模型的可見性(參考上面),JMM 內(nèi)存模型就是 volatile保證可見性特性的原理。
2. 不保證原子性
i++;這條代碼可以分為3個步驟:
如果使用volatile修飾,它只能保證第一步是從主內(nèi)存取得最新值和指令不被重新排序.
例如:從主內(nèi)存取到最新的值a=1,線程A執(zhí)行完+1操作(a=2),如果這個時候線程A讓出時間片,其他線程修改a的值為5,線程A繼續(xù)執(zhí)行,把a=2寫如到主內(nèi)存,這個時候就線程不安全了。主要原因就是把值寫回到主內(nèi)存時,并沒有判斷主內(nèi)存的最新值和之前取到的值一樣就寫回主內(nèi)存了。所以,volatile不保證原子性。
那么如何解決volatile不保證原子性的問題?
我們可以用java.util.concurrent.atomic包下的 AtomicInteger解決這個問題。
3. 禁止指令重排
volatile實現(xiàn)禁止指令重排優(yōu)化,從而避免多線程環(huán)境下程序出現(xiàn)亂序執(zhí)行的現(xiàn)象。volatile禁止指令重排功能 依賴內(nèi)存屏障(內(nèi)容見上)實現(xiàn)。
4. 單例模式下 volatile 的作用
在多線程環(huán)境下,底層為了優(yōu)化有指令重排,加入volatile可以禁止指令重排。
8. 公平鎖和非公平鎖區(qū)別?為什么公平鎖效率低?
1. 公平鎖和非公平鎖區(qū)別?
公平鎖:在并發(fā)壞境中.每個線程在獲取鎖時會先查看此鎖維護(hù)的等待隊列,如果為空,或者當(dāng)前線程是等待隊列的第一個,就占有鎖。否則就會加入到等待隊列中,以后會按照FIFO的規(guī)則從隊列中取到鎖。
非公平鎖:上來就直接嘗試占有鎖,如果嘗試失敗,就執(zhí)行公平鎖邏輯。
2. 為什么公平鎖效率低?
公平鎖要維護(hù)一個隊列,后來的線程要加鎖,即使鎖空閑,也要先檢查有沒有其他線程在等待,如果有自己要掛起,加到隊列后面,然后喚醒隊列最前面的線程。這種情況下相比較非公平鎖多了一次掛起和喚醒,多了線程切換的開銷,這就是非公平鎖效率高于公平鎖的原因,因為非公平鎖減少了線程掛起的幾率,后來的線程有一定幾率逃離被掛起的開銷。
9. 鎖優(yōu)化(自旋鎖、自適應(yīng)自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖、重量級鎖解釋)
這里的鎖優(yōu)化主要是指 JVM 對 synchronized 的優(yōu)化。
1. 自旋鎖和自適應(yīng)自旋鎖
互斥同步對性能的最大的影響是阻塞的實現(xiàn),掛起線程和恢復(fù)線程的操作都需要轉(zhuǎn)入內(nèi)核態(tài)中完成。在許多應(yīng)用中,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間。自旋鎖的思想是讓一個線程在請求一個共享數(shù)據(jù)的鎖時執(zhí)行忙循環(huán)(自旋)一段時間,如果在這段時間內(nèi)能獲得鎖,就可以避免進(jìn)入阻塞狀態(tài)。
自旋鎖雖然能避免進(jìn)入阻塞狀態(tài)從而減少開銷,但是它需要進(jìn)行忙循環(huán)操作占用 CPU 時間,它只適用于共享數(shù)據(jù)的鎖定狀態(tài)很短的場景。自旋等待的時間必須有一定的限度,超過了限定的次數(shù)仍然沒有成功獲取鎖,就應(yīng)當(dāng)使用傳統(tǒng)的方式掛起線程了。自旋次數(shù)的默認(rèn)值是10,用戶可以通過-XX:PreBlockSpin來更改。
在 JDK 1.6 中引入了自適應(yīng)的自旋鎖。自適應(yīng)意味著自旋的次數(shù)不再固定了,而是由前一次在同一個鎖上的自旋次數(shù)及鎖的擁有者的狀態(tài)來決定。如果在同一個鎖對象上,自旋線程之前剛剛獲得過鎖,且現(xiàn)在持有鎖的線程正在運行中,那么虛擬機(jī)會認(rèn)為這次自旋也很有可能會成功,進(jìn)而允許該線程等待持續(xù)相對更長的時間,比如100個循環(huán)。反之,如果某個鎖自旋很少獲得過成功,那么之后再獲取鎖的時候?qū)⒖赡苁÷缘糇孕^程,以避免浪費處理器資源。
2. 鎖消除
鎖消除是指對于被檢測出不可能存在競爭的共享數(shù)據(jù)的鎖進(jìn)行消除。
鎖消除主要是通過逃逸分析來支持,如果堆上的共享數(shù)據(jù)不可能逃逸出去被其它線程訪問到,那么就可以把它們當(dāng)成私有數(shù)據(jù)對待,也就可以將它們的鎖進(jìn)行消除。
對于一些看起來沒有加鎖的代碼,其實隱式的加了很多鎖。例如下面的字符串拼接代碼就隱式加了鎖:
public static String concatString(String s1, String s2, String s3) {return s1 + s2 + s3; }String 是一個不可變的類,編譯器會對 String 的拼接自動優(yōu)化。在 JDK 1.5 之前,會轉(zhuǎn)化為StringBuffer 對象的連續(xù) append() 操作:
public static String concatString(String s1, String s2, String s3) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);sb.append(s3);return sb.toString(); }每個 append() 方法中都有一個同步塊。虛擬機(jī)觀察變量 sb,很快就會發(fā)現(xiàn)它的動態(tài)作用域被限制在 concatString() 方法內(nèi)部。也就是說,sb 的所有引用永遠(yuǎn)不會逃逸到 concatString() 方法之外,其他線程無法訪問到它,因此可以進(jìn)行消除。
3. 鎖粗化
如果一系列的連續(xù)操作都對同一個對象反復(fù)加鎖和解鎖,頻繁的加鎖操作就會導(dǎo)致性能損耗。
上一節(jié)的示例代碼中連續(xù)的 append() 方法就屬于這類情況。如果虛擬機(jī)探測到由這樣的一串零碎的操作都對同一個對象加鎖,將會把加鎖的范圍擴(kuò)展(粗化)到整個操作序列的外部。對于上一節(jié)的示例代碼就是擴(kuò)展到第一個 append() 操作之前直至最后一個 append() 操作之后,這樣只需要加鎖一次就可以了。
4. 偏向鎖
引入偏向鎖的目的和引入輕量級鎖的目的很像,它們都是為了沒有多線程競爭的前提下,減少傳統(tǒng)的重量級鎖使用操作系統(tǒng)互斥量產(chǎn)生的性能消耗。但是不同是:輕量級鎖在無競爭的情況下使用 CAS 操作去代替使用互斥量,而偏向鎖在無競爭的情況下會把整個同步都消除掉。
偏向鎖,顧名思義,它會偏向于第一個訪問鎖的線程,如果在運行過程中,同步鎖只有一個線程訪問,不存在多線程爭用的情況,則線程是不需要觸發(fā)同步的,這種情況下,就會給線程加一個偏向鎖。
如果在運行過程中,遇到了其他線程搶占鎖,則持有偏向鎖的線程會被掛起,JVM會消除它身上的偏向鎖,將鎖恢復(fù)到標(biāo)準(zhǔn)的輕量級鎖。
5. 輕量級鎖
輕量級鎖的目標(biāo)是,減少無實際競爭情況下,使用重量級鎖產(chǎn)生的性能消耗,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。
如果沒有競爭,輕量級鎖使用 CAS 操作避免了使用互斥操作的開銷。但如果存在鎖競爭,除了互斥量開銷外,還會額外發(fā)生CAS操作,因此在有鎖競爭的情況下,輕量級鎖比傳統(tǒng)的重量級鎖更慢,如果鎖競爭激烈,那么輕量級將很快膨脹為重量級鎖。
6. 重量級鎖
內(nèi)置鎖在Java中被抽象為監(jiān)視器鎖(monitor)。在JDK 1.6之前,監(jiān)視器鎖可以認(rèn)為直接對應(yīng)底層操作系統(tǒng)中的互斥量(mutex)。這種同步方式的成本非常高,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換、線程阻塞造成的線程切換等。因此稱這種鎖為“重量級鎖”。
10. AQS原理及應(yīng)用
1. AQS 介紹
AQS的全稱為(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面。
AQS是一個用來構(gòu)建鎖和同步器的框架,使用AQS能簡單且高效地構(gòu)造出應(yīng)用廣泛的大量的同步器,比如我們提到的 ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
2. AQS 原理概覽
AQS核心思想是,如果被請求的共享資源空閑,則將當(dāng)前請求資源的線程設(shè)置為有效的工作線程,并且將共享資源設(shè)置為鎖定狀態(tài)。如果被請求的共享資源被占用,那么就需要一套線程阻塞等待以及被喚醒時鎖分配的機(jī)制,這個機(jī)制AQS是用CLH隊列鎖實現(xiàn)的,即將暫時獲取不到鎖的線程加入到隊列中。
CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結(jié)點之間的關(guān)聯(lián)關(guān)系)。AQS是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結(jié)點(Node)來實現(xiàn)鎖的分配。
看個AQS(AbstractQueuedSynchronizer)原理圖:
AQS使用一個int成員變量來表示同步狀態(tài),通過內(nèi)置的FIFO隊列來完成獲取資源線程的排隊工作。AQS使用CAS對該
同步狀態(tài)進(jìn)行原子操作實現(xiàn)對其值的修改。
狀態(tài)信息通過procted類型的getState,setState,compareAndSetState進(jìn)行操作。
//返回同步狀態(tài)的當(dāng)前值 protected final int getState() {return state; } // 設(shè)置同步狀態(tài)的值 protected final void setState(int newState) {state = newState; } //原子地(CAS操作)將同步狀態(tài)值設(shè)置為給定值update如果當(dāng)前同步狀態(tài)的值等于expect(期望值) protected final boolean compareAndSetState(int expect, int update) {return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }3. AQS 對資源的共享方式
AQS定義兩種資源共享方式
公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
非公平鎖:當(dāng)線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
ReentrantReadWriteLock 可以看成是組合式,因為ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某
一資源進(jìn)行讀。
不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源 state 的獲取與釋放方
式即可,至于具體線程等待隊列的維護(hù)(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了。
4. AQS底層使用了模板方法模式
同步器的設(shè)計是基于模板方法模式的,如果需要自定義同步器一般的方式是這樣(模板方法模式很經(jīng)典的一個應(yīng)
用):
這和我們以往通過實現(xiàn)接口的方式有很大區(qū)別,這是模板方法模式很經(jīng)典的一個運用。
AQS使用了模板方法模式,自定義同步器時需要重寫下面幾個AQS提供的模板方法:
isHeldExclusively()//該線程是否正在獨占資源。只有用到condition才需要去實現(xiàn)它。 tryAcquire(int)//獨占方式。嘗試獲取資源,成功則返回true,失敗則返回false。 tryRelease(int)//獨占方式。嘗試釋放資源,成功則返回true,失敗則返回false。 tryAcquireShared(int)//共享方式。嘗試獲取資源。負(fù)數(shù)表示失敗;0表示成功,但沒有剩余可用資源;正數(shù)表示成功,且有剩余資源。 tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。默認(rèn)情況下,每個方法都拋出 UnsupportedOperationException 。 這些方法的實現(xiàn)必須是內(nèi)部線程安全的,并且
通常應(yīng)該簡短而不是阻塞。AQS類中的其他方法都是final ,所以無法被其他類使用,只有這幾個方法可以被其他類
使用。
以ReentrantLock為例,state初始化為0,表示未鎖定狀態(tài)。A線程lock()時,會調(diào)用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機(jī)會獲取該鎖。當(dāng)然,釋放鎖之前,A線程自己是可以重復(fù)獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態(tài)的。
再以CountDownLatch以例,任務(wù)分為N個子線程去執(zhí)行,state也初始化為N(注意N要與線程個數(shù)一致)。這N個子線程是并行執(zhí)行的,每個子線程執(zhí)行完后countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執(zhí)行完后(即state=0),會unpark()主調(diào)用線程,然后主調(diào)用線程就會從await()函數(shù)返回,繼續(xù)后余動作。
一般來說,自定義同步器要么是獨占方法,要么是共享方式,他們也只需實現(xiàn) tryAcquire-tryRelease 、tryAcquireShared-tryReleaseShared 中的一種即可。但AQS也支持自定義同步器同時實現(xiàn)獨占和共享兩種方式,如 ReentrantReadWriteLock 。
5. AQS 組件總結(jié)
Semaphore(信號量)-允許多個線程同時訪問: synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。
CountDownLatch (倒計時器): CountDownLatch是一個同步工具類,用來協(xié)調(diào)多個線程之間的同步。這個工具通常用來控制線程等待,讓一些線程阻塞直到另一些線程完成一系列操作后才被喚醒。
CyclicBarrier(循環(huán)柵欄): CyclicBarrier的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)。作用是讓一組線程到達(dá)一個屏障(也可以叫
同步點)時被阻塞,直到最后一個線程到達(dá)屏障時,屏障才會開門,所有被屏障攔截的線程才會繼續(xù)干活。CyclicBarrier默認(rèn)的構(gòu)造方法是 CyclicBarrier(int parties),其參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用await方法告訴 CyclicBarrier 我已經(jīng)到達(dá)了屏障,然后當(dāng)前線程被阻塞。
11. CAS
1. CAS是什么
CAS的全稱為Compare-And-Swap,它是一條CPU并發(fā)原語。
它的功能是判斷內(nèi)存某個位置的值是否為期望值,如果是則更改為新的值,這個過程是原子的。
CAS并發(fā)原語體現(xiàn)在JAVA語言中就是sun.misc.Unsafe類中的CAS方法,JVM會幫我們實現(xiàn)CAS匯編指令。這是一種完全依賴于硬件的功能,通過它實現(xiàn)了原子操作。再次強調(diào),由于CAS是一種系統(tǒng)原語,原語屬于操作系統(tǒng)用語范疇范,是由若干條指令組成的,用于完成某個功能的一個過程,并且原語的執(zhí)行必須是連續(xù)的,在執(zhí)行過程中不允許被中斷,也就說CAS是一條CPU的原了指令,不會造成所謂的數(shù)據(jù)不一致問題。
2. CAS底層原理Unsafe深入解析
Unsafe是CAS的核心類,由于Java方法無法直接訪問底層系統(tǒng),需要通過本地(native)方法來訪問,Unsafe相當(dāng)于一個后門,基于該類可以直接操作特定內(nèi)存的數(shù)據(jù)。Unsafe類存在于sun.misc包中,其內(nèi)部方法操作可以像C的指針一樣直接操作內(nèi)存,因為Java中CAS操作的執(zhí)行依賴于Unsafe類的方法。
注意Unsafe類中的所有方法都是native修飾的,也就是Unsafe類中的方法都直接調(diào)用操作系統(tǒng)底層資源執(zhí)行相應(yīng)任務(wù)。
原子整型在i++中操作多線程環(huán)境下不需要加synchronized,也能保證線程安全,是因為它用的是Unsafe類,源代碼如下:
getAndAddInt()方法底層調(diào)用的是unsafe,傳三個參數(shù),當(dāng)前對象,內(nèi)存地址偏移量,增量1。底層調(diào)用的是CAS思想,如果比較成功+1,失敗再重新獲得比較一次,直至成功為止。
var1 AtomicInteger 對象本身
var2 該對象值的引用地址
var4 需要變動的數(shù)量
var5 是用 var1,var2 找出的主內(nèi)存中真實的值,用該對象當(dāng)前的值與 var5 比較:如果相同,更新var5+var4并返回 var5。如果不同,繼續(xù)取值然后再比較,直到更新完成。
3. CAS缺點
通過看源碼,我們發(fā)現(xiàn)有個do while,如果CAS失敗,會一直進(jìn)行嘗試。如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。
當(dāng)對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作。但是,對多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可用鎖來保證原子性。ABA問題概述
CAS算法實現(xiàn)一個重要前提需要取出內(nèi)存中某時刻的數(shù)據(jù)并在當(dāng)下時刻比較并替換,那么在這個時間差類會導(dǎo)致數(shù)據(jù)的變化。
比如說一個線程1 從內(nèi)存位置V中取出A,這時候另一個線程2 也從內(nèi)存中取出A,并且線程2 了一些操作將值變成了B,然后線程2 又將V位置的數(shù)據(jù)變成A,這時候線程1 進(jìn)行CAS作發(fā)現(xiàn)內(nèi)存中仍然是A,然后線程One操作成功。
盡管線程One的CAS慢作成功,但是不代表這個過程就是沒有問題的。
解決:解決ABA問題只靠CAS不能解決,還需要用到原子引用技術(shù)。即AtomicReference
12. 線程同步方法
1. 同步方法
synchronized 關(guān)鍵字修飾方法。由于 java 的每個對象都有一個內(nèi)置鎖,當(dāng)用此關(guān)鍵字修飾方法時,內(nèi)置鎖會保護(hù)整個方法。在調(diào)用該方法前,需要獲得內(nèi)置鎖,否則就處于阻塞狀態(tài)。synchronized關(guān)鍵字也可以修飾靜態(tài)方法,此時如果調(diào)用該靜態(tài)方法,將會鎖住整個類.
代碼如下:
public synchronized void save(){}2. 同步代碼塊
synchronized關(guān)鍵字修飾的語句塊。被該關(guān)鍵字修飾的語句塊會自動加上內(nèi)置鎖,從而實現(xiàn)同步。同步是一種高開銷的操作,因此應(yīng)該盡量減少同步內(nèi)容。通常沒有必要同步整個方法,使用synchronized代碼塊同步關(guān)鍵代碼即可。
代碼如下:
synchronized(object){}3. 使用特殊域變量(volatile)實現(xiàn)線程同步
4. 使用重入鎖實現(xiàn)線程同步
在 javaSE5.0 新增了一個 java.concurrent 包來支持同步。ReentrantLock類可以重入、互斥、實現(xiàn)了Lock接口的鎖。
5. 使用局部變量實現(xiàn)線程同步
如果使用ThreadLocal管理變量,則每一個使用變量的線程都獲得該變量的副本,副本之間相互獨立,這樣每一個線程都可以隨意修改自己的變量副本,而不會對其他線程產(chǎn)生影響。
ThreadLocal與同步機(jī)制:
6.使用阻塞隊列實現(xiàn)線程同步
7.使用原子變量實現(xiàn)線程同步
需要使用線程同步的根本原因在于對普通變量的操作不是原子的。
原子操作就是指將讀取變量值、修改變量值、保存變量值看成一個整體來操作,即這幾種行為要么同時完成,要么都不完成。
在java的util.concurrent.atomic包中提供了創(chuàng)建了原子類型變量的工具類,使用該類可以簡化線程同步,其中AtomicInteger 表可以用原子方式更新int的值。
13. ThreadLocal
早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路。
當(dāng)使用ThreadLocal維護(hù)變量時,ThreadLocal為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,而不會影響其它線程所對應(yīng)的副本。
ThreadLocal是如何做到為每一個線程維護(hù)變量的副本的呢?在ThreadLocal類中有一個Map,用于存儲每一個線程的變量副本,Map中元素的鍵為線程對象,而值對應(yīng)線程的變量副本。
ThreadLocal 和同步機(jī)制的比較
ThreadLocal和線程同步機(jī)制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機(jī)制中,通過對象的鎖機(jī)制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機(jī)制要求程序慎密地分析什么時候?qū)ψ兞窟M(jìn)行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設(shè)計和編寫難度相對較大。
而ThreadLocal則從另一個角度來解決多線程的并發(fā)訪問。ThreadLocal會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數(shù)據(jù)的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進(jìn)行同步了。ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進(jìn)ThreadLocal。
概括起來說,對于多線程資源共享的問題,同步機(jī)制采用了“以時間換空間”的方式,而ThreadLocal采用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
14. ReenTrantLock原理
1. ReentrantLock基本概念
與 synchronized 區(qū)別見上面 synchronized 筆記部分。
15. 線程狀態(tài),start,run,wait,notify,yiled,sleep,join等方法的作用以及區(qū)別
1. 線程狀態(tài)
- NEW:線程剛創(chuàng)建
- RUNNABLE:在 JVM 中正在運行的線程
- BLOCKED:線程處于阻塞狀態(tài),等待監(jiān)視鎖
- WAITING:等待狀態(tài)
- TIMED_WAITING:等待指定的時間后重新被喚醒的狀態(tài)
- TERMINATED:執(zhí)行完成
2. start,run,wait,notify,yiled,sleep,join作用及區(qū)別
1. run()和start()
把需要處理的代碼放到run()方法中,start()方法啟動線程將自動調(diào)用run()方法,這個由java的內(nèi)存機(jī)制規(guī)定的。并且run()方法必需是public訪問權(quán)限,返回值類型為void。
2. wait()
wait()方法使當(dāng)前線程暫停執(zhí)行并釋放對象鎖標(biāo)示,讓其他線程可以進(jìn)入synchronized數(shù)據(jù)塊,當(dāng)前線程被放入對象等待池中。
3. notify() 和 notifyAll()
4. yiled()
使用 yield() 的目的是讓具有相同優(yōu)先級或者更高優(yōu)先級的線程之間能夠適當(dāng)?shù)妮啌Q執(zhí)行。當(dāng)一個線程使用了yield( )方法之后,它就會把自己CPU執(zhí)行的時間讓掉,讓自己或者其它的線程運行。
使當(dāng)前線程從執(zhí)行狀態(tài)(運行狀態(tài))變?yōu)榭蓤?zhí)行態(tài)(就緒狀態(tài))。從而讓其它具有相同優(yōu)先級的等待線程獲取執(zhí)行權(quán)。但是,并不能保證在當(dāng)前線程調(diào)用yield()之后,其它具有相同優(yōu)先級的線程就一定能獲得執(zhí)行權(quán)。也有可能是當(dāng)前線程又進(jìn)入到“運行狀態(tài)”繼續(xù)運行。
5. sleep()
6. join()
join方法的主要作用就是同步,它可以使得線程之間的并行執(zhí)行變?yōu)榇袌?zhí)行。在A線程中調(diào)用了B線程的 join() 方法時,表示只有當(dāng)B線程執(zhí)行完畢時,A線程才能繼續(xù)執(zhí)行。
7. wait()和notify(),notifyAll()是Object類的方法,sleep()和yield()是Thread類的方法。
8. 為什么wait和notify方法要在同步塊中調(diào)用
wait()和notify()因為是線程之間的通信,它們存在競態(tài),會對對象的“鎖標(biāo)志”進(jìn)行操作,所以它們必需在Synchronized函數(shù)或者 synchronized block 中進(jìn)行調(diào)用。如果在non-synchronized 函數(shù)或 non-synchronized block 中進(jìn)行調(diào)用,雖然能編譯通過,但在運行時會發(fā)生IllegalMonitorStateException的異常。
9. wait和sleep區(qū)別
16. 關(guān)于 Atomic 原子類
1. 介紹一下Atomic 原子類
Atomic 翻譯成中文是原子的意思。 Atomic 是指一個操作是不可中斷的。即使是在多個線程一起執(zhí)行的時候,一個操作一旦開始,就不
會被其他線程干擾。所以,所謂原子類說簡單點就是具有原子/原子操作特征的類。
并發(fā)包 java.util.concurrent 的原子類都存放在 java.util.concurrent.atomic 下,如下圖所示。
2. AtomicInteger 類的原理
AtomicInteger 線程安全原理簡單分析
AtomicInteger 類的部分源碼:
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免synchronized 的高開銷,執(zhí)行效率大為提升。
CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內(nèi)存地址,返回值是 valueOffset。另外 value 是一個volatile變量,在內(nèi)存中可見,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值。
17. 線程池相關(guān)
1. 為什么要用線程池?
線程池提供了一種限制和管理資源(包括執(zhí)行一個任務(wù))的方式。 每個線程池還維護(hù)一些基本統(tǒng)計信息,例如已完成任務(wù)的數(shù)量。
使用線程池的好處:
2. 執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢?
3. 如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊》中強制線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風(fēng)險。
Executors 返回線程池對象的弊端如下:
方式一:通過構(gòu)造方法實現(xiàn)
方式二:通過Executor 框架的工具類Executors來實現(xiàn),我們可以創(chuàng)建三種類型的ThreadPoolExecutor:
- FixedThreadPool : 該方法返回一個固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當(dāng)有一個新的任務(wù)提交時,線程池中若有空閑線程,則立即執(zhí)行。若沒有,則新的任務(wù)會被暫存在一個任務(wù)隊列中,待有線程空閑時,便處理在任務(wù)隊列中的任務(wù)。
- SingleThreadExecutor: 方法返回一個只有一個線程的線程池。若多余一個任務(wù)被提交到該線程池,任務(wù)會被保存在一個任務(wù)隊列中,待線程空閑,按先入先出的順序執(zhí)行隊列中的任務(wù)。
- CachedThreadPool: 該方法返回一個可根據(jù)實際情況調(diào)整線程數(shù)量的線程池。線程池的線程數(shù)量不確定,但若有空閑線程可以復(fù)用,則會優(yōu)先使用可復(fù)用的線程。若所有線程均在工作,又有新的任務(wù)提交,則會創(chuàng)建新的線程處理任務(wù)。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后,將返回線程池進(jìn)行復(fù)用。
對應(yīng)Executors工具類中的方法如圖所示:
4. 線程池構(gòu)造函數(shù)7大參數(shù)
在創(chuàng)建了線程池后,當(dāng)有請求任務(wù)來之后,就會安排池中的線程去執(zhí)行請求任務(wù),近似理解為今日當(dāng)值線程。當(dāng)線程池中的線程數(shù)目達(dá)到corePoolSize后,就會把到達(dá)的任務(wù)放到緩存隊列當(dāng)中。
當(dāng)空閑時間達(dá)到keepAIiveTime值時,多余空閑線程會被銷毀直到只剩下corePoolSize個線程為止
5. 線程池處理任務(wù)過程
在創(chuàng)建了線程池后,等待提交過來的任務(wù)請求。
當(dāng)調(diào)用execute()方法添加一個請求任務(wù)時,線程池會做如下判斷:
2.1 如果正在運行的線程數(shù)量小于corePoolSi,那么馬上創(chuàng)建線程運行這個任務(wù):
2.2 如果正在運行的線程數(shù)量大于或等于corePoolSize,那么將這個任務(wù)放入隊列;
2.3 如果這時候隊列滿了且正在運行的線程數(shù)量還小maximumPoolSize,那么還是要創(chuàng)建非核心線程立刻運行這個任務(wù):
2.4 如果隊列滿了且正在運行的線程數(shù)量大于或等于maximumPoolSize,那么線程池會啟動飽和拒絕策略來執(zhí)行。
當(dāng)一個線程完成在務(wù)時,它會從隊列中取下一個任務(wù)來執(zhí)行。
當(dāng)一個線程無事可做超過一定的時間(keepAliveTime)時,線程池會判斷:
如果當(dāng)前運行的線程數(shù)大于corePoolSize,那么這個線程就被停掉。所以線程池的所有任務(wù)完成后它最終會收縮到corePoolSize的大小。
6. 線程池的4種拒絕策略理論概述
拒絕策略概述:等待隊列已經(jīng)滿了,再也塞不下新任務(wù)了,同時,線程池中的max線程也達(dá)到了,無法繼續(xù)為新任務(wù)服務(wù)。這時候我們就需要拒絕策略機(jī)制合理處理這個問題。
4種JDK內(nèi)置拒絕策略
- AbortPolicy(默認(rèn)):直接拋出RejectedExecutionException異常阻止系統(tǒng)正常運行。
- CallerRunsPolicy:"調(diào)用者運行"一種調(diào)節(jié)機(jī)制,該策略既不會拋棄任務(wù),也不會拋出異常,而是將某些任務(wù)回退到調(diào)用者,從而降低新任務(wù)的流量。
- DiscardOldestPolicy:拋棄隊列中等待最久的任務(wù),然后把當(dāng)前任務(wù)加入隊列中嘗試再次提交當(dāng)前任務(wù)。
- DiscardPolicy:直接丟棄任務(wù),不予任何處理也不拋出異常。如果允許任務(wù)丟失,這是最好的一種方案。
以上內(nèi)置拒絕策略均實現(xiàn)了RejectedExecutionHandler接口
7. 線程池配置合理線程數(shù)
1. 要合理配置線程數(shù)首先要知道公司服務(wù)器或阿里云是幾核的
代碼查看服務(wù)器核數(shù):
比如我的CPU核數(shù)4核,執(zhí)行結(jié)果:
2. 看是CPU密集型還是 IO密集型任務(wù)線程
1. CPU密集型
公式:CPU核數(shù)+1個線程的線程池
2. IO密集型
方法一:
由于IO密集型任務(wù)線程并不是一直在執(zhí)行任務(wù),則應(yīng)配置盡可能多的線程,如 CPU核數(shù)*2
方法二:
參考公式:CPU核數(shù)/(1-阻系數(shù)) 比如8核CPU:8/(1-0.9)=80個線程數(shù),阻塞系數(shù)在0.8~0.9之間
18. 手寫簡單的線程池,體現(xiàn)線程復(fù)用
19. 手寫消費者生產(chǎn)者模式
20. 手寫阻塞隊列
21. 手寫多線程交替打印ABC
總結(jié)
- 上一篇: LeetCode:输出整体轮廓线和最长子
- 下一篇: 面经——JVM