volatile硬猫、synchronized补箍、lock詳解
1改执、volatile
被volatile定義的變量被一個(gè)線程修改后,另一個(gè)線程可以感知到坑雅。
能夠保證讀的準(zhǔn)確性辈挂,不能保證寫的準(zhǔn)確性
1.1 機(jī)理
volatile意味著可見性。先看一個(gè)例子:
static class MyRunnable implements Runnable {
private boolean isRun = true;
public void setRun(boolean run) {
isRun = run;
}
@Override
public void run() {
System.out.println("線程1 開始運(yùn)行..." + DateUtils.INSTANCE.getCurrDataStr());
// 當(dāng) isRun 為true時(shí)裹粤,會(huì)一直循環(huán)终蒂。直至isRun為false
while (isRun) {
}
System.out.println("線程1 運(yùn)行結(jié)束了..." + DateUtils.INSTANCE.getCurrDataStr());
}
}
再另啟線程2去改變 isRun 的值,讓線程1退出循環(huán)
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
MyRunnable myRunnable = new MyRunnable();
executorService.execute(myRunnable);
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("線程2 開始運(yùn)行..." + DateUtils.INSTANCE.getCurrDataStr());
try {
Thread.sleep(1000);
System.out.println("線程2 等待2s后遥诉,去停止運(yùn)行線程1..." + DateUtils.INSTANCE.getCurrDataStr());
myRunnable.setRun(false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
打印的結(jié)果可以看到拇泣,線程1并沒有退出循環(huán)。因?yàn)榫€程2將 isRun 變量讀取到它的內(nèi)存空間進(jìn)行修改后矮锈,寫入主內(nèi)存霉翔,但由于線程1一直在私有棧中讀取 isRun 變量,沒有在主內(nèi)存中讀取 isRun 變量愕难,因此不會(huì)退出循環(huán)早龟。
如果我們把isRun賦值行改為:
private volatile boolean isRun = true;
將其用 volatile 修飾惫霸,則強(qiáng)制該變量從主內(nèi)存中讀取猫缭。
這樣我們也就明白了volatile的實(shí)現(xiàn)機(jī)理,即:
- 當(dāng)一個(gè)線程要使用 volatile 變量時(shí)壹店,它會(huì)直接從主內(nèi)存中讀取猜丹,而不使用自己工作內(nèi)存中的副本。
- 當(dāng)一個(gè)線程對(duì)一個(gè) volatile 變量寫時(shí)硅卢,它會(huì)將變量的值刷新到共享內(nèi)存(主內(nèi)存)中射窒。
1.2 非原子性
原子性是指,對(duì)于一個(gè)操作将塑,其操作的內(nèi)容只有全部執(zhí)行/全不執(zhí)行兩個(gè)狀態(tài)脉顿,不存在中間態(tài)。而volatile不能鎖定某組操作点寥,防止其他線程的干擾艾疟,即沒有其他線程的干擾,所以volatile是非原子性的敢辩。也就是非線程安全的蔽莱。
所以如果想要使用一個(gè)原子性的修飾符來控制操作,即在操作變量時(shí)鎖定變量戚长,我們就需要另一個(gè)修飾詞synchronized盗冷。
2、synchronized
synchronized作用的代碼范圍對(duì)于不同線程是互斥的同廉,并且線程子啊釋放鎖的時(shí)候會(huì)將共享變量的值刷新到共享內(nèi)存中仪糖。
對(duì)象設(shè)置:
修飾代碼塊時(shí)柑司,需1個(gè)reference對(duì)象作為鎖的對(duì)象
修飾實(shí)例方法時(shí),默認(rèn)的鎖對(duì)象 = 當(dāng)前對(duì)象
修飾類方法(靜態(tài))時(shí)锅劝,默認(rèn)的鎖對(duì)象 = 當(dāng)前類的Class對(duì)象
2.1 synchronized與volatile的區(qū)別
1帜羊、使用:
volatile只可以修飾變量,synchronized可以修飾對(duì)象鸠天,類讼育,方法,代碼塊稠集,語(yǔ)句奶段。
2、原子性:
volatile是非原子性的剥纷,只能保證讀的準(zhǔn)確性痹籍,不能保證寫的準(zhǔn)確性。多線程并發(fā)訪問變量時(shí)晦鞋,不會(huì)產(chǎn)生阻塞蹲缠。synchronized是原子性的,只有獲取的鎖的線程才能進(jìn)入臨界區(qū)悠垛,從而保證了臨界區(qū)的所有語(yǔ)句全部執(zhí)行线定。多線程并發(fā)訪問會(huì)產(chǎn)生阻塞。
3确买、機(jī)理:
當(dāng)線程對(duì)volatile變量讀時(shí)斤讥,會(huì)把工作內(nèi)存中的值置為無效。當(dāng)線程對(duì)synchronized變量讀時(shí)湾趾,會(huì)在改線程鎖定變量時(shí)將工作內(nèi)存中的變量置為無效芭商。
當(dāng)線程對(duì)volalite變量寫時(shí),會(huì)把修改的值刷新到主內(nèi)存搀缠。當(dāng)線程對(duì)synchronized變量寫時(shí)铛楣,會(huì)在線程釋放鎖的時(shí)候,將修改的值刷新到主內(nèi)存艺普。
2.2 注意點(diǎn)
1簸州、無論synchronized加在方法上還是對(duì)象上,其修飾的都是對(duì)象衷敌,而不是方法或者某個(gè)代碼塊代碼語(yǔ)句
2勿侯、每個(gè)對(duì)象只有一個(gè)鎖與之相對(duì)聯(lián)
3、實(shí)現(xiàn)同步需要很大的系統(tǒng)開銷來做控制缴罗,不要做無謂的鎖定
2.3 synchronized的作用域
synchronized的作用域只有兩種助琐。實(shí)際上,synchronized直接作用于內(nèi)存中的一個(gè)內(nèi)存塊面氓,因此兵钮,可以通過鎖定內(nèi)存塊來鎖定一個(gè)實(shí)例變量或者鎖定一個(gè)靜態(tài)區(qū)域蛆橡。
1、某個(gè)對(duì)象實(shí)例內(nèi)
synchronized aMethod(){} 可以防止多個(gè)線程同時(shí)訪問這個(gè)對(duì)象的synchronized方法掘譬,如果對(duì)象有多個(gè)synchronized方法泰演,則只要一個(gè)線程訪問了任何一個(gè)synchronized方法,則其他線程都不能同時(shí)訪問任何一個(gè)該對(duì)象的synchronized方法葱轩。(synchronized作用于對(duì)象睦焕,且每個(gè)對(duì)象只有一個(gè)鎖)
2、某個(gè)類的范圍
又或者說是作用于靜態(tài)方法/靜態(tài)代碼塊靴拱。synchronized static aMethod(){} 可以防止多個(gè)線程同時(shí)訪問這個(gè)類中的synchronized static 方法垃喊,它可以對(duì)類的所有實(shí)例對(duì)象起作用。
2.4 synchronized應(yīng)用
2.4.1 synchronized方法
每個(gè)實(shí)例對(duì)應(yīng)一個(gè)lock袜炕,線程后的該含有synchronized方法的實(shí)例的鎖才可以執(zhí)行本谜,否則一直阻塞。方法一旦執(zhí)行偎窘,則一直到方法返回才可以釋放鎖乌助。此后被阻塞的線程才能獲得該鎖。對(duì)于一個(gè)實(shí)例陌知,其聲明為synchronized的方法顯然只有一個(gè)能處于執(zhí)行狀態(tài)他托。從而避免了類訪問變量的沖突。
synchronized同步的開銷很大纵诞,如果synchronized作用于一個(gè)比較大的方法上上祈,顯然是不合算的。
2.4.2 synchronized代碼塊
synchronized代碼塊形式如下:
synchronized (synchronizedObject){
//Some thing
}
代碼塊內(nèi)部代碼必須在獲得synchronizedObject的鎖時(shí)才能執(zhí)行浙芙。需要重點(diǎn)說的是synchronized(this),這也是比較常用的代碼塊籽腕。
synchronized的效果類似于在方法前修飾嗡呼,只是修飾的方位縮小成代碼塊。兩個(gè)線程同時(shí)訪問一個(gè)變量時(shí)皇耗,如果一個(gè)線程在執(zhí)行synchronized的代碼南窗,那么該實(shí)例被鎖定,另一個(gè)線程如果要訪問該實(shí)例被synchronized作用的范圍郎楼,則會(huì)被阻塞万伤。
此外,如果不使用this作為鎖呜袁,而是只是想讓一段代碼同步敌买,可以臨時(shí)創(chuàng)建如下鎖:
private byte[] lock=new byte[0];
從操作碼上講,創(chuàng)建一個(gè)長(zhǎng)度為0的數(shù)組對(duì)象是最經(jīng)濟(jì)的阶界,只需要3條操作碼虹钮。
2.4.3. synchronized靜態(tài)方法
synchronized修飾靜態(tài)方法時(shí)或者在普通方法中以類為對(duì)象如下形式:
class StaticSynchronized{
public void aMethod{
synchronized (StaticSynchronized.class){
//Some thing
}
}
}
為synchronized靜態(tài)方法聋庵。
注意的是,對(duì)于同一個(gè)類芙粱,其static和實(shí)例方法如果都用synchronized修飾祭玉,其作用的必然不是同一個(gè)對(duì)象。
2.4.4. synchronized對(duì)象
比較簡(jiǎn)單粗暴的實(shí)現(xiàn)形式春畔,直接把對(duì)象鎖定脱货,思路也很清晰。java負(fù)責(zé)跟蹤被加鎖的對(duì)象律姨,該鎖定對(duì)象的線程每次給對(duì)象加鎖時(shí)對(duì)象的計(jì)數(shù)器+1蹭劈,每次解鎖時(shí)計(jì)數(shù)器-1,如果對(duì)象的計(jì)數(shù)器為0线召,那么解除該線程的鎖定铺韧。
2.5 synchronized的缺陷
synchronized修飾的代碼只有獲取到鎖的線程才能夠執(zhí)行,其他線程只能等待該線程釋放鎖缓淹。一個(gè)線程釋放鎖的情況有以下方法:
- 獲取鎖的線程完成了synchronized修飾的代碼塊的執(zhí)行哈打;
- 線程執(zhí)行時(shí)發(fā)生異常,JVM自動(dòng)釋放鎖讯壶。
鎖會(huì)因?yàn)榈却齀/O料仗,sleep()方法等原因被阻塞而不釋放鎖,此時(shí)如果線程還處于用synchronized修飾的代碼塊區(qū)域里伏蚊,那么其他線程只能等待立轧,這樣就影響了效率。因此java提供了Lock來實(shí)現(xiàn)另一個(gè)機(jī)制躏吊,即不讓線程無限期的等待下去氛改。
思考一個(gè)場(chǎng)景,當(dāng)多個(gè)線程讀寫文件時(shí)比伏,讀操作和寫操作會(huì)發(fā)生沖突胜卤,寫操作和寫操作會(huì)發(fā)生沖突,讀和讀操作不會(huì)發(fā)生沖突赁项。如果使用synchronized來修飾讀和寫的話葛躏,就很可能操作多個(gè)讀操作無法同時(shí)進(jìn)行的可能。如果只有synchronized修飾寫的話悠菜,又有可能造成讀寫沖突舰攒。此時(shí)就需要用到Lock。
3悔醋、Lock
Lock不是語(yǔ)言內(nèi)置的摩窃,synchronized是java關(guān)鍵字,為內(nèi)置特性篙顺。Lock是一個(gè)類偶芍,通過這個(gè)類可以實(shí)現(xiàn)同步訪問充择。
使用synchronized不需要我們手動(dòng)的去控制加鎖和縮放,系統(tǒng)會(huì)自動(dòng)控制匪蟀。而使用Lock類需要手動(dòng)的加鎖和釋放椎麦。不主動(dòng)釋放可能會(huì)造成死鎖。
3.1 Lock類接口設(shè)計(jì)
public interface Lock {
// 獲取鎖材彪。
// 如果鎖不可用观挎,則當(dāng)前線程將被禁用以用于線程調(diào)度目的并處于休眠狀態(tài),直到獲得鎖為止段化。
void lock();
// 與lock用法一樣嘁捷。
// 與lock的區(qū)別是:
// lockInterruptibly鎖定的線程處于等待狀態(tài)時(shí),允許其他線程的打斷显熏。比如說獲取到鎖的線程A可以調(diào)用線程B.interrupt()雄嚣,來中斷等待中的線程B,并拋出一個(gè)interruptException;
// lock鎖定的線程如果在等待時(shí)檢測(cè)到線程使用interrupt喘蟆,則會(huì)繼續(xù)嘗試獲取鎖缓升,失敗則繼續(xù)失眠,只是在成功獲取到鎖之后再把當(dāng)前線程置為interrupt狀態(tài)蕴轨。
void lockInterruptibly() throws InterruptedException;
// 如果鎖可用港谊,則獲取鎖并立即返回值為true 。如果鎖不可用橙弱,則此方法將立即返回值false 歧寺。
boolean tryLock();
// 設(shè)定了一個(gè)等待時(shí)間,如果在這個(gè)時(shí)間內(nèi)獲取到了鎖棘脐,則返回true斜筐,否色返回false結(jié)束
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 釋放鎖。一般放在異常處理操作的finally字符控制的代碼塊中荆残。
void unlock();
// 返回綁定到此Lock實(shí)例的新Condition實(shí)例奴艾。
// 在等待條件之前,鎖必須由當(dāng)前線程持有内斯。調(diào)用Condition.await()將在等待之前自動(dòng)釋放鎖,并在等待返回之前重新獲取鎖像啼。
Condition newCondition();
}
3.2 ReentrantLock可重入鎖
ReentrantLock是一個(gè)類俘闯,實(shí)現(xiàn)了Lock接口。
3.2.1 可重入定義
若一個(gè)程序或子程序可以“在任意時(shí)刻被中斷然后操作系統(tǒng)調(diào)度執(zhí)行另外一段代碼忽冻,這段代碼又調(diào)用了該子程序不會(huì)出錯(cuò)”真朗,則稱之為可重入。即當(dāng)該子程序正在運(yùn)行時(shí)僧诚,執(zhí)行線程可以再次進(jìn)入并執(zhí)行它遮婶,仍然獲得符合設(shè)計(jì)時(shí)預(yù)期的結(jié)果蝗碎。與多線程并發(fā)執(zhí)行的線程安全不同,可重入強(qiáng)調(diào)對(duì)單個(gè)線程執(zhí)行時(shí)重新進(jìn)入同一個(gè)子線程仍然是安全的旗扑。
3.2.2 可重入的條件
- 不在函數(shù)內(nèi)使用靜態(tài)或全局?jǐn)?shù)據(jù)
- 不返回靜態(tài)或全局?jǐn)?shù)據(jù)蹦骑,所有數(shù)據(jù)都由函數(shù)的調(diào)用者提供
- 使用本地?cái)?shù)據(jù)(工作內(nèi)存),或者通過制作全局?jǐn)?shù)據(jù)的本地拷貝來保護(hù)全局?jǐn)?shù)據(jù)
- 不調(diào)用不可重入函數(shù)
3.2.3 可重入與線程安全
可重入一般都是線程安全的臀防。但線程安全的不一定都是可重入的眠菇。在不加鎖的前提下,如果一個(gè)函數(shù)用到了全局或者靜態(tài)變量袱衷,那么它不是線程安全的捎废,也不是可重入的。如果我們加以改進(jìn)致燥,對(duì)全局變量的訪問加鎖登疗,此時(shí)它是線程安全的但不是可重入的,因?yàn)橥ǔ5募渔i方式是針對(duì)不同線程的訪問嫌蚤,當(dāng)同一個(gè)線程多次訪問就會(huì)產(chǎn)生問題辐益。只有當(dāng)函數(shù)滿足可重入的四條件時(shí),才是可重入的搬葬。
3.2.4 synchronized是可重入鎖
如果一個(gè)獲取到鎖1的線程A荷腊,請(qǐng)求一個(gè)被線程B持有的鎖2時(shí),則線程A會(huì)進(jìn)入到阻塞狀態(tài)急凰。如果線程A還是請(qǐng)求鎖1女仰,鎖1是可重入鎖,請(qǐng)求就會(huì)成功抡锈。
synchronized擁有強(qiáng)制原子性的內(nèi)部鎖機(jī)制疾忍,是一個(gè)可重入鎖。因此在一個(gè)線程使用synchronized方法時(shí)床三,調(diào)用該對(duì)象的另一個(gè)synchronized方法一罩,即一個(gè)線程的得到一個(gè)對(duì)象鎖之后再次請(qǐng)求該對(duì)象鎖是永遠(yuǎn)可以拿到的。
3.2.5 synchronized可重入鎖的實(shí)現(xiàn)
前面提到過撇簿,每個(gè)鎖關(guān)聯(lián)了一個(gè)線程持有者和一個(gè)計(jì)數(shù)器聂渊。當(dāng)計(jì)數(shù)器為0時(shí),說明沒有被任何線程所持有四瘫,那么任何線程都可能獲得該鎖而調(diào)用相應(yīng)的方法汉嗽。當(dāng)一個(gè)線程請(qǐng)求鎖成功后,jvm就會(huì)記錄下持有鎖的線程找蜜,并將計(jì)數(shù)器+1饼暑。此時(shí)其他線程請(qǐng)求該鎖,則必須等待。而是已經(jīng)持有該鎖的線程再次請(qǐng)求該所弓叛,則可以再次拿到這個(gè)鎖彰居,同時(shí)計(jì)數(shù)器會(huì)繼續(xù)+1。當(dāng)線程每退出一個(gè)synchronized方法/代碼塊時(shí)撰筷,計(jì)時(shí)器就會(huì)-1陈惰,直至為0,釋放該鎖闭专。
3.2.6 ReentrantLock原理
ReentrantLock主要是利用CAS和AQS隊(duì)列來實(shí)現(xiàn)的奴潘。支持公平鎖和非公平鎖,兩者實(shí)現(xiàn)類似影钉。
CAS:
compare and swap画髓。比較并交換。CAS有3個(gè)操作數(shù):內(nèi)存值V平委,預(yù)期值A(chǔ)奈虾、要修改的新值B。當(dāng)且僅當(dāng)預(yù)期值A(chǔ)和內(nèi)存值V相同時(shí)廉赔,才將內(nèi)存值V修改為B肉微,否則什么都不做。無論是哪種情況蜡塌,它都會(huì)在CAS指令前返回該位置的值碉纳。該操作是一個(gè)原子操作,被廣泛的應(yīng)用在java的底層實(shí)現(xiàn)中馏艾。在java中劳曹,CAS主要是由sun.misc.Unsafe這個(gè)類通過JNI調(diào)用CPU底層指令實(shí)現(xiàn)。
AQS:
AbstractQueuedSynchronizer琅摩。是一個(gè)用于構(gòu)建鎖和同步容器的框架铁孵。事實(shí)上java.util.concurrent包內(nèi)許多類都是基于AQS構(gòu)建,例如ReentrantLock房资、CountDownLatch蜕劝、FurureTask等。AQS解決了在實(shí)現(xiàn)同步容器時(shí)設(shè)計(jì)的大量細(xì)節(jié)問題轰异。
AQS是一個(gè)FIFO隊(duì)列岖沛,代表排隊(duì)等待鎖的線程。隊(duì)列頭節(jié)點(diǎn)稱作“哨兵節(jié)點(diǎn)”或者“啞節(jié)點(diǎn)”搭独,它不與任何線程關(guān)聯(lián)烫止。其他節(jié)點(diǎn)與等待線程關(guān)聯(lián),每個(gè)節(jié)點(diǎn)維護(hù)一個(gè)等待狀態(tài)waitStatus戳稽。
ReentrantLock的基本實(shí)現(xiàn)可以概括為:先通過CAS嘗試獲取鎖。如果此時(shí)已經(jīng)有線程占據(jù)了鎖,那就加入AQS隊(duì)列并且被掛起惊奇。當(dāng)鎖被釋放之后互躬,排在CLH隊(duì)列隊(duì)首的線程會(huì)被喚醒,然后CAS再次嘗試獲取鎖颂郎。在這個(gè)時(shí)候吼渡,如果同時(shí)還有另一個(gè)線程來嘗試獲取鎖,當(dāng)這個(gè)鎖是非公平鎖時(shí)乓序,則有可能被這個(gè)線程搶先獲取到寺酪。但如果鎖是公平鎖,它就會(huì)發(fā)現(xiàn)自己不是在隊(duì)首的話替劈,就會(huì)排到隊(duì)尾寄雀,由隊(duì)首的線程去獲取鎖。
ReentrantLock提供了兩個(gè)構(gòu)造器陨献,默認(rèn)是非公平鎖盒犹。由lock()和unlock的源碼可以看到,它 們只是分別調(diào)用了sync對(duì)象的lock()和release(1)方法眨业。
NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先用一個(gè)CAS操作急膀,判斷state是否為0(表示當(dāng)前鎖未被占用),如果是0龄捡,則把它置為1卓嫂,并且設(shè)置當(dāng)前線程為該鎖的獨(dú)占線程,表示獲取鎖成功聘殖。當(dāng)多個(gè)線程同時(shí)嘗試占用同一個(gè)鎖時(shí)晨雳,CAS操作只能保證一個(gè)線程操作成功,剩下的就去乖乖排隊(duì)了就斤。
"非公平"即提現(xiàn)在這里悍募,如果占用鎖的線程剛釋放鎖,state置為0洋机,而排隊(duì)等待的線程還未被喚醒诞外,新來的線程直接搶了該鎖桩引,那么就相當(dāng)于插隊(duì)了。
若當(dāng)前有三個(gè)線程去競(jìng)爭(zhēng)鎖,假設(shè)線程A的CAS操作成功了拾因,拿到了鎖開開心心的返回了,那么線程B和C則設(shè)置state失敗若专,走到else中脖含。
NonfairSync.lock()
--> sync.lock();
// 調(diào)用的是NonfairSync.lock()
--> final void lock() { @NonfairSync
// CAS操作,判斷state是否為0(表示當(dāng)前鎖未被占用)角骤,如果是0隅忿,則把它置為1心剥,并且設(shè)置當(dāng)前線程為該鎖的獨(dú)占線程,表示獲取鎖成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 獲取鎖失敗
acquire(1);
--> public final void acquire(int arg) { @AbstractQueuedSynchronizer
// 再次嘗試去獲取鎖背桐。細(xì)節(jié)看下面①优烧。。
if (!tryAcquire(arg)
// 如果上面嘗試獲取鎖失敗链峭,addWaiter 是入隊(duì)操作畦娄。流程看下面②。弊仪。熙卡。。励饵。驳癌。。曲横。
// acquireQueued 是掛起操作喂柒。流程看下面③。禾嫉。灾杰。。熙参。艳吠。。孽椰。
&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
①:嘗試去獲取鎖昭娩。如果嘗試獲取成功,方法直接返回黍匾。
非公平鎖tryAcquire的流程是:檢查state狀態(tài)栏渺,若為0,表示鎖未被占用锐涯,那么嘗試占用磕诊。若不為0,檢查當(dāng)前鎖是否被自己占用纹腌,若被自己占用霎终,則更新state字段,表示重入鎖的次數(shù)升薯。如果都沒有成功莱褒,則獲取鎖失敗,返回false涎劈。
protected final boolean tryAcquire(int acquires) { @NonfairSync
// 嘗試非公平鎖广凸,調(diào)用的是 Sync.nonfairTryAcquire()
return nonfairTryAcquire(acquires);
--> final boolean nonfairTryAcquire(int acquires) { @Sync
// 獲取當(dāng)前線程
final Thread current = Thread.currentThread();
// 獲取state值
int c = getState();
// state == 0 ,表示沒有線程占用鎖
if (c == 0) {
if (compareAndSetState(0, acquires)) {
// 占用鎖成功阅茶,設(shè)置獨(dú)占鎖線程為當(dāng)前線程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果當(dāng)前線程已經(jīng)占用該鎖了。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state值為新的重入次數(shù)
setState(nextc);
return true;
}
return false;
}
}
②:嘗試獲取鎖失敗炮障,那就要將當(dāng)前線程入隊(duì)目派。
// 將新節(jié)點(diǎn)和當(dāng)前線程關(guān)聯(lián),并入隊(duì)
private Node addWaiter(Node mode) {
// 初始化節(jié)點(diǎn)胁赢,設(shè)置關(guān)聯(lián)線程和模式(獨(dú)占 or 共享)
Node node = new Node(mode);
// for循環(huán),直到入隊(duì)成功返回
for (;;) {
// 獲取尾節(jié)點(diǎn)引用
Node oldTail = tail;
// 尾節(jié)點(diǎn)不為空白筹,說明隊(duì)列已經(jīng)初始化過了
if (oldTail != null) {
U.putObject(node, Node.PREV, oldTail);
// 設(shè)置新節(jié)點(diǎn)為尾節(jié)點(diǎn)
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
// 尾節(jié)點(diǎn)為空智末,說明隊(duì)列還未初始化,需要初始化head節(jié)點(diǎn)并入隊(duì)新節(jié)點(diǎn)
initializeSyncQueue();
// 在第一次爭(zhēng)用時(shí)初始化頭部和尾部字段
--> private final void initializeSyncQueue() {
Node h;
if (U.compareAndSwapObject(this, HEAD, null, (h = new Node())))
tail = h;
}
}
}
}
這里體現(xiàn)了經(jīng)典的自旋+CAS組合來實(shí)現(xiàn)非阻塞的原子操作徒河。由于initializeSyncQueue的實(shí)現(xiàn)使用CAS操作系馆,所以只有一個(gè)線程會(huì)創(chuàng)建head節(jié)點(diǎn)成功。
③:入隊(duì)后進(jìn)行掛起顽照。這個(gè)方法讓已經(jīng)入隊(duì)的線程嘗試獲取鎖由蘑,若失敗則會(huì)被掛起
// 已經(jīng)入隊(duì)的線程嘗試獲取鎖
final boolean acquireQueued(final Node node, int arg) {
try {
// 標(biāo)記線程是否被中斷過
boolean interrupted = false;
for (;;) {
// 獲取當(dāng)前節(jié)點(diǎn)的前面一個(gè)節(jié)點(diǎn)
final Node p = node.predecessor();
// 如果前面一個(gè)節(jié)點(diǎn)是是head,則該節(jié)點(diǎn)排第二代兵。便有資格去嘗試獲取鎖
if (p == head && tryAcquire(arg)) {
// 獲取成功尼酿,將當(dāng)前節(jié)點(diǎn)設(shè)置為head節(jié)點(diǎn)
setHead(node);
// 原h(huán)ead節(jié)點(diǎn)出隊(duì),在某個(gè)時(shí)間點(diǎn)被gc回收
p.next = null; // help GC
// 返回是否被中斷過
return interrupted;
}
// 判斷獲取失敗后是否可以掛起植影,若可以則掛起裳擎。接著看下面的④。思币。鹿响。。谷饿。惶我。。博投。绸贡。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 線程若被中斷,設(shè)置interrupted為true
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
④:判斷當(dāng)前線程獲取鎖失敗之后是否需要掛起
// 檢查并更新未能獲取的節(jié)點(diǎn)的狀態(tài)贬堵。如果線程應(yīng)該阻塞恃轩,則返回 true。這是所有采集循環(huán)中的主要信號(hào)控制黎做。要求 pred == node.prev叉跛。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 前面一個(gè)節(jié)點(diǎn)的狀態(tài)
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前面一個(gè)節(jié)點(diǎn)的狀態(tài)為signal,返回true
return true;
// 前面一個(gè)節(jié)點(diǎn)的狀態(tài)為cancelled蒸殿,返回true
if (ws > 0) {
// 從隊(duì)尾向前尋找第一個(gè)狀態(tài)不為cancelled的節(jié)點(diǎn)
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 將前驅(qū)節(jié)點(diǎn)的狀態(tài)設(shè)置為signal
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
// 掛起當(dāng)前線程,返回線程中斷狀態(tài)并重置
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
線程入隊(duì)后能過掛起的前提是筷厘,它的前驅(qū)節(jié)點(diǎn)的狀態(tài)是signal鸣峭,它的含義是,提醒前面的節(jié)點(diǎn)酥艳,如果前面的節(jié)點(diǎn)獲取到鎖并出隊(duì)后摊溶,把自己?jiǎn)拘选K詓houldParkAfterFailedAcquire會(huì)先判斷當(dāng)前節(jié)點(diǎn)的前驅(qū)是否狀態(tài)符合要求充石,如符合則返回true莫换,然后調(diào)用parkAndCheckInterrupt,將自己掛起骤铃。如果不符合拉岁,再看前驅(qū)節(jié)點(diǎn)是否大于0(canceled),若是那么向前遍歷直到找到第一個(gè)符合條件的前驅(qū)惰爬,若不是則將前驅(qū)節(jié)點(diǎn)的狀態(tài)設(shè)置為singal喊暖。
NonfairSync.unlock()
// 嘗試釋放此鎖。如果當(dāng)前線程是這個(gè)鎖的持有者撕瞧,那么持有計(jì)數(shù)就會(huì)遞減陵叽。如果保持計(jì)數(shù)現(xiàn)在為零,則釋放鎖丛版。
public void unlock() {
sync.release(1);
--> public final boolean release(int arg) { @AbstractQueuedSynchronizer
// 調(diào)用 Sync.tryRelease()
// 嘗試釋放鎖巩掺,如果釋放成功,那么查看頭節(jié)點(diǎn)的狀態(tài)是否是signal硼婿,如果是則喚醒頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)關(guān)聯(lián)的線程锌半,如果釋放失敗,那么返回false寇漫。表示解鎖失敗刊殉。
// 具體細(xì)節(jié),看下面
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
}
嘗試釋放當(dāng)前線程占用的鎖
protected final boolean tryRelease(int releases) {
// 計(jì)算釋放后state值
int c = getState() - releases;
// 如果不是當(dāng)前線程占用鎖州胳,那么拋出異常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 鎖被重入次數(shù)為0记焊,表示釋放成功
free = true;
// 清空獨(dú)占線程
setExclusiveOwnerThread(null);
}
// 更新state值
setState(c);
return free;
}
入?yún)⑹?。tryRelease的過程是:當(dāng)前釋放鎖的線程若不持有鎖栓撞,那就拋出異常遍膜。若持有鎖,計(jì)算釋放后的state是否為0瓤湘,若為0則表示鎖已經(jīng)被成功釋放瓢颅,并且清空獨(dú)占線程。最后更新state值弛说,返回free挽懦。
FairSync
公平鎖和非公平鎖的不同之處在于,公平鎖在獲取鎖的時(shí)候木人,不會(huì)先去檢查state的狀態(tài)信柿,而是直接執(zhí)行
final void lock() {
acquire(1);
}
超時(shí)機(jī)制
在ReentrantLock的tryLock(long timeout, TimeUnit unit)提供了超時(shí)獲取鎖的功能冀偶。如果在指定時(shí)間內(nèi)獲取到了鎖就返回true,如果沒有就返回false渔嚷。這種機(jī)制避免了線程無限期等待鎖的釋放进鸠。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
還是看一下具體實(shí)現(xiàn)
public boolean tryLock(long timeout, TimeUnit unit) @ReentrantLock
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
--> public final boolean tryAcquireNanos(int arg, long nanosTimeout) @AbstractQueuedSynchronizer
throws InterruptedException {
// 如果線程被中斷了,拋出異常
if (Thread.interrupted())
throw new InterruptedException();
// 先嘗試獲取鎖形病,獲取成功就直接返回客年。獲取失敗則進(jìn)入doAcquireNanos。下面看下doAcquireNanos()
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
}
在有限的時(shí)間內(nèi)去競(jìng)爭(zhēng)鎖
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果等待時(shí)間小于0窒朋,直接返回獲取鎖失敗
if (nanosTimeout <= 0L)
return false;
// 計(jì)算最后嘗試獲取鎖的時(shí)間點(diǎn)
final long deadline = System.nanoTime() + nanosTimeout;
// 線程入隊(duì)
final Node node = addWaiter(Node.EXCLUSIVE);
try {
// 又是自旋
for (;;) {
// 獲取前驅(qū)節(jié)點(diǎn)
final Node p = node.predecessor();
// 如果前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)搀罢,并且獲取鎖成功,則將當(dāng)前節(jié)點(diǎn)變成頭節(jié)點(diǎn)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return true;
}
// 計(jì)算嘗試獲取鎖的剩余時(shí)長(zhǎng)侥猩。
nanosTimeout = deadline - System.nanoTime();
// 如果已經(jīng)超時(shí),則取消獲取抵赢。返回false
if (nanosTimeout <= 0L) {
cancelAcquire(node);
return false;
}
// 超時(shí)時(shí)間未到欺劳,且需要掛起
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
// 阻塞當(dāng)前線程知道超時(shí)時(shí)間到期
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
doAcquireNanos的流程簡(jiǎn)述為:線程先入隊(duì)等待,然后開始自旋铅鲤,嘗試獲取鎖划提,獲取成功就返回,失敗則在隊(duì)列中找一個(gè)安全點(diǎn)把自己掛起直到超時(shí)時(shí)間過期邢享。這里為什么還需要循環(huán)呢鹏往?因?yàn)楫?dāng)前線程節(jié)點(diǎn)的前驅(qū)狀態(tài)可能不是signal,那么在當(dāng)前這一輪循環(huán)中線程不會(huì)被掛起骇塘,然后更 新超時(shí)時(shí)間伊履,開始新一輪的嘗試。
3.3 ReadWriteLock讀寫鎖
3.3.1 ReadWriteLock接口
public interface ReadWriteLock {
// 返回讀鎖款违。
Lock readLock();
// 返回寫鎖
Lock writeLock();
}
提供了讀和寫兩個(gè)操作唐瀑,兩個(gè)鎖的存在,是為了將讀和寫分開來操作插爹。為了提高效率哄辣,多個(gè)線程可以同時(shí)進(jìn)行讀的操作,但寫鎖是獨(dú)占的赠尾。而讀和寫又不能同時(shí)進(jìn)行力穗。
3.3.2 ReentrantReadWriteLock可重入讀寫鎖
ReentrantReadWriteLock是ReadWriteLock接口的唯一實(shí)例。同時(shí)提供了很多操作方法气嫁。
讀寫鎖代碼示例:
// 讀
class StartReadRunnable implements Runnable {
@Override
public void run() {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":開始讀当窗。。杉编。超全。咆霜。" + DateUtils.INSTANCE.getCurrDataStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":結(jié)束讀。嘶朱。蛾坯。。疏遏。" + DateUtils.INSTANCE.getCurrDataStr());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
}
// 寫
static class StartWriteRunnable implements Runnable {
@Override
public void run() {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":開始寫脉课。。财异。倘零。。" + DateUtils.INSTANCE.getCurrDataStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":結(jié)束寫戳寸。呈驶。。疫鹊。袖瞻。" + DateUtils.INSTANCE.getCurrDataStr());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
class ReadAndWriteLockDemo {
static ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
static Lock readLock = mReadWriteLock.readLock();
static Lock writeLock = mReadWriteLock.writeLock();
public static void main(String[] args) {
// 創(chuàng)建8個(gè)線程,2個(gè)線程寫拆吆,6個(gè)線程讀聋迎。
ExecutorService executorService = Executors.newFixedThreadPool(8);
for (int i = 0; i < 4; i++) {
if (i % 2 == 0) {
executorService.execute(new StartReadRunnable());
} else {
executorService.execute(new StartWriteRunnable());
}
}
for (int i = 0; i < 4; i++) {
executorService.execute(new StartReadRunnable());
}
}
打印日志:
pool-1-thread-1:開始讀。枣耀。霉晕。。捞奕。22/04/08 14:15:30
pool-1-thread-1:結(jié)束讀牺堰。。缝彬。萌焰。。22/04/08 14:15:32
pool-1-thread-2:開始寫谷浅。扒俯。。一疯。撼玄。22/04/08 14:15:32 // 線程1讀完,線程2才能接著寫
pool-1-thread-2:結(jié)束寫墩邀。掌猛。。。荔茬。22/04/08 14:15:34
pool-1-thread-4:開始寫废膘。。慕蔚。丐黄。。22/04/08 14:15:34 // 線程2寫完孔飒,線程4才能接著寫
pool-1-thread-4:結(jié)束寫灌闺。。坏瞄。桂对。。22/04/08 14:15:36
pool-1-thread-3:開始讀鸠匀。蕉斜。。缀棍。蛛勉。22/04/08 14:15:36 // 線程4寫完,線程3才能接著寫
pool-1-thread-5:開始讀睦柴。。毡熏。坦敌。。22/04/08 14:15:36
pool-1-thread-6:開始讀痢法。狱窘。。财搁。蘸炸。22/04/08 14:15:36
pool-1-thread-8:開始讀。尖奔。搭儒。。提茁。22/04/08 14:15:36
pool-1-thread-7:開始讀淹禾。。茴扁。铃岔。。22/04/08 14:15:36
pool-1-thread-3:結(jié)束讀峭火。毁习。智嚷。。纺且。22/04/08 14:15:38
pool-1-thread-5:結(jié)束讀盏道。。隆檀。摇天。。22/04/08 14:15:38
pool-1-thread-8:結(jié)束讀恐仑。泉坐。。裳仆。腕让。22/04/08 14:15:38
pool-1-thread-6:結(jié)束讀。歧斟。纯丸。。静袖。22/04/08 14:15:38
pool-1-thread-7:結(jié)束讀觉鼻。。队橙。坠陈。。22/04/08 14:15:38 // 線程3捐康、4仇矾、5、6解总、7贮匕、8可以同時(shí)讀
3.4 公平鎖
公平鎖即當(dāng)多個(gè)線程等待同一個(gè)鎖的時(shí)候,當(dāng)鎖被釋放了花枫,并不是多個(gè)線程隨機(jī)獲取到鎖刻盐,而是被等待時(shí)間最長(zhǎng)的線程獲取到。synchronized是一個(gè)非公平鎖乌昔,無法保證獲取到鎖的先后順序隙疚。ReentrantLock和ReentrantReadWriteLock默認(rèn)也是非公平鎖,但可以在構(gòu)造方法中傳入一個(gè)boolean值磕道,來標(biāo)識(shí)是否是公平的供屉。
// 創(chuàng)建ReentrantLock的實(shí)例。這相當(dāng)于使用ReentrantLock(false)
public ReentrantLock() {
sync = new NonfairSync(); // 非公平鎖
}
// 使用給定的公平策略創(chuàng)建ReentrantLock的實(shí)例。
// 參數(shù):fair – 如果此鎖應(yīng)使用公平排序策略伶丐,則為true
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
參考上面的讀寫實(shí)例悼做,可以看到線程并不是按順序執(zhí)行的。如果我們?cè)赗eentrantReadWriteLock構(gòu)造方法中傳入true哗魂,則打印結(jié)果如下:
pool-1-thread-1:開始讀肛走。。录别。朽色。。22/04/08 14:28:16
pool-1-thread-1:結(jié)束讀组题。葫男。。崔列。梢褐。22/04/08 14:28:18
pool-1-thread-2:開始寫。赵讯。盈咳。。边翼。22/04/08 14:28:18
pool-1-thread-2:結(jié)束寫鱼响。。组底。热押。。22/04/08 14:28:20
pool-1-thread-3:開始讀斤寇。。拥褂。娘锁。。22/04/08 14:28:20
pool-1-thread-3:結(jié)束讀饺鹃。莫秆。。悔详。镊屎。22/04/08 14:28:22
pool-1-thread-4:開始寫。茄螃。缝驳。。。22/04/08 14:28:22
pool-1-thread-4:結(jié)束寫用狱。运怖。。夏伊。摇展。22/04/08 14:28:24
pool-1-thread-5:開始讀。溺忧。咏连。。鲁森。22/04/08 14:28:24
pool-1-thread-6:開始讀祟滴。。刀森。踱启。。22/04/08 14:28:24
pool-1-thread-7:開始讀研底。埠偿。。榜晦。冠蒋。22/04/08 14:28:24
pool-1-thread-8:開始讀。乾胶。抖剿。。识窿。22/04/08 14:28:24
pool-1-thread-6:結(jié)束讀斩郎。。喻频。缩宜。。22/04/08 14:28:26
pool-1-thread-7:結(jié)束讀甥温。锻煌。。姻蚓。宋梧。22/04/08 14:28:26
pool-1-thread-5:結(jié)束讀。狰挡。捂龄。释涛。。22/04/08 14:28:26
pool-1-thread-8:結(jié)束讀跺讯。枢贿。。刀脏。局荚。22/04/08 14:28:26
3.5 Lock和synchronized的選擇
- synchronized是內(nèi)置語(yǔ)言實(shí)現(xiàn)的關(guān)鍵字,Lock是為了實(shí)現(xiàn)更高級(jí)鎖功能而提供的接口愈污;
- Lock實(shí)現(xiàn)了tryLock等接口耀态,線程未獲取到鎖時(shí)不用一直等待;
- synchronized發(fā)生異常時(shí)自動(dòng)釋放占有的鎖暂雹,Lock需要在finally塊中手動(dòng)釋放鎖首装。
- Lock可以通過Lock.lockInterruptibly()來中斷鎖;
- 由于Lock提供了時(shí)間限制同步杭跪,可被打斷同步等機(jī)制仙逻,線程激烈競(jìng)爭(zhēng)時(shí)Lock的性能遠(yuǎn)優(yōu)于synchronized,即有大量線程時(shí)推薦使用Lock
- ReentrantReadWriteLock實(shí)現(xiàn)了封裝好的讀寫鎖用于大量讀少量寫的場(chǎng)景涧尿。解決了synchronized難以讀寫同步的問題系奉。