下面最近發(fā)的一些并發(fā)編程的文章匯總羞反,通過閱讀這些文章大家再看大廠面試中的并發(fā)編程問題就沒有那么頭疼了。今天給大家總結(jié)一下唠雕,面試中出鏡率很高的幾個多線程面試題勉抓,希望對大家學(xué)習(xí)和面試都能有所幫助。備注:文中的代碼自己實現(xiàn)一遍的話效果會更佳哦惑申!
一 面試中關(guān)于 synchronized 關(guān)鍵字的 5 連擊
1.1 說一說自己對于 synchronized 關(guān)鍵字的了解
synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性具伍,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執(zhí)行。
另外圈驼,在 Java 早期版本中人芽,synchronized屬于重量級鎖,效率低下碗脊,因為監(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實現(xiàn)的啼肩,Java 的線程是映射到操作系統(tǒng)的原生線程之上的橄妆。如果要掛起或者喚醒一個線程,都需要操作系統(tǒng)幫忙完成祈坠,而操作系統(tǒng)實現(xiàn)線程之間的切換時需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)害碾,這個狀態(tài)之間的轉(zhuǎn)換需要相對比較長的時間,時間成本相對較高赦拘,這也是為什么早期的 synchronized 效率低的原因慌随。慶幸的是在 Java 6 之后 Java 官方對從 JVM 層面對synchronized 較大優(yōu)化,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯了躺同。JDK1.6對鎖的實現(xiàn)引入了大量的優(yōu)化阁猜,如自旋鎖、適應(yīng)性自旋鎖蹋艺、鎖消除剃袍、鎖粗化、偏向鎖捎谨、輕量級鎖等技術(shù)來減少鎖操作的開銷民效。
1.2 說說自己是怎么使用 synchronized 關(guān)鍵字,在項目中用到了嗎
synchronized關(guān)鍵字最主要的三種使用方式:
修飾實例方法涛救,作用于當(dāng)前對象實例加鎖畏邢,進入同步代碼前要獲得當(dāng)前對象實例的鎖
修飾靜態(tài)方法,作用于當(dāng)前類對象加鎖检吆,進入同步代碼前要獲得當(dāng)前類對象的鎖?舒萎。也就是給當(dāng)前類加鎖,會作用于類的所有對象實例蹭沛,因為靜態(tài)成員不屬于任何一個實例對象臂寝,是類成員( static 表明這是該類的一個靜態(tài)資源,不管new了多少個對象摊灭,只有一份交煞,所以對該類的所有對象都加了鎖)。所以如果一個線程A調(diào)用一個實例對象的非靜態(tài) synchronized 方法斟或,而線程B需要調(diào)用這個實例對象所屬類的靜態(tài) synchronized 方法素征,是允許的,不會發(fā)生互斥現(xiàn)象萝挤,因為訪問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖御毅,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實例對象鎖。
修飾代碼塊怜珍,指定加鎖對象端蛆,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖酥泛。?和 synchronized 方法一樣今豆,synchronized(this)代碼塊也是鎖定當(dāng)前對象的嫌拣。synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。這里再提一下:synchronized關(guān)鍵字加到非 static 靜態(tài)方法上是給對象實例上鎖呆躲。另外需要注意的是:盡量不要使用 synchronized(String a) 因為JVM中异逐,字符串常量池具有緩沖功能!
下面我已一個常見的面試題為例講解一下 synchronized 關(guān)鍵字的具體使用插掂。
面試中面試官經(jīng)常會說:“單例模式了解嗎灰瞻?來給我手寫一下!給我解釋一下雙重檢驗鎖方式實現(xiàn)單利模式的原理唄辅甥!”
雙重校驗鎖實現(xiàn)對象單例(線程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判斷對象是否已經(jīng)實例過酝润,沒有實例化過才進入加鎖代碼
if (uniqueInstance == null) {
//類對象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要璃弄。
uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的要销, uniqueInstance = new Singleton(); 這段代碼其實是分為三步執(zhí)行:
為 uniqueInstance 分配內(nèi)存空間
初始化 uniqueInstance
將 uniqueInstance 指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性,執(zhí)行順序有可能變成 1->3->2夏块。指令重排在單線程環(huán)境下不會出先問題蕉陋,但是在多線程環(huán)境下會導(dǎo)致一個線程獲得還沒有初始化的實例。例如拨扶,線程 T1 執(zhí)行了 1 和 3,此時 T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空茁肠,因此返回 uniqueInstance患民,但此時 uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排垦梆,保證在多線程環(huán)境下也能正常運行匹颤。
1.3 講一下 synchronized 關(guān)鍵字的底層原理
synchronized 關(guān)鍵字底層原理屬于 JVM 層面。
① synchronized 同步語句塊的情況
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代碼塊");
}
}
}
通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關(guān)字節(jié)碼信息:首先切換到類的對應(yīng)目錄執(zhí)行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件托猩,然后執(zhí)行javap -c -s -v -l SynchronizedDemo.class印蓖。
從上面我們可以看出:
synchronized 同步語句塊的實現(xiàn)使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代碼塊的開始位置京腥,monitorexit 指令則指明同步代碼塊的結(jié)束位置赦肃。?當(dāng)執(zhí)行 monitorenter 指令時,線程試圖獲取鎖也就是獲取 monitor(monitor對象存在于每個Java對象的對象頭中公浪,synchronized 鎖便是通過這種方式獲取鎖的他宛,也是為什么Java中任意對象可以作為鎖的原因) 的持有權(quán).當(dāng)計數(shù)器為0則可以成功獲取,獲取后將鎖計數(shù)器設(shè)為1也就是加1欠气。相應(yīng)的在執(zhí)行 monitorexit 指令后厅各,將鎖計數(shù)器設(shè)為0,表明鎖被釋放预柒。如果獲取對象鎖失敗队塘,那當(dāng)前線程就要阻塞等待袁梗,直到鎖被另外一個線程釋放為止。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令憔古,取得代之的確實是 ACC_SYNCHRONIZED 標(biāo)識遮怜,該標(biāo)識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個方法是否聲明為同步方法投放,從而執(zhí)行相應(yīng)的同步調(diào)用奈泪。
1.4 說說 JDK1.6 之后的synchronized 關(guān)鍵字底層做了哪些優(yōu)化,可以詳細(xì)介紹一下這些優(yōu)化嗎
JDK1.6 對鎖的實現(xiàn)引入了大量的優(yōu)化灸芳,如偏向鎖涝桅、輕量級鎖、自旋鎖烙样、適應(yīng)性自旋鎖冯遂、鎖消除、鎖粗化等技術(shù)來減少鎖操作的開銷谒获。
鎖主要存在四中狀態(tài)蛤肌,依次是:無鎖狀態(tài)、偏向鎖狀態(tài)批狱、輕量級鎖狀態(tài)裸准、重量級鎖狀態(tài),他們會隨著競爭的激烈而逐漸升級赔硫。注意鎖可以升級不可降級炒俱,這種策略是為了提高獲得鎖和釋放鎖的效率。
關(guān)于這幾種優(yōu)化的詳細(xì)信息可以查看:synchronized 關(guān)鍵字使用爪膊、底層原理权悟、JDK1.6 之后的底層優(yōu)化以及 和ReenTrantLock 的對比
1.5 談?wù)?synchronized和ReenTrantLock 的區(qū)別
① 兩者都是可重入鎖
兩者都是可重入鎖⊥剖ⅲ“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖峦阁。比如一個線程獲得了某個對象的鎖,此時這個對象鎖還沒有釋放耘成,當(dāng)其再次想要獲取這個對象的鎖的時候還是可以獲取的榔昔,如果不可鎖重入的話,就會造成死鎖瘪菌。同一個線程每次獲取鎖件豌,鎖的計數(shù)器都自增1,所以要等到鎖的計數(shù)器下降為0時才能釋放鎖控嗜。
② synchronized 依賴于 JVM 而 ReenTrantLock 依賴于 API
synchronized 是依賴于 JVM 實現(xiàn)的茧彤,前面我們也講到了 虛擬機團隊在 JDK1.6 為 synchronized 關(guān)鍵字進行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機層面實現(xiàn)的疆栏,并沒有直接暴露給我們曾掂。ReenTrantLock 是 JDK 層面實現(xiàn)的(也就是 API 層面惫谤,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼珠洗,來看它是如何實現(xiàn)的溜歪。
③ ReenTrantLock 比 synchronized 增加了一些高級功能
相比synchronized,ReenTrantLock增加了一些高級功能许蓖。主要來說主要有三點:①等待可中斷蝴猪;②可實現(xiàn)公平鎖;③可實現(xiàn)選擇性通知(鎖可以綁定多個條件)
ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制膊爪,通過lock.lockInterruptibly()來實現(xiàn)這個機制自阱。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情米酬。
ReenTrantLock可以指定是公平鎖還是非公平鎖沛豌。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖赃额。?ReenTrantLock默認(rèn)情況是非公平的加派,可以通過 ReenTrantLock類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。
synchronized關(guān)鍵字與wait()和notify/notifyAll()方法相結(jié)合可以實現(xiàn)等待/通知機制跳芳,ReentrantLock類當(dāng)然也可以實現(xiàn)芍锦,但是需要借助于Condition接口與newCondition() 方法。Condition是JDK1.5之后才有的飞盆,它具有很好的靈活性娄琉,比如可以實現(xiàn)多路通知功能也就是在一個Lock對象中可以創(chuàng)建多個Condition實例(即對象監(jiān)視器),線程對象可以注冊在指定的Condition中桨啃,從而可以有選擇性的進行線程通知,在調(diào)度線程上更加靈活檬输。 在使用notify/notifyAll()方法進行通知時照瘾,被通知的線程是由 JVM 選擇的,用ReentrantLock類結(jié)合Condition實例可以實現(xiàn)“選擇性通知”?丧慈,這個功能非常重要析命,而且是Condition接口默認(rèn)提供的。而synchronized關(guān)鍵字就相當(dāng)于整個Lock對象中只有一個Condition實例逃默,所有的線程都注冊在它一個身上鹃愤。如果執(zhí)行notifyAll()方法的話就會通知所有處于等待狀態(tài)的線程這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒注冊在該Condition實例中的所有等待線程完域。
如果你想使用上述功能软吐,那么選擇ReenTrantLock是一個不錯的選擇。
④ 性能已不是選擇標(biāo)準(zhǔn)
二 面試中關(guān)于線程池的 4 連擊
2.1 講一下Java內(nèi)存模型
在 JDK1.2 之前吟税,Java的內(nèi)存模型實現(xiàn)總是從主存(即共享內(nèi)存)讀取變量凹耙,是不需要進行特別的注意的姿现。而在當(dāng)前的 Java 內(nèi)存模型下,線程可以把變量保存本地內(nèi)存(比如機器的寄存器)中肖抱,而不是直接在主存中進行讀寫备典。這就可能造成一個線程在主存中修改了一個變量的值,而另外一個線程還繼續(xù)使用它在寄存器中的變量值的拷貝意述,造成數(shù)據(jù)的不一致提佣。
要解決這個問題,就需要把變量聲明為?volatile荤崇,這就指示 JVM拌屏,這個變量是不穩(wěn)定的,每次使用它都到主存中進行讀取天试。
說白了槐壳,?volatile?關(guān)鍵字的主要作用就是保證變量的可見性然后還有一個作用是防止指令重排序。
2.2 說說 synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
synchronized關(guān)鍵字和volatile關(guān)鍵字比較
volatile關(guān)鍵字是線程同步的輕量級實現(xiàn)喜每,所以volatile性能肯定比synchronized關(guān)鍵字要好务唐。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊。synchronized關(guān)鍵字在JavaSE1.6之后進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升带兜,實際開發(fā)中使用 synchronized 關(guān)鍵字的場景還是更多一些枫笛。
多線程訪問volatile關(guān)鍵字不會發(fā)生阻塞,而synchronized關(guān)鍵字可能會發(fā)生阻塞
volatile關(guān)鍵字能保證數(shù)據(jù)的可見性刚照,但不能保證數(shù)據(jù)的原子性刑巧。synchronized關(guān)鍵字兩者都能保證。
volatile關(guān)鍵字主要用于解決變量在多個線程之間的可見性无畔,而 synchronized關(guān)鍵字解決的是多個線程之間訪問資源的同步性啊楚。
三 面試中關(guān)于 線程池的 2 連擊
3.1 為什么要用線程池?
線程池提供了一種限制和管理資源(包括執(zhí)行一個任務(wù))浑彰。 每個線程池還維護一些基本統(tǒng)計信息恭理,例如已完成任務(wù)的數(shù)量。
這里借用《Java并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:
降低資源消耗郭变。?通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗颜价。
提高響應(yīng)速度。?當(dāng)任務(wù)到達時诉濒,任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行周伦。
提高線程的可管理性。?線程是稀缺資源未荒,如果無限制的創(chuàng)建专挪,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性,使用線程池可以進行統(tǒng)一的分配狈蚤,調(diào)優(yōu)和監(jiān)控困肩。
3.2 實現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務(wù)的話需要實現(xiàn)的Runnable接口或Callable接口。 Runnable接口或Callable接口實現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行脆侮。兩者的區(qū)別在于 Runnable 接口不會返回結(jié)果但是 Callable 接口可以返回結(jié)果锌畸。
備注:?工具類Executors可以實現(xiàn)Runnable對象和Callable對象之間的相互轉(zhuǎn)換。(Executors.callable(Runnable task)或Executors.callable(Runnable task靖避,Object resule))潭枣。
3.3 執(zhí)行execute()方法和submit()方法的區(qū)別是什么呢?
1)execute() 方法用于提交不需要返回值的任務(wù)幻捏,所以無法判斷任務(wù)是否被線程池執(zhí)行成功與否盆犁;
2)submit()方法用于提交需要返回值的任務(wù)。線程池會返回一個future類型的對象篡九,通過這個future對象可以判斷任務(wù)是否執(zhí)行成功谐岁,并且可以通過future的get()方法來獲取返回值,get()方法會阻塞當(dāng)前線程直到任務(wù)完成榛臼,而使用 get(long timeout伊佃,TimeUnit unit)方法則會阻塞當(dāng)前線程一段時間后立即返回,這時候有可能任務(wù)沒有執(zhí)行完沛善。
3.4 如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊》中強制線程池不允許使用 Executors 去創(chuàng)建航揉,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則金刁,規(guī)避資源耗盡的風(fēng)險**
Executors 返回線程池對象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor?: 允許請求的隊列長度為 Integer.MAX_VALUE,可能堆積大量的請求帅涂,從而導(dǎo)致OOM。
CachedThreadPool 和 ScheduledThreadPool?: 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE 尤蛮,可能會創(chuàng)建大量線程媳友,從而導(dǎo)致OOM。
方式一:通過構(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í)行完畢后缩挑,將返回線程池進行復(fù)用但两。
對應(yīng)Executors工具類中的方法如圖所示:
四 面試中關(guān)于 Atomic 原子類的 4 連擊
4.1 介紹一下Atomic 原子類
Atomic 翻譯成中文是原子的意思。在化學(xué)上供置,我們知道原子是構(gòu)成一般物質(zhì)的最小單位谨湘,在化學(xué)反應(yīng)中是不可分割的。在我們這里 Atomic 是指一個操作是不可中斷的芥丧。即使是在多個線程一起執(zhí)行的時候紧阔,一個操作一旦開始,就不會被其他線程干擾娄柳。
所以寓辱,所謂原子類說簡單點就是具有原子/原子操作特征的類。
并發(fā)包 java.util.concurrent 的原子類都存放在java.util.concurrent.atomic下,如下圖所示赤拒。
4.2 JUC 包中的原子類是哪4類?
基本類型
使用原子的方式更新基本類型
AtomicInteger:整形原子類
AtomicLong:長整型原子類
AtomicBoolean :布爾型原子類
數(shù)組類型
使用原子的方式更新數(shù)組里的某個元素
AtomicIntegerArray:整形數(shù)組原子類
AtomicLongArray:長整形數(shù)組原子類
AtomicReferenceArray :引用類型數(shù)組原子類
引用類型
AtomicReference:引用類型原子類
AtomicStampedRerence:原子更新引用類型里的字段原子類
AtomicMarkableReference :原子更新帶有標(biāo)記位的引用類型
對象的屬性修改類型
AtomicIntegerFieldUpdater:原子更新整形字段的更新器
AtomicLongFieldUpdater:原子更新長整形字段的更新器
AtomicStampedReference :原子更新帶有版本號的引用類型秫筏。該類將整數(shù)值與引用關(guān)聯(lián)起來,可用于解決原子的更新數(shù)據(jù)和數(shù)據(jù)的版本號挎挖,可以解決使用 CAS 進行原子更新時可能出現(xiàn)的 ABA 問題这敬。
4.3 講講 AtomicInteger 的使用
AtomicInteger 類常用方法
public final int get() //獲取當(dāng)前的值
public final int getAndSet(int newValue)//獲取當(dāng)前的值,并設(shè)置新的值
public final int getAndIncrement()//獲取當(dāng)前的值蕉朵,并自增
public final int getAndDecrement() //獲取當(dāng)前的值崔涂,并自減
public final int getAndAdd(int delta) //獲取當(dāng)前的值,并加上預(yù)期的值
boolean compareAndSet(int expect, int update) //如果輸入的數(shù)值等于預(yù)期值始衅,則以原子方式將該值設(shè)置為輸入值(update)
public final void lazySet(int newValue)//最終設(shè)置為newValue,使用 lazySet 設(shè)置之后可能導(dǎo)致其他線程在之后的一小段時間內(nèi)還是可以讀到舊的值冷蚂。
AtomicInteger 類的使用示例
使用 AtomicInteger 之后,不用對 increment() 方法加鎖也可以保證線程安全汛闸。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后蝙茶,不需要對該方法加鎖,也可以實現(xiàn)線程安全诸老。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
4.4 能不能給我簡單介紹一下 AtomicInteger 類的原理
AtomicInteger 線程安全原理簡單分析
AtomicInteger 類的部分源碼:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作時提供“比較并替換”的作用)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作隆夯,從而避免 synchronized 的高開銷,執(zhí)行效率大為提升。
CAS的原理是拿期望的值和原本的一個值作比較蹄衷,如果相同則更新成新的值忧额。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的內(nèi)存地址愧口,返回值是 valueOffset睦番。另外 value 是一個volatile變量,在內(nèi)存中可見耍属,因此 JVM 可以保證任何時刻任何線程總能拿到該變量的最新值抡砂。
關(guān)于 Atomic 原子類這部分更多內(nèi)容可以查看我的這篇文章:并發(fā)編程面試必備:JUC 中的 Atomic 原子類總結(jié)
五 AQS
5.1 AQS 介紹
AQS的全稱為(AbstractQueuedSynchronizer),這個類在java.util.concurrent.locks包下面恬涧。
AQS是一個用來構(gòu)建鎖和同步器的框架注益,使用AQS能簡單且高效地構(gòu)造出應(yīng)用廣泛的大量的同步器,比如我們提到的ReentrantLock溯捆,Semaphore丑搔,其他的諸如ReentrantReadWriteLock,SynchronousQueue提揍,F(xiàn)utureTask等等皆是基于AQS的啤月。當(dāng)然,我們自己也能利用AQS非常輕松容易地構(gòu)造出符合我們自己需求的同步器劳跃。
5.2 AQS 原理分析
在面試中被問到并發(fā)知識的時候谎仲,大多都會被問到“請你說一下自己對于AQS原理的理解”。下面給大家一個示例供大家參加刨仑,面試不是背題郑诺,大家一定要假如自己的思想,即使加入不了自己的思想也要保證自己能夠通俗的講出來而不是背出來杉武。
下面大部分內(nèi)容其實在AQS類注釋上已經(jīng)給出了旗笔,不過是英語看著比較吃力一點奏瞬,感興趣的話可以看看源碼。
5.2.1 AQS 原理概覽
AQS核心思想是厅克,如果被請求的共享資源空閑授舟,則將當(dāng)前請求資源的線程設(shè)置為有效的工作線程捐腿,并且將共享資源設(shè)置為鎖定狀態(tài)缕减。如果被請求的共享資源被占用笼痛,那么就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制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)進行原子操作實現(xiàn)對其值的修改味抖。
private volatile int state;//共享變量,使用volatile修飾保證線程可見性
狀態(tài)信息通過procted類型的getState灰粮,setState仔涩,compareAndSetState進行操作
//返回同步狀態(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);
}
5.2.2 AQS 對資源的共享方式
AQS定義兩種資源共享方式
Exclusive(獨占):只有一個線程能執(zhí)行,如ReentrantLock粘舟。又可分為公平鎖和非公平鎖:
公平鎖:按照線程在隊列中的排隊順序熔脂,先到者先拿到鎖
非公平鎖:當(dāng)線程要獲取鎖時,無視隊列順序直接去搶鎖柑肴,誰搶到就是誰的
Share(共享):多個線程可同時執(zhí)行霞揉,如Semaphore/CountDownLatch。Semaphore晰骑、CountDownLatCh适秩、 CyclicBarrier、ReadWriteLock 我們都會在后面講到硕舆。
ReentrantReadWriteLock 可以看成是組合式秽荞,因為ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。
不同的自定義同步器爭用共享資源的方式也不同抚官。自定義同步器在實現(xiàn)時只需要實現(xiàn)共享資源 state 的獲取與釋放方式即可扬跋,至于具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經(jīng)在頂層實現(xiàn)好了凌节。
5.2.3 AQS底層使用了模板方法模式
同步器的設(shè)計是基于模板方法模式的钦听,如果需要自定義同步器一般的方式是這樣(模板方法模式很經(jīng)典的一個應(yīng)用):
使用者繼承AbstractQueuedSynchronizer并重寫指定的方法。(這些重寫方法很簡單倍奢,無非是對于共享資源state的獲取和釋放)
將AQS組合在自定義同步組件的實現(xiàn)中彪见,并調(diào)用其模板方法,而這些模板方法會調(diào)用使用者重寫的方法娱挨。
這和我們以往通過實現(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(即釋放鎖)為止阻星,其它線程才有機會獲取該鎖朋鞍。當(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.3 AQS 組件總結(jié)
Semaphore(信號量)-允許多個線程同時訪問:?synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源衣迷。
CountDownLatch (倒計時器):?CountDownLatch是一個同步工具類畏鼓,用來協(xié)調(diào)多個線程之間的同步。這個工具通常用來控制線程等待壶谒,它可以讓某一個線程等待直到倒計時結(jié)束云矫,再開始執(zhí)行。
CyclicBarrier(循環(huán)柵欄):?CyclicBarrier 和 CountDownLatch 非常類似汗菜,它也可以實現(xiàn)線程間的技術(shù)等待让禀,但是它的功能比 CountDownLatch 更加復(fù)雜和強大。主要應(yīng)用場景和 CountDownLatch 類似陨界。CyclicBarrier 的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)巡揍。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞菌瘪,直到最后一個線程到達屏障時腮敌,屏障才會開門,所有被屏障攔截的線程才會繼續(xù)干活俏扩。CyclicBarrier默認(rèn)的構(gòu)造方法是 CyclicBarrier(int parties)缀皱,其參數(shù)表示屏障攔截的線程數(shù)量,每個線程調(diào)用await方法告訴 CyclicBarrier 我已經(jīng)到達了屏障动猬,然后當(dāng)前線程被阻塞啤斗。
加Java架構(gòu)師群獲取Java工程化、高性能及分布式赁咙、高性能钮莲、深入淺出免钻。高架構(gòu)。性能調(diào)優(yōu)崔拥、Spring极舔,MyBatis,Netty源碼分析和大數(shù)據(jù)等多個知識點高級進階干貨的直播免費學(xué)習(xí)權(quán)限 都是大牛帶飛 讓你少走很多的彎路的 群..號是:855801563 對了 小白勿進 最好是有開發(fā)經(jīng)驗
注:加群要求
1链瓦、具有工作經(jīng)驗的拆魏,面對目前流行的技術(shù)不知從何下手,需要突破技術(shù)瓶頸的可以加慈俯。
2渤刃、在公司待久了,過得很安逸贴膘,但跳槽時面試碰壁卖子。需要在短時間內(nèi)進修、跳槽拿高薪的可以加刑峡。
3洋闽、如果沒有工作經(jīng)驗,但基礎(chǔ)非常扎實突梦,對java工作機制诫舅,常用設(shè)計思想,常用java開發(fā)框架掌握熟練的宫患,可以加骚勘。
4、覺得自己很牛B撮奏,一般需求都能搞定俏讹。但是所學(xué)的知識點沒有系統(tǒng)化,很難在技術(shù)領(lǐng)域繼續(xù)突破的可以加畜吊。
5.阿里Java高級大牛直播講解知識點泽疆,分享知識,多年工作經(jīng)驗的梳理和總結(jié)玲献,帶著大家全面殉疼、科學(xué)地建立自己的技術(shù)體系和技術(shù)認(rèn)知!