Java SDK 并發(fā)包通過 Lock 和 Condition 兩個(gè)接口來實(shí)現(xiàn)管程油额,其中 Lock 用于解決互斥
問題,Condition 用于解決同步問題刻帚。
在介紹 Lock 的使用之前潦嘶,有個(gè)問題需要思考一下:
Java 語言本身提供的 synchronized 也是管程的一種實(shí)現(xiàn)读慎,既然 Java 從語言層面已經(jīng)實(shí)現(xiàn)了管
程了莺债,那為什么還要在 SDK 里提供另外一種實(shí)現(xiàn)呢?
再造管程的理由
你也許曾經(jīng)聽到過很多這方面的傳說昆码,例如在 Java 的 1.5 版本中顷歌,synchronized 性能不如
SDK 里面的 Lock锰蓬,但 1.6 版本之后,synchronized 做了很多優(yōu)化眯漩,將性能追了上來芹扭,所以 1.6
之后的版本又有人推薦使用 synchronized 了麻顶。
在介紹死鎖問題的時(shí)候辅肾,提出了一個(gè)破壞不可搶占條件方案轮锥,但是這個(gè)方案 synchronized 沒有辦法解決。原因是 synchronized 申請(qǐng)資源的時(shí)候份汗,如果申請(qǐng)不到杯活,線程直接進(jìn)入阻塞狀態(tài)了熬词,而線程進(jìn)入阻塞狀態(tài),啥都干不了歪今,也釋放不了線程已經(jīng)占有的資源颜矿。
如果我們重新設(shè)計(jì)一把互斥鎖去解決這個(gè)問題骑疆,那該怎么設(shè)計(jì)呢?有三種方案箍铭。
- 能夠響應(yīng)中斷诈火。synchronized 的問題是,持有鎖 A 后刀崖,如果嘗試獲取鎖 B 失敗教沾,那么線程就進(jìn)入阻塞狀態(tài)译断,一旦發(fā)生死鎖,就沒有任何機(jī)會(huì)來喚醒阻塞的線程。但如果阻塞狀態(tài)的線程能夠響應(yīng)中斷信號(hào)巡语,也就是說當(dāng)我們給阻塞的線程發(fā)送中斷信號(hào)的時(shí)候男公,能夠喚醒它合陵,那它就有機(jī)會(huì)釋放曾經(jīng)持有的鎖 A。這樣就破壞了不可搶占條件了踏拜。
- 支持超時(shí)低剔。如果線程在一段時(shí)間之內(nèi)沒有獲取到鎖,不是進(jìn)入阻塞狀態(tài)姻锁,而是返回一個(gè)錯(cuò)誤猜欺,那這個(gè)線程也有機(jī)會(huì)釋放曾經(jīng)持有的鎖。這樣也能破壞不可搶占條件钓试。
- 非阻塞地獲取鎖副瀑。如果嘗試獲取鎖失敗,并不進(jìn)入阻塞狀態(tài)挽鞠,而是直接返回狈孔,那這個(gè)線程也有機(jī)會(huì)釋放曾經(jīng)持有的鎖。這樣也能破壞不可搶占條件嫁赏。這三種方案可以全面彌補(bǔ) synchronized 的問題油挥。
實(shí)現(xiàn)的就是 Lock 接口的三個(gè)方法款熬。詳情如下:
// 支持中斷的 API
void lockInterruptibly()
throws InterruptedException;
// 支持超時(shí)的 API
boolean tryLock(long time, TimeUnit unit)
throws InterruptedException;
// 支持非阻塞獲取鎖的 API
boolean tryLock();
如何保證可見性
Java SDK 里面 Lock 的使用贤牛,有一個(gè)經(jīng)典的范例殉簸,就是try{}finally{}沽讹,需要重點(diǎn)關(guān)注的是在 finally 里面釋放鎖。Java 里多線程的可見性是通過 Happens-Before 規(guī)則保證的椭微,而 synchronized 之所以能夠保證可見性蝇率,也是因?yàn)橛袑?duì)于“不可搶占”這個(gè)條件刽沾,占用部分資源的線程進(jìn)一步申請(qǐng)其他資源時(shí),如果申請(qǐng)不到锅尘,可以主動(dòng)釋放它占有的資源布蔗,這樣不可搶占這個(gè)條件就破壞掉了纵揍。
一條 synchronized 相關(guān)的規(guī)則:synchronized 的解鎖 Happens-Before 于后續(xù)對(duì)這個(gè)鎖的加鎖。那 Java SDK 里面 Lock 靠什么保證可見性呢泽谨?例如在下面的代碼中吧雹,線程 T1 對(duì) value 進(jìn)行了 +=1 操作,那后續(xù)的線程 T2 能夠看到 value 的正確結(jié)果嗎搓蚪?
class X {
private final Lock rtl =
丁鹉、 new ReentrantLock();
int value;
public void addOne() {
// 獲取鎖
rtl.lock();
try {
value+=1;
} finally {
// 保證鎖能釋放
rtl.unlock();
}
}
}
答案必須是肯定的。Java SDK 里面鎖的實(shí)現(xiàn)非常復(fù)雜杜耙,這里我就不展開細(xì)說了佑女,但是原理還是需要簡(jiǎn)單介紹一下:它是利用了 volatile 相關(guān)的 Happens-Before 規(guī)則谈竿。Java SDK 里面的ReentrantLock空凸,內(nèi)部持有一個(gè) volatile 的成員變量 state,獲取鎖的時(shí)候呀洲,會(huì)讀寫 state 的值道逗;解鎖的時(shí)候,也會(huì)讀寫 state 的值(簡(jiǎn)化后的代碼如下面所示)滓窍。也就是說吏夯,在執(zhí)行 value+=1之前,程序先讀寫了一次 volatile 變量 state裆赵,在執(zhí)行 value+=1 之后跺嗽,又讀寫了一次 volatile變量 state。根據(jù)相關(guān)的 Happens-Before 規(guī)則:
- 順序性規(guī)則:對(duì)于線程 T1陈醒,value+=1 Happens-Before 釋放鎖的操作 unlock()瞧甩;
- volatile 變量規(guī)則:由于 state = 1 會(huì)先讀取 state,所以線程 T1 的 unlock() 操作
Happens-Before 線程 T2 的 lock() 操作爷辙;
- volatile 變量規(guī)則:由于 state = 1 會(huì)先讀取 state,所以線程 T1 的 unlock() 操作
- 傳遞性規(guī)則:線程 T2 的 lock() 操作 Happens-Before 線程 T1 的 value+=1 膝晾。
class SampleLock {
volatile int state;
// 加鎖
lock() {
// 省略代碼無數(shù)
state = 1;
}
// 解鎖
unlock() {
// 省略代碼無數(shù)
state = 0;
}
}
什么是可重入鎖
如果觀察過BlockingQueue,會(huì)發(fā)現(xiàn)我們創(chuàng)建的鎖的具體類名是 ReentrantLock幻赚,這個(gè)翻譯過來叫可重入鎖落恼,這個(gè)概念前面我們一直沒有介紹過离熏。所謂可重入鎖,顧名思義钻蔑,指的是線程可以重復(fù)獲取同一把鎖咪笑。例如下面代碼中府喳,當(dāng)線程 T1 執(zhí)行到 ① 處時(shí),已經(jīng)獲取到了鎖 rtl 兜粘,當(dāng)在 ① 處調(diào)用get() 方法時(shí)孔轴,會(huì)在 ② 再次對(duì)鎖 rtl 執(zhí)行加鎖操作路鹰。此時(shí)收厨,如果鎖 rtl 是可重入的,那么線程 T1可以再次加鎖成功雁竞;如果鎖 rtl 是不可重入的碑诉,那么線程 T1 此時(shí)會(huì)被阻塞。
可重入函數(shù)怎么理解呢德挣?所謂可重入函數(shù)格嗅,指的是多個(gè)線程可以同時(shí)調(diào)用該函數(shù),每個(gè)線程都能得到正確結(jié)果吗浩;同時(shí)在一個(gè)線程內(nèi)支持線程切換没隘,無論被切換多少次右蒲,結(jié)果都是正確的瑰妄。多線程可以同時(shí)執(zhí)行映砖,還支持線程切換,這意味著什么呢竹宋?線程安全啊蜈七。所以飒硅,可重入函數(shù)是線程安全的。
class X {
private final Lock rtl =
作谚、 new ReentrantLock();
int value;
public int get() {
// 獲取鎖
rtl.lock(); ②
try {
return value;
} finally {
// 保證鎖能釋放
rtl.unlock();
}
}
public void addOne() {
// 獲取鎖
rtl.lock();
try {
value = 1 + get(); ①
} finally {
// 保證鎖能釋放
rtl.unlock();
}
}
}
公平鎖與非公平鎖
在使用 ReentrantLock 的時(shí)候三娩,這個(gè)類有兩個(gè)構(gòu)造函數(shù),一個(gè)是無參構(gòu)造函數(shù)妹懒,一個(gè)是傳入 fair 參數(shù)的構(gòu)造函數(shù)雀监。fair 參數(shù)代表的是鎖的公平策略,如果傳入 true 就表示需要構(gòu)造一個(gè)公平鎖彬伦,反之則表示要構(gòu)造一個(gè)非公平鎖滔悉。
// 無參構(gòu)造函數(shù):默認(rèn)非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
// 根據(jù)公平策略參數(shù)創(chuàng)建鎖
public ReentrantLock(boolean fair){
sync = fair ? new FairSync()
: new NonfairSync();
}
比如入口等待隊(duì)列伊诵,鎖都對(duì)應(yīng)著一個(gè)等待隊(duì)列,如果一個(gè)線程沒有獲得鎖回官,就會(huì)進(jìn)入等待隊(duì)列曹宴,當(dāng)有線程釋放鎖的時(shí)候,就需要從等待隊(duì)列中喚醒一個(gè)等待的線程歉提。如果是公平鎖版扩,喚醒的策略就是誰等待的時(shí)間長(zhǎng),就喚醒誰柿扣,很公平;如果是非公平鎖司草,則不提供這個(gè)公平保證,有可能等待時(shí)間短的線程反而先被喚醒吨岭。
用鎖的最佳實(shí)踐
最值得推薦的是并發(fā)大師Doug Lea《Java 并發(fā)編程:設(shè)計(jì)原則與模式》一書中魁巩,推薦的三個(gè)用鎖的最佳實(shí)踐葬馋,它們分別是:
- 永遠(yuǎn)只在更新對(duì)象的成員變量時(shí)加鎖
- 永遠(yuǎn)只在訪問可變的成員變量時(shí)加鎖
- 永遠(yuǎn)不在調(diào)用其他對(duì)象的方法時(shí)加鎖
對(duì)于第三條規(guī)則,因?yàn)檎{(diào)用其他對(duì)象的方法区匣,實(shí)在是太不安全了欺旧,也許“其他”方法里面有線程 sleep()的調(diào)用栅哀,也可能會(huì)有奇慢無比的 I/O 操作茵瀑,這些都會(huì)嚴(yán)重影響性能马昨。更可怕的是,“其他”類的方法可能也會(huì)加鎖,然后雙重加鎖就可能導(dǎo)致死鎖泼菌。