----------------------------------------業(yè)精于勤荒于嬉闸昨,形成思?xì)в陔S-------------------------------
在Java程序中有時(shí)候我們可能需要推遲一些高開銷的對(duì)象初始化操作僵缺,等到使用到這些對(duì)象時(shí)再去初始化皿淋。但要正確實(shí)現(xiàn)線程安全的延時(shí)初始化需要一些技巧型将,否則可能會(huì)出現(xiàn)問題。比如下面使用雙重校驗(yàn)鎖實(shí)現(xiàn)的演示加載的單例模式就是存在線程安全問題的:
/**
* 使用雙重校驗(yàn)鎖
*/
class Single4 {
private static Single4 single = null;
private Single4() {
}
public static Single4 getSingleton() {
if (single == null) {
synchronized (Single4.class) {
if (single == null) {
single = new Single4();//問題就出現(xiàn)在這里
}
}
}
return single;
}
}
問題的根源:single = new Single4();
其實(shí)創(chuàng)建一個(gè)對(duì)象可以分為3步完成的:
1. memory = allocate();//1.為對(duì)象分配內(nèi)存空間
2. ctorInstance(single);//2.初始化對(duì)象
3. single = memory;//3.設(shè)置single指向剛分配的內(nèi)存地址
上面?zhèn)未a中的2和3之間可能會(huì)被重排序润歉,重排序后執(zhí)行的順序是這樣的:
1. memory = allocate();//1.為對(duì)象分配內(nèi)存空間
2. single = memory;//3.設(shè)置single指向剛分配的內(nèi)存地址
//這時(shí)候?qū)ο筮€沒有初始化足陨!
3. ctorInstance(single);//2.初始化對(duì)象
時(shí)間 | 線程A | 線程B |
---|---|---|
t1 | A1:分配對(duì)象的內(nèi)存空間 | |
t2 | A3:設(shè)置single指向內(nèi)存空間 | |
t3 | B1:判斷single是否為空 | |
t4 | B2:由于single不為null,線程B將訪問single引用的對(duì)象 | |
t5 | A2:初始化對(duì)象 | |
t6 | A4:訪問single引用的對(duì)象 |
由上表也可以看出读虏,當(dāng)線程A中初始化single發(fā)生了重排序责静,并且執(zhí)行完t2后時(shí)間片用完,線程B獲得時(shí)間片盖桥,獲取single時(shí)候灾螃,判斷single不為null,接下來線程B將訪問single引用的對(duì)象揩徊,但此時(shí)對(duì)象還沒有進(jìn)行初始化腰鬼,所以就會(huì)引發(fā)異常!
聰明的你塑荒,一定想到了:如果不讓創(chuàng)建對(duì)象過程發(fā)生重排序熄赡,不就解決問題啦。是的其實(shí)只要對(duì)上面的代碼做一個(gè)小小的修改就能做到:
/**
* 使用雙重校驗(yàn)鎖
*/
class Single4 {
private volatile static Single4 single = null;
private Single4() {
}
public static Single4 getSingleton() {
if (single == null) {
synchronized (Single4.class) {
if (single == null) {
single = new Single4();
}
}
}
return single;
}
好像沒有什么改變啊齿税,有的只是你沒注意到:
private volatile static Single4 single = null;
對(duì)本谜,就是在聲明引用的時(shí)候在前面加上volatile
關(guān)鍵字,加上volatile
后偎窘,在多線程環(huán)境下創(chuàng)建對(duì)象過程中的重排序是被禁止的。它是怎么做到的呢溜在,那么下面就讓我們一起來看看volatile
是怎么做到的陌知。
1.volatile
volatile是輕量級(jí)的synchronized,它在多處理器開發(fā)保證了共享變量的“可見性”掖肋∑推希可見性的意思是當(dāng)一個(gè)線程修改了共享變量時(shí),另一個(gè)線程能讀到這個(gè)修改的值志笼。
1.1volatile的實(shí)現(xiàn)原理
Java語言規(guī)范第三版中對(duì)volatile的定義如下:Java編程語言允許線程訪問共享變量沿盅,為了確保共享變量能被準(zhǔn)確和一致地更新,線程應(yīng)該確保通過排他鎖單獨(dú)獲得這個(gè)變量纫溃。
意思就是說腰涧,如果一個(gè)變量被聲明為volatile,Java內(nèi)存模型會(huì)確保所有線程所有的線程看到這個(gè)變量的值是一致的紊浩。
volatile是如何保證可見性的呢窖铡?下面我們看看我們?cè)趯?duì)volatile變量進(jìn)行寫操作時(shí)疗锐,cpu會(huì)做什么?
single = new Singleton();//single是volatile變量
轉(zhuǎn)化成匯編代碼如下:
0x01a3deld: movb $0×0,0×1104800(%esi);0x01a3de24: lock add1 $0×0,(%esp);
其中Lock前綴的指令在多核處理器下會(huì)引發(fā)了兩件事:
- 將當(dāng)前處理器緩存行的數(shù)據(jù)寫到系統(tǒng)內(nèi)存;
- 這個(gè)寫回緩存的操作會(huì)使在其他CPU里緩存了該內(nèi)存地址的數(shù)據(jù)失效费彼。
1.2volatile的內(nèi)存語義
volatile寫-讀的內(nèi)存語義
- volatile的寫-讀與鎖的釋放-獲取有相同的內(nèi)存效果
- volatile寫和鎖的釋放有相同的內(nèi)存語義
- volatile讀與鎖的獲取有相同的內(nèi)存語義
其實(shí)理解volatile特性的一個(gè)好方法:
把對(duì)volatile變量的單個(gè)讀/寫滑臊,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫操作做了同步。
鎖的happens-before規(guī)則保證釋放鎖和獲取鎖的兩個(gè)線程之間的內(nèi)存可見性箍铲,這意味這對(duì)一個(gè)volatile變量的讀雇卷,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫入值。
volatile變量自身有兩個(gè)特性:
原子性:對(duì)于任意單個(gè)volatile變量的讀/寫具有原子性,但是類似與volatileVal++這種復(fù)合操作來說,它就不具有原子性颠猴。
可見性:對(duì)于一個(gè)volatile變量的讀关划,總是能看到任意線程對(duì)這個(gè)volatile變量最后的寫入。
當(dāng)寫一個(gè)volatile變量時(shí)芙粱,JMM會(huì)把線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存
當(dāng)讀一個(gè)volatile變量時(shí)祭玉,JMM會(huì)把線程對(duì)應(yīng)的本地內(nèi)存置為無效,線程接下來將從主內(nèi)存中讀取共享變量春畔。
注意:
一些編程大牛往往會(huì)告誡我們說脱货,盡量不要去使用volatile,因?yàn)槭褂胿olatile稍有不慎就會(huì)出現(xiàn)問題律姨。如果嚴(yán)格遵循 volatile 的使用條件:
- 變量真正獨(dú)立于其他變量
- 不依賴于自己以前的值
如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作振峻,這些操作整體上不具有原子性。在某些情況下可以使用volatile代替synchronized來簡(jiǎn)化代碼择份。然而扣孟,使用volatile的代碼往往比使用鎖的代碼更加容易出錯(cuò)。(見volatileDemo)
2.synchronized
在多線程并發(fā)編程中synchronized一直是元老級(jí)角色荣赶,也有很多人稱它為重量級(jí)鎖凤价。但是隨著Java 1.6對(duì)aynchronized進(jìn)行了各種優(yōu)化之后,有些情況下他就并不那么重了拔创。
synchronized的使用場(chǎng)景有一下三種:
- 對(duì)普通方法的同步利诺,鎖是當(dāng)前實(shí)例的對(duì)象
- 對(duì)靜態(tài)方法的同步,鎖是當(dāng)前類的Class對(duì)象
- 對(duì)代碼塊進(jìn)行同步剩燥,鎖是synchronized括號(hào)里配置的對(duì)象
JVM規(guī)范規(guī)定JVM基于進(jìn)入和退出Monitor對(duì)象來實(shí)現(xiàn)方法同步和代碼塊同步慢逾,但兩者的實(shí)現(xiàn)細(xì)節(jié)不一樣。代碼塊同步是使用monitorenter和monitorexit指令實(shí)現(xiàn)灭红,而方法同步是使用另外一種方式實(shí)現(xiàn)的侣滩,細(xì)節(jié)在JVM規(guī)范里并沒有詳細(xì)說明,但是方法的同步同樣可以使用這兩個(gè)指令來實(shí)現(xiàn)变擒。monitorenter指令是在編譯后插入到同步代碼塊的開始位置君珠,而monitorexit是插入到方法結(jié)束處和異常處, JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)娇斑。任何對(duì)象都有一個(gè) monitor 與之關(guān)聯(lián)葛躏,當(dāng)且一個(gè)monitor 被持有后澈段,它將處于鎖定狀態(tài)。線程執(zhí)行到 monitorenter 指令時(shí)舰攒,將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的 monitor 的所有權(quán)败富,即嘗試獲得對(duì)象的鎖。
2.1 Java對(duì)象頭
鎖存在Java對(duì)象頭里摩窃。如果對(duì)象是數(shù)組類型兽叮,則虛擬機(jī)用3個(gè)Word(字寬)存儲(chǔ)對(duì)象頭,如果對(duì)象是非數(shù)組類型猾愿,則用2字寬存儲(chǔ)對(duì)象頭鹦聪。在32位虛擬機(jī)中,一字寬等于四字節(jié)蒂秘,即32bit泽本。
Java對(duì)象頭里的Mark Word里默認(rèn)存儲(chǔ)對(duì)象的HashCode,分代年齡和鎖標(biāo)記位姻僧。32位JVM的Mark Word的默認(rèn)存儲(chǔ)結(jié)構(gòu)如下:
在運(yùn)行期間Mark Word里存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化撇贺。Mark Word可能變化為存儲(chǔ)以下4種數(shù)據(jù):
2.2 幾種鎖的類型
線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài)松嘶,頻繁的阻塞和喚醒對(duì)CPU來說是一件負(fù)擔(dān)很重的工作。
Java SE1.6為了減少獲得鎖和釋放鎖所帶來的性能消耗巢音,引入了“偏向鎖”和“輕量級(jí)鎖”尽超,所以在Java SE1.6里鎖一共有四種狀態(tài),無鎖狀態(tài),偏向鎖狀態(tài)燥狰,輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài)棘脐,它會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)。
鎖可以升級(jí)但不能降級(jí)龙致,意味著偏向鎖升級(jí)成輕量級(jí)鎖后不能降級(jí)成偏向鎖蛀缝。這種鎖升級(jí)卻不能降級(jí)的策略,目的是為了提高獲得鎖和釋放鎖的效率目代。
2.2.1 偏向鎖
Hotspot的作者經(jīng)過以往的研究發(fā)現(xiàn)大多數(shù)情況下鎖不僅不存在多線程競(jìng)爭(zhēng)屈梁,而且總是由同一線程多次獲得嗤练。偏向鎖的目的是在某個(gè)線程獲得鎖之后煞抬,消除這個(gè)線程鎖重入(CAS)的開銷革答,看起來讓這個(gè)線程得到了偏護(hù)曙强。
偏向鎖的進(jìn)一步理解
偏向鎖的釋放不需要做任何事情碟嘴,這也就意味著加過偏向鎖的MarkValue會(huì)一直保留偏向鎖的狀態(tài)娜扇,因此即便同一個(gè)線程持續(xù)不斷地加鎖解鎖,也是沒有開銷的捎废。
另一方面登疗,偏向鎖比輕量鎖更容易被終結(jié)辐益,輕量鎖是在有鎖競(jìng)爭(zhēng)出現(xiàn)時(shí)升級(jí)為重量鎖智政,而一般偏向鎖是在有不同線程申請(qǐng)鎖時(shí)升級(jí)為輕量鎖续捂,這也就意味著假如一個(gè)對(duì)象先被線程1加鎖解鎖宦搬,再被線程2加鎖解鎖间校,這過程中沒有鎖沖突憔足,也一樣會(huì)發(fā)生偏向鎖失效,不同的是這回要先退化為無鎖的狀態(tài)控妻,再加輕量鎖饼暑,如圖:
另外湿弦,JVM對(duì)那種會(huì)有多線程加鎖勇吊,但不存在鎖競(jìng)爭(zhēng)的情況也做了優(yōu)化疏咐,聽起來比較拗口毕籽,但在現(xiàn)實(shí)應(yīng)用中確實(shí)是可能出現(xiàn)這種情況关筒,因?yàn)榫€程之前除了互斥之外也可能發(fā)生同步關(guān)系蒸播,被同步的兩個(gè)線程(一前一后)對(duì)共享對(duì)象鎖的競(jìng)爭(zhēng)很可能是沒有沖突的。
偏向鎖的獲取
當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí)胀屿,會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)鎖偏向的線程ID宿崭,以后該線程在進(jìn)入和退出同步塊時(shí)不需要花費(fèi)CAS操作來加鎖和解鎖葡兑,而只需簡(jiǎn)單的測(cè)試一下對(duì)象頭的Mark Word里是否存儲(chǔ)著指向當(dāng)前線程的偏向鎖讹堤,如果測(cè)試成功房资,表示線程已經(jīng)獲得了鎖轰异,如果測(cè)試失敗,則需要再測(cè)試下Mark Word中偏向鎖的標(biāo)識(shí)是否設(shè)置成1(表示當(dāng)前是偏向鎖)婴削,如果沒有設(shè)置唉俗,則使用CAS競(jìng)爭(zhēng)鎖虫溜,如果設(shè)置了衡楞,則嘗試使用CAS將對(duì)象頭的偏向鎖指向當(dāng)前線程瘾境。
偏向鎖的撤銷
偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放鎖的機(jī)制迷守,所以當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí)兑凿,持有偏向鎖的線程才會(huì)釋放鎖急膀。偏向鎖的撤銷卓嫂,需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有字節(jié)碼正在執(zhí)行)晨雳,它會(huì)首先暫停擁有偏向鎖的線程餐禁,然后檢查持有偏向鎖的線程是否活著帮非,如果線程不處于活動(dòng)狀態(tài)末盔,則將對(duì)象頭設(shè)置成無鎖狀態(tài)陨舱,如果線程仍然活著游盲,擁有偏向鎖的棧會(huì)被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄谜慌,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向于其他線程畦娄,要么恢復(fù)到無鎖或者標(biāo)記對(duì)象不適合作為偏向鎖熙卡,最后喚醒暫停的線程驳癌。下圖中的線程1演示了偏向鎖初始化的流程颓鲜,線程2演示了偏向鎖撤銷的流程甜滨。
偏向鎖的設(shè)置
關(guān)閉偏向鎖:偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活瘤袖,如有必要可以使用JVM參數(shù)來關(guān)閉延遲-XX:BiasedLockingStartupDelay = 0衣摩。如果你確定自己應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖-XX:-UseBiasedLocking=false捂敌,那么默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)艾扮。
2.2.2 輕量級(jí)鎖
輕量級(jí)鎖加鎖
線程在執(zhí)行同步塊之前,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間占婉,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word磺箕。然后線程嘗試使用CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針滞磺。如果成功广凸,當(dāng)前線程獲得鎖,如果失敗扭吁,則自旋獲取鎖,當(dāng)自旋獲取鎖仍然失敗時(shí)枫吧,表示存在其他線程競(jìng)爭(zhēng)鎖(兩條或兩條以上的線程競(jìng)爭(zhēng)同一個(gè)鎖),則輕量級(jí)鎖會(huì)膨脹成重量級(jí)鎖。
輕量級(jí)鎖解鎖
輕量級(jí)解鎖時(shí)镀层,會(huì)使用原子的CAS操作來將Displaced Mark Word替換回到對(duì)象頭,如果成功惶我,則表示同步過程已完成。如果失敗捧挺,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時(shí)喚醒被掛起的線程黑竞。下圖是兩個(gè)線程同時(shí)爭(zhēng)奪鎖,導(dǎo)致鎖膨脹的流程圖。
2.2.3 重量級(jí)鎖
重量鎖在JVM中又叫對(duì)象監(jiān)視器(Monitor)幅聘,它很像C中的Mutex,除了具備Mutex互斥的功能陵叽,它還負(fù)責(zé)實(shí)現(xiàn)了Semaphore的功能,也就是說它至少包含一個(gè)競(jìng)爭(zhēng)鎖的隊(duì)列,和一個(gè)信號(hào)阻塞隊(duì)列(wait隊(duì)列)独令,前者負(fù)責(zé)做互斥舍败,后一個(gè)用于做線程同步裙戏。
2.2.4 鎖的優(yōu)缺點(diǎn)對(duì)比
2.3 鎖的內(nèi)存語義
鎖是java并發(fā)編程中最重要的同步機(jī)制壹罚。鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個(gè)鎖的線程發(fā)送消息。下面是鎖釋放-獲取的示例代碼:
class MonitorExample {
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
……
} //6
}
假設(shè)線程A執(zhí)行writer()方法漠吻,隨后線程B執(zhí)行reader()方法。根據(jù)happens before規(guī)則,這個(gè)過程包含的happens before 關(guān)系可以分為兩類:
根據(jù)程序次序規(guī)則试读,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6。
根據(jù)監(jiān)視器鎖規(guī)則倘屹,3 happens before 4拍谐。
根據(jù)happens before 的傳遞性力穗,2 happens before 5当窗。
上述happens before 關(guān)系的圖形化表現(xiàn)形式如下:
在上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn)庶香,代表了一個(gè)happens before 關(guān)系。黑色箭頭表示程序順序規(guī)則七扰;橙色箭頭表示監(jiān)視器鎖規(guī)則奢赂;藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證。
上圖表示在線程A釋放了鎖之后颈走,隨后線程B獲取同一個(gè)鎖膳灶。在上圖中,2 happens before 5立由。因此轧钓,線程A在釋放鎖之前所有可見的共享變量,在線程B獲取同一個(gè)鎖之后锐膜,將立刻變得對(duì)B線程可見聋迎。
鎖釋放和獲取的內(nèi)存語義
當(dāng)線程釋放鎖時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。以上面的MonitorExample程序?yàn)槔珹線程釋放鎖后辉浦,共享數(shù)據(jù)的狀態(tài)示意圖如下:
當(dāng)線程獲取鎖時(shí)甩卓,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效缀棍。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量。下面是鎖獲取的狀態(tài)示意圖:
對(duì)比鎖釋放-獲取的內(nèi)存語義與volatile寫-讀的內(nèi)存語義机错,可以看出:鎖釋放與volatile寫有相同的內(nèi)存語義爬范;鎖獲取與volatile讀有相同的內(nèi)存語義。
下面對(duì)鎖釋放和鎖獲取的內(nèi)存語義做個(gè)總結(jié):
線程A釋放一個(gè)鎖弱匪,實(shí)質(zhì)上是線程A向接下來將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(線程A對(duì)共享變量所做修改的)消息青瀑。
線程B獲取一個(gè)鎖,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做修改的)消息萧诫。
線程A釋放鎖斥难,隨后線程B獲取這個(gè)鎖,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息帘饶。
3. java.util.concurrent.locks包下常用的類
下面我們就來探討一下java.util.concurrent.locks包中常用的類和接口哑诊。
3.1 Lock
首先要說明的就是Lock,通過查看Lock的源碼可知及刻,Lock是一個(gè)接口:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面來逐個(gè)講述Lock接口中每個(gè)方法的使用镀裤,lock()竞阐、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用來獲取鎖的暑劝。unLock()方法是用來釋放鎖的骆莹。
在Lock中聲明了四個(gè)方法來獲取鎖,那么這四個(gè)方法有何區(qū)別呢铃岔?
首先lock()方法是平常使用得最多的一個(gè)方法汪疮,就是用來獲取鎖。如果鎖已被其他線程獲取毁习,則進(jìn)行等待智嚷。
由于在前面講到如果采用Lock,必須主動(dòng)去釋放鎖纺且,并且在發(fā)生異常時(shí)盏道,不會(huì)自動(dòng)釋放鎖。因此一般來說载碌,使用Lock必須在try{}catch{}塊中進(jìn)行猜嘱,并且將釋放鎖的操作放在finally塊中進(jìn)行,以保證鎖一定被被釋放嫁艇,防止死鎖的發(fā)生朗伶。通常使用Lock來進(jìn)行同步的話,是以下面這種形式去使用的:
Lock lock = ...;
lock.lock();
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
tryLock()方法是有返回值的步咪,它表示用來嘗試獲取鎖论皆,如果獲取成功,則返回true猾漫,如果獲取失數闱纭(即鎖已被其他線程獲取)悯周,則返回false粒督,也就說這個(gè)方法無論如何都會(huì)立即返回。在拿不到鎖時(shí)不會(huì)一直在那等待禽翼。
tryLock(long time,TimeUnitunit)方法和tryLock()方法是類似的屠橄,只不過區(qū)別在于這個(gè)方法在拿不到鎖時(shí)會(huì)等待一定的時(shí)間,在時(shí)間期限之內(nèi)如果還拿不到鎖闰挡,就返回false锐墙。如果如果一開始拿到鎖或者在等待期間內(nèi)拿到了鎖,則返回true解总。
所以贮匕,一般情況下通過tryLock來獲取鎖時(shí)是這樣使用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//處理任務(wù)
}catch(Exception ex){
}finally{
lock.unlock(); //釋放鎖
}
}else {
//如果不能獲取鎖,則直接做其他事情
}
lockInterruptibly()方法比較特殊花枫,當(dāng)通過這個(gè)方法去獲取鎖時(shí)刻盐,如果線程正在等待獲取鎖掏膏,則這個(gè)線程能夠響應(yīng)中斷,即中斷線程的等待狀態(tài)敦锌。也就使說馒疹,當(dāng)兩個(gè)線程同時(shí)通過lock.lockInterruptibly()想獲取某個(gè)鎖時(shí),假若此時(shí)線程A獲取到了鎖乙墙,而線程B只有在等待颖变,那么對(duì)線程B調(diào)用threadB.interrupt()方法能夠中斷線程B的等待過程。
由于lockInterruptibly()的聲明中拋出了異常听想,所以lock.lockInterruptibly()必須放在try塊中或者在調(diào)用lockInterruptibly()的方法外聲明拋出InterruptedException腥刹。
因此lockInterruptibly()一般的使用形式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
注意,當(dāng)一個(gè)線程獲取了鎖之后汉买,是不會(huì)被interrupt()方法中斷的衔峰。因?yàn)楸旧碓谇懊娴奈恼轮兄v過單獨(dú)調(diào)用interrupt()方法不能中斷正在運(yùn)行過程中的線程,只能中斷阻塞過程中的線程蛙粘。
因此當(dāng)通過lockInterruptibly()方法獲取某個(gè)鎖時(shí)垫卤,如果不能獲取到,只有進(jìn)行等待的情況下出牧,是可以響應(yīng)中斷的穴肘。
而用synchronized修飾的話,當(dāng)一個(gè)線程處于等待某個(gè)鎖的狀態(tài)舔痕,是無法被中斷的评抚,只有一直等待下去。
3.2 ReentrantLock
ReentrantLock赵讯,意思是“可重入鎖”盈咳,關(guān)于可重入鎖的概念在下一節(jié)講述耿眉。ReentrantLock是唯一實(shí)現(xiàn)了Lock接口的類边翼,并且ReentrantLock提供了更多的方法。下面通過一些實(shí)例看具體看一下如何使用ReentrantLock鸣剪。
例子1组底,lock()的正確使用方法
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
Lock lock = new ReentrantLock(); //注意這個(gè)地方
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
小伙伴們先想一下這段代碼的輸出結(jié)果是什么?
Thread-0得到了鎖
Thread-1得到了鎖
Thread-0釋放了鎖
Thread-1釋放了鎖
也許有朋友會(huì)問筐骇,怎么會(huì)輸出這個(gè)結(jié)果债鸡?第二個(gè)線程怎么會(huì)在第一個(gè)線程釋放鎖之前得到了鎖?原因在于铛纬,在insert方法中的lock變量是局部變量厌均,每個(gè)線程執(zhí)行該方法時(shí)都會(huì)保存一個(gè)副本,那么理所當(dāng)然每個(gè)線程執(zhí)行到lock.lock()處獲取的是不同的鎖告唆,所以就不會(huì)發(fā)生沖突棺弊。
知道了原因改起來就比較容易了晶密,只需要將lock聲明為類的屬性即可。
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個(gè)地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
lock.lock();
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
}
}
這樣就是正確地使用Lock的方法了模她。
例子2稻艰,tryLock()的使用方法
public class Test {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Lock lock = new ReentrantLock(); //注意這個(gè)地方
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.insert(Thread.currentThread());
};
}.start();
}
public void insert(Thread thread) {
if(lock.tryLock()) {
try {
System.out.println(thread.getName()+"得到了鎖");
for(int i=0;i<5;i++) {
arrayList.add(i);
}
} catch (Exception e) {
// TODO: handle exception
}finally {
System.out.println(thread.getName()+"釋放了鎖");
lock.unlock();
}
} else {
System.out.println(thread.getName()+"獲取鎖失敗");
}
}
}
輸出結(jié)果:
Thread-0得到了鎖
Thread-1獲取鎖失敗
Thread-0釋放了鎖
例子3,lockInterruptibly()響應(yīng)中斷的使用方法:
public class Test {
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
MyThread thread1 = new MyThread(test);
MyThread thread2 = new MyThread(test);
thread1.start();
thread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
public void insert(Thread thread) throws InterruptedException{
lock.lockInterruptibly(); //注意侈净,如果需要正確中斷等待鎖的線程尊勿,必須將獲取鎖放在外面,然后將InterruptedException拋出
try {
System.out.println(thread.getName()+"得到了鎖");
long startTime = System.currentTimeMillis();
for( ; ;) {
if(System.currentTimeMillis() - startTime >= Integer.MAX_VALUE)
break;
//插入數(shù)據(jù)
}
}
finally {
System.out.println(Thread.currentThread().getName()+"執(zhí)行finally");
lock.unlock();
System.out.println(thread.getName()+"釋放了鎖");
}
}
}
class MyThread extends Thread {
private Test test = null;
public MyThread(Test test) {
this.test = test;
}
@Override
public void run() {
try {
test.insert(Thread.currentThread());
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName()+"被中斷");
}
}
}
運(yùn)行之后畜侦,發(fā)現(xiàn)thread2能夠被正確中斷元扔。
3.3 ReadWriteLock
ReadWriteLock也是一個(gè)接口,在它里面只定義了兩個(gè)方法:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
一個(gè)用來獲取讀鎖旋膳,一個(gè)用來獲取寫鎖摇展。也就是說將文件的讀寫操作分開,分成2個(gè)鎖來分配給線程溺忧,從而使得多個(gè)線程可以同時(shí)進(jìn)行讀操作咏连。下面的ReentrantReadWriteLock實(shí)現(xiàn)了ReadWriteLock接口。
3.4 ReentrantReadWriteLock
ReentrantReadWriteLock里面提供了很多豐富的方法鲁森,不過最主要的有兩個(gè)方法:readLock()和writeLock()用來獲取讀鎖和寫鎖祟滴。
下面通過幾個(gè)例子來看一下ReentrantReadWriteLock具體用法。
假如有多個(gè)線程要同時(shí)進(jìn)行讀操作的話歌溉,先看一下synchronized達(dá)到的效果:
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public synchronized void get(Thread thread) {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進(jìn)行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
}
}
這段程序的輸出結(jié)果會(huì)是垄懂,直到thread1執(zhí)行完讀操作之后,才會(huì)打印thread2執(zhí)行讀操作的信息痛垛。
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0讀操作完畢
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1讀操作完畢
而改成用讀寫鎖的話:
public class Test {
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
final Test test = new Test();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
new Thread(){
public void run() {
test.get(Thread.currentThread());
};
}.start();
}
public void get(Thread thread) {
rwl.readLock().lock();
try {
long start = System.currentTimeMillis();
while(System.currentTimeMillis() - start <= 1) {
System.out.println(thread.getName()+"正在進(jìn)行讀操作");
}
System.out.println(thread.getName()+"讀操作完畢");
} finally {
rwl.readLock().unlock();
}
}
}
此時(shí)打印的結(jié)果為:
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-1正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0正在進(jìn)行讀操作
Thread-0讀操作完畢
Thread-1讀操作完畢
說明thread1和thread2在同時(shí)進(jìn)行讀操作草慧。
這樣就大大提升了讀操作的效率
不過要注意的是,如果有一個(gè)線程已經(jīng)占用了讀鎖匙头,則此時(shí)其他線程如果要申請(qǐng)寫鎖漫谷,則申請(qǐng)寫鎖的線程會(huì)一直等待釋放讀鎖。
如果有一個(gè)線程已經(jīng)占用了寫鎖蹂析,則此時(shí)其他線程如果申請(qǐng)寫鎖或者讀鎖舔示,則申請(qǐng)的線程會(huì)一直等待釋放寫鎖。
關(guān)于ReentrantReadWriteLock類中的其他方法感興趣的朋友可以自行查閱API文檔电抚。
3.5 Lock和synchronized的選擇
總結(jié)來說惕稻,Lock和synchronized有以下幾點(diǎn)不同:
1)Lock是一個(gè)接口,而synchronized是Java中的關(guān)鍵字蝙叛,synchronized是內(nèi)置的語言實(shí)現(xiàn)俺祠;
2)synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生蜘渣;而Lock在發(fā)生異常時(shí)妓布,如果沒有主動(dòng)通過unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象宋梧,因此使用Lock時(shí)需要在finally塊中釋放鎖匣沼;
3)Lock可以讓等待鎖的線程響應(yīng)中斷,而synchronized卻不行捂龄,使用synchronized時(shí)释涛,等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷倦沧;
4)通過Lock可以知道有沒有成功獲取鎖唇撬,而synchronized卻無法辦到。
5)Lock可以提高多個(gè)線程進(jìn)行讀操作的效率展融。
在性能上來說窖认,如果競(jìng)爭(zhēng)資源不激烈,兩者的性能是差不多的告希,而當(dāng)競(jìng)爭(zhēng)資源非常激烈時(shí)(即有大量線程同時(shí)競(jìng)爭(zhēng))扑浸,此時(shí)Lock的性能要遠(yuǎn)遠(yuǎn)優(yōu)于synchronized。所以說燕偶,在具體使用時(shí)要根據(jù)適當(dāng)情況選擇喝噪。
參考文獻(xiàn)
《Java并發(fā)編程藝術(shù)》
http://ifeve.com/java-memory-model-5/
http://blog.csdn.net/true100/article/details/51693715 luojinping.com/2015/07/09/java%E9%94%81%E4%BC%98%E5%8C%96/
http://blog.jobbole.com/104902/