并發(fā)編程之Lock和Condition實(shí)現(xiàn)管程

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ì)呢?有三種方案箍铭。

    1. 能夠響應(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。這樣就破壞了不可搶占條件了踏拜。
    1. 支持超時(shí)低剔。如果線程在一段時(shí)間之內(nèi)沒有獲取到鎖,不是進(jìn)入阻塞狀態(tài)姻锁,而是返回一個(gè)錯(cuò)誤猜欺,那這個(gè)線程也有機(jī)會(huì)釋放曾經(jīng)持有的鎖。這樣也能破壞不可搶占條件钓试。
    1. 非阻塞地獲取鎖副瀑。如果嘗試獲取鎖失敗,并不進(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ī)則:

    1. 順序性規(guī)則:對(duì)于線程 T1陈醒,value+=1 Happens-Before 釋放鎖的操作 unlock()瞧甩;
    1. volatile 變量規(guī)則:由于 state = 1 會(huì)先讀取 state,所以線程 T1 的 unlock() 操作
      Happens-Before 線程 T2 的 lock() 操作爷辙;
    1. 傳遞性規(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í)踐葬馋,它們分別是:

    1. 永遠(yuǎn)只在更新對(duì)象的成員變量時(shí)加鎖
    1. 永遠(yuǎn)只在訪問可變的成員變量時(shí)加鎖
    1. 永遠(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)致死鎖泼菌。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末篷角,一起剝皮案震驚了整個(gè)濱河市俩滥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖棱貌,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件障贸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)篷牌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門夭苗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚕泽,“玉大人,你說我怎么就攤上這事。” “怎么了锡宋?”我有些...
    開封第一講書人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵癌刽,是天一觀的道長(zhǎng)显拜。 經(jīng)常有香客問我矮台,道長(zhǎng)确虱,這世上最難降的妖魔是什么校辩? 我笑而不...
    開封第一講書人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任把鉴,我火速辦了婚禮,結(jié)果婚禮上混埠,老公的妹妹穿的比我還像新娘扳炬。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開白布瞧壮。 她就那樣靜靜地躺著,像睡著了一般蛾娶。 火紅的嫁衣襯著肌膚如雪蛔琅。 梳的紋絲不亂的頭發(fā)上罗售,一...
    開封第一講書人閱讀 49,764評(píng)論 1 290
  • 那天所禀,我揣著相機(jī)與錄音,去河邊找鬼辙培。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的缴渊。 我是一名探鬼主播赏壹,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼疟暖!你這毒婦竟也來了卡儒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤俐巴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后硬爆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欣舵,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年缀磕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缘圈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片劣光。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糟把,靈堂內(nèi)的尸體忽然破棺而出绢涡,到底是詐尸還是另有隱情,我是刑警寧澤遣疯,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布雄可,位于F島的核電站,受9級(jí)特大地震影響缠犀,放射性物質(zhì)發(fā)生泄漏数苫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一辨液、第九天 我趴在偏房一處隱蔽的房頂上張望虐急。 院中可真熱鬧,春花似錦滔迈、人聲如沸止吁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敬惦。三九已至,卻和暖如春间涵,著一層夾襖步出監(jiān)牢的瞬間仁热,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來泰國(guó)打工勾哩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抗蠢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓思劳,卻偏偏與公主長(zhǎng)得像迅矛,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子潜叛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容