1 synchronized 暴擊!
1.1 介紹下 synchronized
synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性,synchronized關(guān)鍵字可以保證被它修飾的方法或者代碼塊在任意時(shí)刻只能有一個(gè)線程執(zhí)行。
另外,在 Java 早期版本中片橡,synchronized屬于重量級(jí)鎖,效率低下逆趋,因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的哮伟,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。
如果要掛起或者喚醒一個(gè)線程陈肛,都需要操作系統(tǒng)幫忙完成揍鸟,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長的時(shí)間句旱,時(shí)間成本相對(duì)較高阳藻,這也是為什么早期的 synchronized 效率低的原因。
慶幸的是在 Java 6 之后 Java 官方對(duì)從 JVM 層面對(duì)synchronized 較大優(yōu)化谈撒,引入了大量的優(yōu)化腥泥,如自旋鎖啃匿、適應(yīng)性自旋鎖蛔外、鎖消除、鎖粗化溯乒、偏向鎖夹厌、輕量級(jí)鎖等技術(shù)來減少鎖操作的開銷,所以現(xiàn)在的 synchronized 鎖效率也優(yōu)化得很不錯(cuò)了裆悄。
1.2 實(shí)際怎么使用 synchronized 矛纹,在項(xiàng)目中用到了嗎
synchronized關(guān)鍵字最主要的三種使用方式:
- 修飾實(shí)例方法,作用于當(dāng)前對(duì)象實(shí)例加鎖灯帮,進(jìn)入同步代碼前要獲得當(dāng)前對(duì)象實(shí)例的鎖
- 修飾靜態(tài)方法崖技,作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖 钟哥。也就是給當(dāng)前類加鎖迎献,會(huì)作用于類的所有對(duì)象實(shí)例,因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象腻贰,是類成員( static 表明這是該類的一個(gè)靜態(tài)資源吁恍,不管new了多少個(gè)對(duì)象,只有一份播演,所以對(duì)該類的所有對(duì)象都加了鎖)冀瓦。所以如果一個(gè)線程A調(diào)用一個(gè)實(shí)例對(duì)象的非靜態(tài) synchronized 方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài) synchronized 方法写烤,是允許的翼闽,不會(huì)發(fā)生互斥現(xiàn)象,因?yàn)樵L問靜態(tài) synchronized 方法占用的鎖是當(dāng)前類的鎖洲炊,而訪問非靜態(tài) synchronized 方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖感局。
- 修飾代碼塊尼啡,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖询微,進(jìn)入同步代碼庫前要獲得給定對(duì)象的鎖崖瞭。 和 synchronized 方法一樣,synchronized(this)代碼塊也是鎖定當(dāng)前對(duì)象的撑毛。synchronized 關(guān)鍵字加到 static 靜態(tài)方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖书聚。這里再提一下:synchronized關(guān)鍵字加到非 static 靜態(tài)方法上是給對(duì)象實(shí)例上鎖。另外需要注意的是:盡量不要使用 synchronized(String a) 因?yàn)镴VM中藻雌,字符串常量池具有緩沖功能雌续!
面試中面試官經(jīng)常會(huì)說:“單例模式了解嗎?來給我手寫一下胯杭!給我解釋一下雙重檢驗(yàn)鎖方式實(shí)現(xiàn)單利模式的原理唄西雀!”
雙重校驗(yàn)鎖實(shí)現(xiàn)對(duì)象單例(線程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判斷對(duì)象是否已經(jīng)實(shí)例過,沒有實(shí)例化過才進(jìn)入加鎖代碼
if (uniqueInstance == null) {
//類對(duì)象加鎖
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
另外歉摧,需要注意 uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要艇肴。
uniqueInstance 采用 volatile 關(guān)鍵字修飾也是很有必要的, uniqueInstance = new Singleton(); 這段代碼其實(shí)是分為三步執(zhí)行:
- 為 uniqueInstance 分配內(nèi)存空間
- 初始化 uniqueInstance
- 將 uniqueInstance 指向分配的內(nèi)存地址
但是由于 JVM 具有指令重排的特性叁温,執(zhí)行順序有可能變成 1->3->2再悼。指令重排在單線程環(huán)境下不會(huì)出先問題,但是在多線程環(huán)境下會(huì)導(dǎo)致一個(gè)線程獲得還沒有初始化的實(shí)例膝但。例如冲九,線程 T1 執(zhí)行了 1 和 3,此時(shí) T2 調(diào)用 getUniqueInstance() 后發(fā)現(xiàn) uniqueInstance 不為空跟束,因此返回 uniqueInstance莺奸,但此時(shí) uniqueInstance 還未被初始化。
使用 volatile 可以禁止 JVM 的指令重排冀宴,保證在多線程環(huán)境下也能正常運(yù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é)碼信息
從上面我們可以看出:
synchronized 同步語句塊的實(shí)現(xiàn)使用的是 monitorenter 和 monitorexit 指令略贮,其中 monitorenter 指令指向同步代碼塊的開始位置甚疟,monitorexit 指令則指明同步代碼塊的結(jié)束位置。 當(dāng)執(zhí)行 monitorenter 指令時(shí)逃延,線程試圖獲取鎖也就是獲取 monitor(monitor對(duì)象存在于每個(gè)Java對(duì)象的對(duì)象頭中览妖,synchronized 鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對(duì)象可以作為鎖的原因) 的持有權(quán).當(dāng)計(jì)數(shù)器為0則可以成功獲取揽祥,獲取后將鎖計(jì)數(shù)器設(shè)為1也就是加1讽膏。相應(yīng)的在執(zhí)行 monitorexit 指令后,將鎖計(jì)數(shù)器設(shè)為0拄丰,表明鎖被釋放府树。如果獲取對(duì)象鎖失敗是嗜,那當(dāng)前線程就要阻塞等待,直到鎖被另外一個(gè)線程釋放為止挺尾。
② synchronized 修飾方法的的情況
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令歪玲,取得代之的確實(shí)是 ACC_SYNCHRONIZED 標(biāo)識(shí)冈敛,該標(biāo)識(shí)指明了該方法是一個(gè)同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標(biāo)志來辨別一個(gè)方法是否聲明為同步方法振乏,從而執(zhí)行相應(yīng)的同步調(diào)用恢准。
1.4 說說 JDK1.6 之后的synchronized 關(guān)鍵字底層做了哪些優(yōu)化魂挂,可以詳細(xì)介紹一下這些優(yōu)化嗎
JDK1.6 對(duì)鎖的實(shí)現(xiàn)引入了大量的優(yōu)化,如偏向鎖馁筐、輕量級(jí)鎖涂召、自旋鎖、適應(yīng)性自旋鎖敏沉、鎖消除果正、鎖粗化等技術(shù)來減少鎖操作的開銷。
鎖主要存在四中狀態(tài)盟迟,依次是:無鎖狀態(tài)秋泳、偏向鎖狀態(tài)、輕量級(jí)鎖狀態(tài)攒菠、重量級(jí)鎖狀態(tài)迫皱,他們會(huì)隨著競(jìng)爭(zhēng)的激烈而逐漸升級(jí)。注意鎖可以升級(jí)不可降級(jí)辖众,這種策略是為了提高獲得鎖和釋放鎖的效率卓起。
1.5 談?wù)?synchronized和ReenTrantLock 的區(qū)別
① 兩者都是可重入鎖
兩者都是可重入鎖“颊ǎ“可重入鎖”概念是:自己可以再次獲取自己的內(nèi)部鎖戏阅。比如一個(gè)線程獲得了某個(gè)對(duì)象的鎖,此時(shí)這個(gè)對(duì)象鎖還沒有釋放啤它,當(dāng)其再次想要獲取這個(gè)對(duì)象的鎖的時(shí)候還是可以獲取的饲握,如果不可鎖重入的話,就會(huì)造成死鎖蚕键。同一個(gè)線程每次獲取鎖救欧,鎖的計(jì)數(shù)器都自增1,所以要等到鎖的計(jì)數(shù)器下降為0時(shí)才能釋放鎖锣光。
② synchronized 依賴于 JVM 而 ReenTrantLock 依賴于 API
synchronized 是依賴于 JVM 實(shí)現(xiàn)的笆怠,前面我們也講到了 虛擬機(jī)團(tuán)隊(duì)在 JDK1.6 為 synchronized 關(guān)鍵字進(jìn)行了很多優(yōu)化,但是這些優(yōu)化都是在虛擬機(jī)層面實(shí)現(xiàn)的誊爹,并沒有直接暴露給我們蹬刷。ReenTrantLock 是 JDK 層面實(shí)現(xiàn)的(也就是 API 層面瓢捉,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源代碼办成,來看它是如何實(shí)現(xiàn)的泡态。
③ ReenTrantLock 比 synchronized 增加了一些高級(jí)功能
相比synchronized,ReenTrantLock增加了一些高級(jí)功能迂卢。主要來說主要有三點(diǎn):①等待可中斷某弦;②可實(shí)現(xiàn)公平鎖;③可實(shí)現(xiàn)選擇性通知(鎖可以綁定多個(gè)條件)
- ReenTrantLock提供了一種能夠中斷等待鎖的線程的機(jī)制而克,通過lock.lockInterruptibly()來實(shí)現(xiàn)這個(gè)機(jī)制靶壮。也就是說正在等待的線程可以選擇放棄等待,改為處理其他事情员萍。
- ReenTrantLock可以指定是公平鎖還是非公平鎖腾降。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖碎绎。 ReenTrantLock默認(rèn)情況是非公平的螃壤,可以通過 ReenTrantLock類的ReentrantLock(boolean fair)構(gòu)造方法來制定是否是公平的。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈筋帖。交流學(xué)習(xí)企鵝群號(hào):948368769(里面有大量的面試題及答案)里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring映穗,MyBatis,Netty源碼分析幕随,高并發(fā)蚁滋、高性能、分布式赘淮、微服務(wù)架構(gòu)的原理辕录,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系梢卸。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源走诞,目前受益良多
- synchronized關(guān)鍵字與wait()和notify/notifyAll()方法相結(jié)合可以實(shí)現(xiàn)等待/通知機(jī)制,ReentrantLock類當(dāng)然也可以實(shí)現(xiàn)蛤高,但是需要借助于Condition接口與newCondition() 方法蚣旱。Condition是JDK1.5之后才有的,它具有很好的靈活性戴陡,比如可以實(shí)現(xiàn)多路通知功能也就是在一個(gè)Lock對(duì)象中可以創(chuàng)建多個(gè)Condition實(shí)例(即對(duì)象監(jiān)視器)塞绿,線程對(duì)象可以注冊(cè)在指定的Condition中,從而可以有選擇性的進(jìn)行線程通知恤批,在調(diào)度線程上更加靈活异吻。 在使用notify/notifyAll()方法進(jìn)行通知時(shí),被通知的線程是由 JVM 選擇的,用ReentrantLock類結(jié)合Condition實(shí)例可以實(shí)現(xiàn)“選擇性通知” 诀浪,這個(gè)功能非常重要棋返,而且是Condition接口默認(rèn)提供的。而synchronized關(guān)鍵字就相當(dāng)于整個(gè)Lock對(duì)象中只有一個(gè)Condition實(shí)例雷猪,所有的線程都注冊(cè)在它一個(gè)身上睛竣。如果執(zhí)行notifyAll()方法的話就會(huì)通知所有處于等待狀態(tài)的線程這樣會(huì)造成很大的效率問題,而Condition實(shí)例的signalAll()方法 只會(huì)喚醒注冊(cè)在該Condition實(shí)例中的所有等待線程求摇。
如果你想使用上述功能射沟,那么選擇ReenTrantLock是一個(gè)不錯(cuò)的選擇。
④ 性能已不是選擇標(biāo)準(zhǔn)
2 線程池
2.1 講一下Java內(nèi)存模型
在 JDK1.2 之前月帝,Java的內(nèi)存模型實(shí)現(xiàn)總是從主存(即共享內(nèi)存)讀取變量,是不需要進(jìn)行特別的注意的幽污。而在當(dāng)前的 Java 內(nèi)存模型下嚷辅,線程可以把變量保存本地內(nèi)存(比如機(jī)器的寄存器)中,而不是直接在主存中進(jìn)行讀寫距误。這就可能造成一個(gè)線程在主存中修改了一個(gè)變量的值簸搞,而另外一個(gè)線程還繼續(xù)使用它在寄存器中的變量值的拷貝,造成數(shù)據(jù)的不一致准潭。
要解決這個(gè)問題趁俊,就需要把變量聲明為volatile,這就指示 JVM刑然,這個(gè)變量是不穩(wěn)定的寺擂,每次使用它都到主存中進(jìn)行讀取。
說白了泼掠,volatile關(guān)鍵字的主要作用就是保證變量的可見性然后還有一個(gè)作用是防止指令重排序怔软。
2.2 說說 synchronized 關(guān)鍵字和 volatile 關(guān)鍵字的區(qū)別
synchronized關(guān)鍵字和volatile關(guān)鍵字比較
- volatile關(guān)鍵字是線程同步的輕量級(jí)實(shí)現(xiàn),所以volatile性能肯定比synchronized關(guān)鍵字要好择镇。但是volatile關(guān)鍵字只能用于變量而synchronized關(guān)鍵字可以修飾方法以及代碼塊挡逼。synchronized關(guān)鍵字在JavaSE1.6之后進(jìn)行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級(jí)鎖以及其它各種優(yōu)化之后執(zhí)行效率有了顯著提升,實(shí)際開發(fā)中使用 synchronized 關(guān)鍵字的場(chǎng)景還是更多一些腻豌。
- 多線程訪問volatile關(guān)鍵字不會(huì)發(fā)生阻塞家坎,而synchronized關(guān)鍵字可能會(huì)發(fā)生阻塞
- volatile關(guān)鍵字能保證數(shù)據(jù)的可見性,但不能保證數(shù)據(jù)的原子性吝梅。synchronized關(guān)鍵字兩者都能保證虱疏。
- volatile關(guān)鍵字主要用于解決變量在多個(gè)線程之間的可見性,而 synchronized關(guān)鍵字解決的是多個(gè)線程之間訪問資源的同步性苏携。
三 面試中關(guān)于 線程池的 2 連擊
3.1 為什么要用線程池订框?
線程池提供了一種限制和管理資源(包括執(zhí)行一個(gè)任務(wù))。 每個(gè)線程池還維護(hù)一些基本統(tǒng)計(jì)信息兜叨,例如已完成任務(wù)的數(shù)量穿扳。
這里借用《Java并發(fā)編程的藝術(shù)》提到的來說一下使用線程池的好處:
- 降低資源消耗衩侥。 通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。
- 提高響應(yīng)速度矛物。 當(dāng)任務(wù)到達(dá)時(shí)茫死,任務(wù)可以不需要的等到線程創(chuàng)建就能立即執(zhí)行。
- 提高線程的可管理性履羞。 線程是稀缺資源峦萎,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源忆首,還會(huì)降低系統(tǒng)的穩(wěn)定性爱榔,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控糙及。
3.2 實(shí)現(xiàn)Runnable接口和Callable接口的區(qū)別
如果想讓線程池執(zhí)行任務(wù)的話需要實(shí)現(xiàn)的Runnable接口或Callable接口详幽。 Runnable接口或Callable接口實(shí)現(xiàn)類都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor執(zhí)行。兩者的區(qū)別在于 Runnable 接口不會(huì)返回結(jié)果但是 Callable 接口可以返回結(jié)果浸锨。
備注: 工具類Executors可以實(shí)現(xiàn)Runnable對(duì)象和Callable對(duì)象之間的相互轉(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ù)宪肖。線程池會(huì)返回一個(gè)future類型的對(duì)象,通過這個(gè)future對(duì)象可以判斷任務(wù)是否執(zhí)行成功健爬,并且可以通過future的get()方法來獲取返回值匈庭,get()方法會(huì)阻塞當(dāng)前線程直到任務(wù)完成,而使用get(long timeout浑劳,TimeUnit unit)方法則會(huì)阻塞當(dāng)前線程一段時(shí)間后立即返回阱持,這時(shí)候有可能任務(wù)沒有執(zhí)行完。
3.4 如何創(chuàng)建線程池
《阿里巴巴Java開發(fā)手冊(cè)》中強(qiáng)制線程池不允許使用 Executors 去創(chuàng)建魔熏,而是通過 ThreadPoolExecutor 的方式衷咽,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)**
Executors 返回線程池對(duì)象的弊端如下:
- FixedThreadPool 和 SingleThreadExecutor : 允許請(qǐng)求的隊(duì)列長度為 Integer.MAX_VALUE,可能堆積大量的請(qǐng)求蒜绽,從而導(dǎo)致OOM镶骗。
- CachedThreadPool 和 ScheduledThreadPool : 允許創(chuàng)建的線程數(shù)量為 Integer.MAX_VALUE ,可能會(huì)創(chuàng)建大量線程躲雅,從而導(dǎo)致OOM鼎姊。
方式一:通過構(gòu)造方法實(shí)現(xiàn)
方式二:通過Executor 框架的工具類Executors來實(shí)現(xiàn)
我們可以創(chuàng)建三種類型的ThreadPoolExecutor:
- FixedThreadPool : 該方法返回一個(gè)固定線程數(shù)量的線程池。該線程池中的線程數(shù)量始終不變。當(dāng)有一個(gè)新的任務(wù)提交時(shí)相寇,線程池中若有空閑線程慰于,則立即執(zhí)行。若沒有唤衫,則新的任務(wù)會(huì)被暫存在一個(gè)任務(wù)隊(duì)列中婆赠,待有線程空閑時(shí),便處理在任務(wù)隊(duì)列中的任務(wù)佳励。
- SingleThreadExecutor: 方法返回一個(gè)只有一個(gè)線程的線程池休里。若多余一個(gè)任務(wù)被提交到該線程池,任務(wù)會(huì)被保存在一個(gè)任務(wù)隊(duì)列中赃承,待線程空閑妙黍,按先入先出的順序執(zhí)行隊(duì)列中的任務(wù)。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈瞧剖。交流學(xué)習(xí)企鵝群號(hào):948368769(里面有大量的面試題及答案)里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring拭嫁,MyBatis,Netty源碼分析筒繁,高并發(fā)噩凹、高性能巴元、分布式毡咏、微服務(wù)架構(gòu)的原理,JVM性能優(yōu)化逮刨、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系呕缭。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源,目前受益良多
- CachedThreadPool: 該方法返回一個(gè)可根據(jù)實(shí)際情況調(diào)整線程數(shù)量的線程池修己。線程池的線程數(shù)量不確定恢总,但若有空閑線程可以復(fù)用,則會(huì)優(yōu)先使用可復(fù)用的線程睬愤。若所有線程均在工作片仿,又有新的任務(wù)提交,則會(huì)創(chuàng)建新的線程處理任務(wù)尤辱。所有線程在當(dāng)前任務(wù)執(zhí)行完畢后砂豌,將返回線程池進(jìn)行復(fù)用。
對(duì)應(yīng)Executors工具類中的方法如圖所示:
4 Atomic 原子類
4.1 介紹一下Atomic 原子類
Atomic 翻譯成中文是原子的意思光督。在化學(xué)上阳距,我們知道原子是構(gòu)成一般物質(zhì)的最小單位,在化學(xué)反應(yīng)中是不可分割的结借。在我們這里 Atomic 是指一個(gè)操作是不可中斷的筐摘。即使是在多個(gè)線程一起執(zhí)行的時(shí)候,一個(gè)操作一旦開始,就不會(huì)被其他線程干擾咖熟。
所以圃酵,所謂原子類說簡(jiǎn)單點(diǎn)就是具有原子/原子操作特征的類。
并發(fā)包java.util.concurrent的原子類都存放在java.util.concurrent.atomic下,如下圖所示球恤。
4.2 JUC 包中的原子類是哪4類?
基本類型
使用原子的方式更新基本類型
- AtomicInteger:整形原子類
- AtomicLong:長整型原子類
- AtomicBoolean :布爾型原子類
數(shù)組類型
使用原子的方式更新數(shù)組里的某個(gè)元素
- AtomicIntegerArray:整形數(shù)組原子類
- AtomicLongArray:長整形數(shù)組原子類
- AtomicReferenceArray :引用類型數(shù)組原子類
引用類型
- AtomicReference:引用類型原子類
- AtomicStampedRerence:原子更新引用類型里的字段原子類
- AtomicMarkableReference :原子更新帶有標(biāo)記位的引用類型
對(duì)象的屬性修改類型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新長整形字段的更新器
- AtomicStampedReference :原子更新帶有版本號(hào)的引用類型辜昵。該類將整數(shù)值與引用關(guān)聯(lián)起來,可用于解決原子的更新數(shù)據(jù)和數(shù)據(jù)的版本號(hào)咽斧,可以解決使用 CAS 進(jìn)行原子更新時(shí)可能出現(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)致其他線程在之后的一小段時(shí)間內(nèi)還是可以讀到舊的值坎匿。</pre>
AtomicInteger 類的使用示例
使用 AtomicInteger 之后,不用對(duì) increment() 方法加鎖也可以保證線程安全雷激。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使用AtomicInteger之后替蔬,不需要對(duì)該方法加鎖,也可以實(shí)現(xiàn)線程安全屎暇。
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
4.4 能不能給我簡(jiǎn)單介紹一下 AtomicInteger 類的原理
AtomicInteger 線程安全原理簡(jiǎn)單分析
AtomicInteger 類的部分源碼:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作時(shí)提供“比較并替換”的作用)
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的原理是拿期望的值和原本的一個(gè)值作比較凶异,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個(gè)本地方法挤巡,這個(gè)方法是用來拿到“原來的值”的內(nèi)存地址剩彬,返回值是 valueOffset。另外 value 是一個(gè)volatile變量矿卑,在內(nèi)存中可見喉恋,因此 JVM 可以保證任何時(shí)刻任何線程總能拿到該變量的最新值。
5 AQS
5.1 AQS 介紹
AQS的全稱為(AbstractQueuedSynchronizer)母廷,這個(gè)類在java.util.concurrent.locks包下面轻黑。
AQS是一個(gè)用來構(gòu)建鎖和同步器的框架,使用AQS能簡(jiǎn)單且高效地構(gòu)造出應(yīng)用廣泛的大量的同步器徘意,比如我們提到的ReentrantLock苔悦,Semaphore,其他的諸如ReentrantReadWriteLock椎咧,SynchronousQueue玖详,F(xiàn)utureTask等等皆是基于AQS的把介。當(dāng)然,我們自己也能利用AQS非常輕松容易地構(gòu)造出符合我們自己需求的同步器蟋座。
5.2 AQS 原理分析
AQS 原理這部分參考了部分博客拗踢,在5.2節(jié)末尾放了鏈接。
在面試中被問到并發(fā)知識(shí)的時(shí)候向臀,大多都會(huì)被問到“請(qǐng)你說一下自己對(duì)于AQS原理的理解”巢墅。下面給大家一個(gè)示例供大家參加,面試不是背題券膀,大家一定要假如自己的思想君纫,即使加入不了自己的思想也要保證自己能夠通俗的講出來而不是背出來。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流圈芹彬。交流學(xué)習(xí)企鵝群號(hào):948368769(里面有大量的面試題及答案)里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring蓄髓,MyBatis,Netty源碼分析舒帮,高并發(fā)会喝、高性能、分布式玩郊、微服務(wù)架構(gòu)的原理肢执,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備的知識(shí)體系译红。還能領(lǐng)取免費(fèi)的學(xué)習(xí)資源预茄,目前受益良多
下面大部分內(nèi)容其實(shí)在AQS類注釋上已經(jīng)給出了,不過是英語看著比較吃力一點(diǎn)临庇,感興趣的話可以看看源碼反璃。
5.2.1 AQS 原理概覽
AQS核心思想是昵慌,如果被請(qǐng)求的共享資源空閑假夺,則將當(dāng)前請(qǐng)求資源的線程設(shè)置為有效的工作線程,并且將共享資源設(shè)置為鎖定狀態(tài)斋攀。如果被請(qǐng)求的共享資源被占用已卷,那么就需要一套線程阻塞等待以及被喚醒時(shí)鎖分配的機(jī)制,這個(gè)機(jī)制AQS是用CLH隊(duì)列鎖實(shí)現(xiàn)的淳蔼,即將暫時(shí)獲取不到鎖的線程加入到隊(duì)列中侧蘸。
CLH(Craig,Landin,and Hagersten)隊(duì)列是一個(gè)虛擬的雙向隊(duì)列(虛擬的雙向隊(duì)列即不存在隊(duì)列實(shí)例,僅存在結(jié)點(diǎn)之間的關(guān)聯(lián)關(guān)系)鹉梨。AQS是將每條請(qǐng)求共享資源的線程封裝成一個(gè)CLH鎖隊(duì)列的一個(gè)結(jié)點(diǎn)(Node)來實(shí)現(xiàn)鎖的分配讳癌。
看個(gè)AQS(AbstractQueuedSynchronizer)原理圖:
AQS使用一個(gè)int成員變量來表示同步狀態(tài),通過內(nèi)置的FIFO隊(duì)列來完成獲取資源線程的排隊(duì)工作存皂。AQS使用CAS對(duì)該同步狀態(tài)進(jìn)行原子操作實(shí)現(xiàn)對(duì)其值的修改晌坤。
private volatile int state;//共享變量逢艘,使用volatile修飾保證線程可見性
狀態(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);
}
5.2.2 AQS 對(duì)資源的共享方式
AQS定義兩種資源共享方式
-
Exclusive(獨(dú)占):只有一個(gè)線程能執(zhí)行它改,如ReentrantLock。又可分為公平鎖和非公平鎖:
- 公平鎖:按照線程在隊(duì)列中的排隊(duì)順序商乎,先到者先拿到鎖
- 非公平鎖:當(dāng)線程要獲取鎖時(shí)央拖,無視隊(duì)列順序直接去搶鎖,誰搶到就是誰的
- Share(共享):多個(gè)線程可同時(shí)執(zhí)行鹉戚,如Semaphore/CountDownLatch鲜戒。Semaphore、CountDownLatCh抹凳、 CyclicBarrier袍啡、ReadWriteLock 我們都會(huì)在后面講到。
ReentrantReadWriteLock 可以看成是組合式却桶,因?yàn)镽eentrantReadWriteLock也就是讀寫鎖允許多個(gè)線程同時(shí)對(duì)某一資源進(jìn)行讀境输。
不同的自定義同步器爭(zhēng)用共享資源的方式也不同。自定義同步器在實(shí)現(xiàn)時(shí)只需要實(shí)現(xiàn)共享資源 state 的獲取與釋放方式即可颖系,至于具體線程等待隊(duì)列的維護(hù)(如獲取資源失敗入隊(duì)/喚醒出隊(duì)等)嗅剖,AQS已經(jīng)在頂層實(shí)現(xiàn)好了。
5.2.3 AQS底層使用了模板方法模式
同步器的設(shè)計(jì)是基于模板方法模式的嘁扼,如果需要自定義同步器一般的方式是這樣(模板方法模式很經(jīng)典的一個(gè)應(yīng)用):
- 使用者繼承AbstractQueuedSynchronizer并重寫指定的方法信粮。(這些重寫方法很簡(jiǎn)單,無非是對(duì)于共享資源state的獲取和釋放)
- 將AQS組合在自定義同步組件的實(shí)現(xiàn)中趁啸,并調(diào)用其模板方法强缘,而這些模板方法會(huì)調(diào)用使用者重寫的方法。
這和我們以往通過實(shí)現(xiàn)接口的方式有很大區(qū)別不傅,這是模板方法模式很經(jīng)典的一個(gè)運(yùn)用旅掂。
AQS使用了模板方法模式,自定義同步器時(shí)需要重寫下面幾個(gè)AQS提供的模板方法:
isHeldExclusively()//該線程是否正在獨(dú)占資源访娶。只有用到condition才需要去實(shí)現(xiàn)它商虐。
tryAcquire(int)//獨(dú)占方式。嘗試獲取資源崖疤,成功則返回true秘车,失敗則返回false。
tryRelease(int)//獨(dú)占方式劫哼。嘗試釋放資源叮趴,成功則返回true,失敗則返回false权烧。
tryAcquireShared(int)//共享方式眯亦。嘗試獲取資源咳蔚。負(fù)數(shù)表示失敗搔驼;0表示成功谈火,但沒有剩余可用資源;正數(shù)表示成功舌涨,且有剩余資源糯耍。
tryReleaseShared(int)//共享方式。嘗試釋放資源囊嘉,成功則返回true温技,失敗則返回false。
默認(rèn)情況下扭粱,每個(gè)方法都拋出UnsupportedOperationException舵鳞。 這些方法的實(shí)現(xiàn)必須是內(nèi)部線程安全的,并且通常應(yīng)該簡(jiǎn)短而不是阻塞琢蛤。AQS類中的其他方法都是final 蜓堕,所以無法被其他類使用,只有這幾個(gè)方法可以被其他類使用博其。
以ReentrantLock為例套才,state初始化為0,表示未鎖定狀態(tài)慕淡。A線程lock()時(shí)背伴,會(huì)調(diào)用tryAcquire()獨(dú)占該鎖并將state+1。此后峰髓,其他線程再tryAcquire()時(shí)就會(huì)失敗傻寂,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機(jī)會(huì)獲取該鎖携兵。當(dāng)然疾掰,釋放鎖之前,A線程自己是可以重復(fù)獲取此鎖的(state會(huì)累加)眉孩,這就是可重入的概念个绍。但要注意勒葱,獲取多少次就要釋放多么次浪汪,這樣才能保證state是能回到零態(tài)的。
再以CountDownLatch以例凛虽,任務(wù)分為N個(gè)子線程去執(zhí)行死遭,state也初始化為N(注意N要與線程個(gè)數(shù)一致)。這N個(gè)子線程是并行執(zhí)行的凯旋,每個(gè)子線程執(zhí)行完后countDown()一次呀潭,state會(huì)CAS(Compare and Swap)減1钉迷。等到所有子線程都執(zhí)行完后(即state=0),會(huì)unpark()主調(diào)用線程钠署,然后主調(diào)用線程就會(huì)從await()函數(shù)返回糠聪,繼續(xù)后余動(dòng)作。
一般來說谐鼎,自定義同步器要么是獨(dú)占方法舰蟆,要么是共享方式,他們也只需實(shí)現(xiàn)tryAcquire-tryRelease狸棍、tryAcquireShared-tryReleaseShared中的一種即可身害。但AQS也支持自定義同步器同時(shí)實(shí)現(xiàn)獨(dú)占和共享兩種方式,如ReentrantReadWriteLock草戈。
5.3 AQS 組件總結(jié)
- Semaphore(信號(hào)量)-允許多個(gè)線程同時(shí)訪問: synchronized 和 ReentrantLock 都是一次只允許一個(gè)線程訪問某個(gè)資源塌鸯,Semaphore(信號(hào)量)可以指定多個(gè)線程同時(shí)訪問某個(gè)資源。
- CountDownLatch (倒計(jì)時(shí)器): CountDownLatch是一個(gè)同步工具類唐片,用來協(xié)調(diào)多個(gè)線程之間的同步丙猬。這個(gè)工具通常用來控制線程等待,它可以讓某一個(gè)線程等待直到倒計(jì)時(shí)結(jié)束费韭,再開始執(zhí)行淮悼。
- CyclicBarrier(循環(huán)柵欄): CyclicBarrier 和 CountDownLatch 非常類似,它也可以實(shí)現(xiàn)線程間的技術(shù)等待揽思,但是它的功能比 CountDownLatch 更加復(fù)雜和強(qiáng)大袜腥。主要應(yīng)用場(chǎng)景和 CountDownLatch 類似。CyclicBarrier 的字面意思是可循環(huán)使用(Cyclic)的屏障(Barrier)钉汗。它要做的事情是羹令,讓一組線程到達(dá)一個(gè)屏障(也可以叫同步點(diǎn))時(shí)被阻塞,直到最后一個(gè)線程到達(dá)屏障時(shí)损痰,屏障才會(huì)開門福侈,所有被屏障攔截的線程才會(huì)繼續(xù)干活。CyclicBarrier默認(rèn)的構(gòu)造方法是 CyclicBarrier(int parties)卢未,其參數(shù)表示屏障攔截的線程數(shù)量肪凛,每個(gè)線程調(diào)用await方法告訴 CyclicBarrier 我已經(jīng)到達(dá)了屏障,然后當(dāng)前線程被阻塞辽社。