Java synchronized 和 ReentrantLock對(duì)比
前段時(shí)間學(xué)習(xí)了java的線程同步的一些知識(shí)腥寇,認(rèn)識(shí)到了線程同步的關(guān)鍵字synchronized 和 線程鎖java.util.concurrent.lock包下的 ReentrantLock ,下面對(duì)兩種方式做分析對(duì)比
多線程和并發(fā)性并不是什么新內(nèi)容,但是 Java 語(yǔ)言設(shè)計(jì)中的創(chuàng)新之一就是俏拱,它是第一個(gè)直接把跨平臺(tái)線程模型和正規(guī)的內(nèi)存模型集成到語(yǔ)言中的主流語(yǔ)言捧搞。核心類庫(kù)包含一個(gè) Thread 類腹尖,可以用它來構(gòu)建、啟動(dòng)和操縱線程憾朴,Java 語(yǔ)言包括了跨線程傳達(dá)并發(fā)性約束的構(gòu)造 —— synchronized 和 volatile 狸捕。在簡(jiǎn)化與平臺(tái)無關(guān)的并發(fā)類的開發(fā)的同時(shí),它決沒有使并發(fā)類的編寫工作變得更繁瑣伊脓,只是使它變得更容易了。
synchronized 快速回顧
把代碼塊聲明為 synchronized魁衙,有兩個(gè)重要后果报腔,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。原子性意味著一個(gè)線程一次只能執(zhí)行由一個(gè)指定監(jiān)控對(duì)象(lock)保護(hù)的代碼剖淀,從而防止多個(gè)線程在更新共享狀態(tài)時(shí)相互沖突纯蛾。可見性則更為微妙纵隔;它要對(duì)付內(nèi)存緩存和編譯器優(yōu)化的各種反常行為翻诉。一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中捌刮、在處理器特定的緩存中碰煌,還是通過指令重排或者其他編譯器優(yōu)化),不受緩存變量值的約束绅作,但是如果開發(fā)人員使用了同步芦圾,如下面的代碼所示,那么運(yùn)行庫(kù)將確保某一線程對(duì)變量所做的更新先于對(duì)現(xiàn)有 synchronized 塊所進(jìn)行的更新俄认,當(dāng)進(jìn)入由同一監(jiān)控器(lock)保護(hù)的另一個(gè) synchronized 塊時(shí)个少,將立刻可以看到這些對(duì)變量所做的更新。類似的規(guī)則也存在于 volatile 變量上
synchronized (lockObject) {
// update object state
}
所以眯杏,實(shí)現(xiàn)同步操作需要考慮安全更新多個(gè)共享變量所需的一切夜焦,不能有爭(zhēng)用條件,不能破壞數(shù)據(jù)(假設(shè)同步的邊界位置正確)岂贩,而且要保證正確同步的其他線程可以看到這些變量的最新值茫经。通過定義一個(gè)清晰的、跨平臺(tái)的內(nèi)存模型(該模型在 JDK 5.0 中做了修改萎津,改正了原來定義中的某些錯(cuò)誤)科平,通過遵守下面這個(gè)簡(jiǎn)單規(guī)則,構(gòu)建“一次編寫姜性,隨處運(yùn)行”的并發(fā)類是有可能的:
不論什么時(shí)候瞪慧,只要您將編寫的變量接下來可能被另一個(gè)線程讀取,或者您將讀取的變量最后是被另一個(gè)線程寫入的部念,那么您必須進(jìn)行同步弃酌。
不過現(xiàn)在好了一點(diǎn)氨菇,在最近的 JVM 中,沒有爭(zhēng)用的同步(一個(gè)線程擁有鎖的時(shí)候妓湘,沒有其他線程企圖獲得鎖)的性能成本還是很低的查蓉。(也不總是這樣;早期 JVM 中的同步還沒有優(yōu)化榜贴,所以讓很多人都這樣認(rèn)為豌研,但是現(xiàn)在這變成了一種誤解,人們認(rèn)為不管是不是爭(zhēng)用唬党,同步都有很高的性能成本鹃共。)
對(duì) synchronized 的改進(jìn)
如此看來同步相當(dāng)好了,是么驶拱?那么為什么 JSR 166 小組花了這么多時(shí)間來開發(fā) java.util.concurrent.lock 框架呢霜浴?
答案很簡(jiǎn)單-同步是不錯(cuò),但它并不完美蓝纲。它有一些功能性的限制 —— 它無法中斷一個(gè)正在等候獲得鎖的線程阴孟,也無法通過輪詢得到鎖,如果不想等下去税迷,也就沒法得到鎖永丝。同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進(jìn)行,多數(shù)情況下箭养,這沒問題(而且與異常處理交互得很好)类溢,但是,確實(shí)存在一些非塊結(jié)構(gòu)的鎖定更合適的情況露懒。
ReentrantLock 類
java.util.concurrent.lock 中的 Lock 框架是鎖定的一個(gè)抽象闯冷,它允許把鎖定的實(shí)現(xiàn)作為 Java 類,而不是作為語(yǔ)言的特性來實(shí)現(xiàn)懈词。這就為 Lock 的多種實(shí)現(xiàn)留下了空間蛇耀,各種實(shí)現(xiàn)可能有不同的調(diào)度算法、性能特性或者鎖定語(yǔ)義坎弯。 ReentrantLock 類實(shí)現(xiàn)了 Lock 纺涤,它擁有與 synchronized 相同的并發(fā)性和內(nèi)存語(yǔ)義,但是添加了類似輪詢鎖抠忘、定時(shí)鎖等候和可中斷鎖等候的一些特性撩炊。此外,它還提供了在激烈爭(zhēng)用情況下更佳的性能崎脉。(換句話說拧咳,當(dāng)許多線程都想訪問共享資源時(shí),JVM 可以花更少的時(shí)候來調(diào)度線程囚灼,把更多時(shí)間用在執(zhí)行線程上骆膝。)
reentrant 鎖意味著什么呢祭衩?簡(jiǎn)單來說,它有一個(gè)與鎖相關(guān)的獲取計(jì)數(shù)器阅签,如果擁有鎖的某個(gè)線程再次得到鎖掐暮,那么獲取計(jì)數(shù)器就加1,然后鎖需要被釋放兩次才能獲得真正釋放政钟。這模仿了 synchronized 的語(yǔ)義路克;如果線程進(jìn)入由線程已經(jīng)擁有的監(jiān)控器保護(hù)的 synchronized 塊,就允許線程繼續(xù)進(jìn)行养交,當(dāng)線程退出第二個(gè)(或者后續(xù)) synchronized 塊的時(shí)候精算,不釋放鎖,只有線程退出它進(jìn)入的監(jiān)控器保護(hù)的第一個(gè) synchronized 塊時(shí)层坠,才釋放鎖殖妇。
在查看清單 1 中的代碼示例時(shí)刁笙,可以看到 Lock 和 synchronized 有一點(diǎn)明顯的區(qū)別 —— lock 必須在 finally 塊中釋放破花。否則,如果受保護(hù)的代碼將拋出異常疲吸,鎖就有可能永遠(yuǎn)得不到釋放座每! 這一點(diǎn)區(qū)別看起來可能沒什么,但是實(shí)際上摘悴,它極為重要峭梳。忘記在 finally 塊中釋放鎖,可能會(huì)在程序中留下一個(gè)定時(shí)炸彈蹂喻,當(dāng)有一天炸彈爆炸時(shí)葱椭,您要花費(fèi)很大力氣才有找到源頭在哪。而使用同步口四,JVM 將確保鎖會(huì)獲得自動(dòng)釋放孵运。
Lock lock = new ReentrantLock();
lock.lock();
try {
// update object state
}
finally {
lock.unlock();
}
針對(duì)于上面說的lock 必須在 finally 塊中釋放。否則蔓彩,如果受保護(hù)的代碼將拋出異常治笨,鎖就有可能永遠(yuǎn)得不到釋放! 進(jìn)行模擬
package com.viashare.reentrantlock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* Created by Jeffy on 16/5/18.
*/
public class ReEntrantLockMain {
static final ReentrantLock REENTRANT_LOCK = new ReentrantLock();
static final Lock LOCK = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(new ThreadTask1());
Thread thread2 = new Thread(new ThreadTask2());
thread1.setName("ThreadTask1");
thread2.setName("ThreadTask2");
thread1.start();
thread2.start();
}
static final class ThreadTask1 implements Runnable {
@Override
public void run() {
try {
System.err.println("ThreadTask1 is running");
Thread.sleep(3000);
onlyOneThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static final class ThreadTask2 implements Runnable {
@Override
public void run() {
try {
System.err.println("ThreadTask2 is running");
Thread.sleep(3000);
onlyOneThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static final void onlyOneThread() {
REENTRANT_LOCK.lock();
// 模擬異常
// int a = 12/0;
System.err.println("Thread " + Thread.currentThread().getName() + " are visiting this method");
REENTRANT_LOCK.unlock();
}
}
除此之外赤嚼,與目前的 synchronized 實(shí)現(xiàn)相比旷赖,爭(zhēng)用下的 ReentrantLock 實(shí)現(xiàn)更具可伸縮性。(在未來的 JVM 版本中更卒,synchronized 的爭(zhēng)用性能很有可能會(huì)獲得提高等孵。)這意味著當(dāng)許多線程都在爭(zhēng)用同一個(gè)鎖時(shí),使用 ReentrantLock 的總體開支通常要比 synchronized 少得多蹂空。
比較 ReentrantLock 和 synchronized 的可伸縮性
使用ReentrantLock 的性能要高于synchronized
條件變量
根類 Object 包含某些特殊的方法流济,用來在線程的 wait() 锐锣、 notify() 和 notifyAll() 之間進(jìn)行通信。這些是高級(jí)的并發(fā)性特性绳瘟,許多開發(fā)人員從來沒有用過它們 —— 這可能是件好事雕憔,因?yàn)樗鼈兿喈?dāng)微妙,很容易使用不當(dāng)糖声。幸運(yùn)的是斤彼,隨著 JDK 5.0 中引入 java.util.concurrent ,開發(fā)人員幾乎更加沒有什么地方需要使用這些方法了蘸泻。
通知與鎖定之間有一個(gè)交互 —— 為了在對(duì)象上 wait 或 notify 琉苇,您必須持有該對(duì)象的鎖。就像 Lock 是同步的概括一樣悦施, Lock 框架包含了對(duì) wait 和 notify 的概括并扇,這個(gè)概括叫作 條件(Condition) 。 Lock 對(duì)象則充當(dāng)綁定到這個(gè)鎖的條件變量的工廠對(duì)象抡诞,與標(biāo)準(zhǔn)的 wait 和 notify 方法不同穷蛹,對(duì)于指定的 Lock ,可以有不止一個(gè)條件變量與它關(guān)聯(lián)昼汗。這樣就簡(jiǎn)化了許多并發(fā)算法的開發(fā)肴熏。例如, 條件(Condition) 的 Javadoc 顯示了一個(gè)有界緩沖區(qū)實(shí)現(xiàn)的示例顷窒,該示例使用了兩個(gè)條件變量蛙吏,“not full”和“not empty”,它比每個(gè) lock 只用一個(gè) wait 設(shè)置的實(shí)現(xiàn)方式可讀性要好一些(而且更有效)鞋吉。 Condition 的方法與 wait 鸦做、 notify 和 notifyAll 方法類似,分別命名為 await 谓着、 signal 和 signalAll 泼诱,因?yàn)樗鼈儾荒芨采w Object 上的對(duì)應(yīng)方法。
公平鎖 VS 不公平鎖
如果查看 Javadoc漆魔,您會(huì)看到坷檩, ReentrantLock 構(gòu)造器的一個(gè)參數(shù)是 boolean 值,它允許您選擇想要一個(gè) 公平(fair)鎖改抡,還是一個(gè) 不公平(unfair)鎖矢炼。公平鎖使線程按照請(qǐng)求鎖的順序依次獲得鎖;而不公平鎖則允許直接獲取鎖阿纤,在這種情況下句灌,線程有時(shí)可以比先請(qǐng)求鎖的其他線程先得到鎖。
為什么我們不讓所有的鎖都公平呢?畢竟胰锌,公平是好事骗绕,不公平是不好的,不是嗎资昧?(當(dāng)孩子們想要一個(gè)決定時(shí)酬土,總會(huì)叫嚷“這不公平”。我們認(rèn)為公平非常重要格带,孩子們也知道撤缴。)在現(xiàn)實(shí)中,公平保證了鎖是非常健壯的鎖叽唱,有很大的性能成本屈呕。要確保公平所需要的記帳(bookkeeping)和同步,就意味著被爭(zhēng)奪的公平鎖要比不公平鎖的吞吐率更低棺亭。作為默認(rèn)設(shè)置虎眨,應(yīng)當(dāng)把公平設(shè)置為 false ,除非公平對(duì)您的算法至關(guān)重要镶摘,需要嚴(yán)格按照線程排隊(duì)的順序?qū)ζ溥M(jìn)行服務(wù)嗽桩。
那么同步又如何呢??jī)?nèi)置的監(jiān)控器鎖是公平的嗎钉稍?答案令許多人感到大吃一驚涤躲,它們是不公平的棺耍,而且永遠(yuǎn)都是不公平的贡未。但是沒有人抱怨過線程饑渴,因?yàn)?JVM 保證了所有線程最終都會(huì)得到它們所等候的鎖蒙袍。確保統(tǒng)計(jì)上的公平性俊卤,對(duì)多數(shù)情況來說,這就已經(jīng)足夠了害幅,而這花費(fèi)的成本則要比絕對(duì)的公平保證的低得多消恍。所以,默認(rèn)情況下 ReentrantLock 是“不公平”的以现,這一事實(shí)只是把同步中一直是事件的東西表面化而已狠怨。如果您在同步的時(shí)候并不介意這一點(diǎn),那么在 ReentrantLock 時(shí)也不必為它擔(dān)心
ReentrantLock 默認(rèn)的構(gòu)造函數(shù)是不公平的邑遏,公平意味著付出代價(jià)佣赖,性能上要大打折扣
處處都好?
看起來 ReentrantLock 無論在哪方面都比 synchronized 好 —— 所有 synchronized 能做的记盒,它都能做憎蛤,它擁有與 synchronized 相同的內(nèi)存和并發(fā)性語(yǔ)義,還擁有 synchronized 所沒有的特性,在負(fù)荷下還擁有更好的性能俩檬。那么萎胰,我們是不是應(yīng)當(dāng)忘記 synchronized ,不再把它當(dāng)作已經(jīng)已經(jīng)得到優(yōu)化的好主意呢棚辽?或者甚至用 ReentrantLock 重寫我們現(xiàn)有的 synchronized 代碼技竟?實(shí)際上,幾本 Java 編程方面介紹性的書籍在它們多線程的章節(jié)中就采用了這種方法屈藐,完全用 Lock 來做示例灵奖,只把 synchronized 當(dāng)作歷史。但我覺得這是把好事做得太過了估盘。
還不要拋棄 synchronized
雖然 ReentrantLock 是個(gè)非常動(dòng)人的實(shí)現(xiàn)瓷患,相對(duì) synchronized 來說,它有一些重要的優(yōu)勢(shì)遣妥,但是我認(rèn)為急于把 synchronized 視若敝屣擅编,絕對(duì)是個(gè)嚴(yán)重的錯(cuò)誤。 java.util.concurrent.lock 中的鎖定類是用于高級(jí)用戶和高級(jí)情況的工具 箫踩。一般來說爱态,除非您對(duì) Lock 的某個(gè)高級(jí)特性有明確的需要,或者有明確的證據(jù)(而不是僅僅是懷疑)表明在特定情況下境钟,同步已經(jīng)成為可伸縮性的瓶頸锦担,否則還是應(yīng)當(dāng)繼續(xù)使用 synchronized。
為什么我在一個(gè)顯然“更好的”實(shí)現(xiàn)的使用上主張保守呢慨削?因?yàn)閷?duì)于 java.util.concurrent.lock 中的鎖定類來說洞渔,synchronized 仍然有一些優(yōu)勢(shì)。比如缚态,在使用 synchronized 的時(shí)候磁椒,不可能忘記釋放鎖;在退出 synchronized 塊時(shí)玫芦,JVM 會(huì)為您做這件事浆熔。您很容易忘記用 finally 塊釋放鎖,這對(duì)程序非常有害桥帆。您的程序能夠通過測(cè)試医增,但會(huì)在實(shí)際工作中出現(xiàn)死鎖,那時(shí)會(huì)很難指出原因(這也是為什么根本不讓初級(jí)開發(fā)人員使用 Lock 的一個(gè)好理由老虫。)
另一個(gè)原因是因?yàn)橐豆牵?dāng) JVM 用 synchronized 管理鎖定請(qǐng)求和釋放時(shí),JVM 在生成線程轉(zhuǎn)儲(chǔ)時(shí)能夠包括鎖定信息张遭。這些對(duì)調(diào)試非常有價(jià)值邓萨,因?yàn)樗鼈兡軜?biāo)識(shí)死鎖或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個(gè)線程擁有 Lock 對(duì)象缔恳。而且宝剖,幾乎每個(gè)開發(fā)人員都熟悉 synchronized,它可以在 JVM 的所有版本中工作歉甚。在 JDK 5.0 成為標(biāo)準(zhǔn)(從現(xiàn)在開始可能需要兩年)之前万细,使用 Lock 類將意味著要利用的特性不是每個(gè) JVM 都有的,而且不是每個(gè)開發(fā)人員都熟悉的纸泄。
什么時(shí)候選擇用 ReentrantLock 代替 synchronized
既然如此赖钞,我們什么時(shí)候才應(yīng)該使用 ReentrantLock 呢?答案非常簡(jiǎn)單 —— 在確實(shí)需要一些 synchronized 所沒有的特性的時(shí)候聘裁,比如時(shí)間鎖等候雪营、可中斷鎖等候、無塊結(jié)構(gòu)鎖衡便、多個(gè)條件變量或者輪詢鎖献起。 ReentrantLock 還具有可伸縮性的好處,應(yīng)當(dāng)在高度爭(zhēng)用的情況下使用它镣陕,但是請(qǐng)記住谴餐,大多數(shù) synchronized 塊幾乎從來沒有出現(xiàn)過爭(zhēng)用,所以可以把高度爭(zhēng)用放在一邊呆抑。我建議用 synchronized 開發(fā)岂嗓,直到確實(shí)證明 synchronized 不合適,而不要僅僅是假設(shè)如果使用 ReentrantLock “性能會(huì)更好”鹊碍。請(qǐng)記住厌殉,這些是供高級(jí)用戶使用的高級(jí)工具。(而且妹萨,真正的高級(jí)用戶喜歡選擇能夠找到的最簡(jiǎn)單工具年枕,直到他們認(rèn)為簡(jiǎn)單的工具不適用為止炫欺。)乎完。一如既往,首先要把事情做好品洛,然后再考慮是不是有必要做得更快树姨。
結(jié)束語(yǔ)
Lock 框架是同步的兼容替代品,它提供了 synchronized 沒有提供的許多特性桥状,它的實(shí)現(xiàn)在爭(zhēng)用下提供了更好的性能帽揪。但是,這些明顯存在的好處辅斟,還不足以成為用 ReentrantLock 代替 synchronized 的理由转晰。相反,應(yīng)當(dāng)根據(jù)您是否 需要 ReentrantLock 的能力來作出選擇。大多數(shù)情況下查邢,您不應(yīng)當(dāng)選擇它 —— synchronized 工作得很好蔗崎,可以在所有 JVM 上工作,更多的開發(fā)人員了解它扰藕,而且不太容易出錯(cuò)缓苛。只有在真正需要 Lock 的時(shí)候才用它。在這些情況下邓深,您會(huì)很高興擁有這款工具未桥。