Java中的synchronized和ReentrantLock的區(qū)別詳細解讀
前言
軟件并發(fā)已經(jīng)成為現(xiàn)代軟件開發(fā)的基礎(chǔ)能力,而 Java 精心設(shè)計的高效并發(fā)機制,正是構(gòu)建大規(guī)模應(yīng)用的基礎(chǔ)之一。
本篇博文的重點是,synchronized 和 ReentrantLock 有什么區(qū)別? 有人說 synchronized 最慢,這話靠譜嗎?
常見回答
synchronized 是 Java 內(nèi)建的同步機制,所以也有人稱其為 Intrinsic Locking,它提供了互斥的語義和可見性,當(dāng)一個線程已經(jīng)獲取當(dāng)前鎖時,其他試圖獲取的線程只能等待或者阻塞在那里。
在 Java 5 以前,synchronized 是僅有的同步手段,在代碼中, synchronized 可以用來修飾方法,也可以使用在特定的代碼塊兒上,本質(zhì)上 synchronized 方法等同于把方法全部語句用 synchronized 塊包起來。
ReentrantLock,通常翻譯為再入鎖,是 Java 5 提供的鎖實現(xiàn),它的語義和 synchronized 基本相同。再入鎖通過代碼直接調(diào)用 lock() 方法獲取,代碼書寫也更加靈活。與此同時,ReentrantLock 提供了很多實用的方法,能夠?qū)崿F(xiàn)很多 synchronized 無法做到的細節(jié)控制,比如可以控制 fairness,也就是公平性,或者利用定義條件等。但是,編碼中也需要注意,必須要明確調(diào)用 unlock() 方法釋放,不然就會一直持有該鎖。
synchronized 和 ReentrantLock 的性能不能一概而論,早期版本 synchronized 在很多場景下性能相差較大,在后續(xù)版本進行了較多改進,在低競爭場景中表現(xiàn)可能優(yōu)于 ReentrantLock。
具體分析
對于并發(fā)編程,不同公司或者面試官面試風(fēng)格也不一樣,有個別大廠喜歡一直追問你相關(guān)機制的擴展或者底層,有的喜歡從實用角度出發(fā),所以你在準(zhǔn)備并發(fā)編程方面需要一定的耐心。
鎖作為并發(fā)的基礎(chǔ)工具之一,至少需要掌握:
- 理解什么是線程安全。
- synchronized、ReentrantLock 等機制的基本使用與案例。
更進一步,你還需要:
- 掌握 synchronized、ReentrantLock 底層實現(xiàn);理解鎖膨脹、降級;理解偏斜鎖、自旋鎖、輕量級鎖、重量級鎖等概念。
- 掌握并發(fā)包中 java.util.concurrent.lock 各種不同實現(xiàn)和案例分析。
實戰(zhàn)剖析
首先,我們需要理解什么是線程安全。
在 Brain Goetz 等專家撰寫的《Java 并發(fā)編程實戰(zhàn)》(Java Concurrency in Practice)中,線程安全是一個多線程環(huán)境下正確性的概念,也就是保證多線程環(huán)境下共享的、可修改的狀態(tài)的正確性,這里的狀態(tài)反映在程序中其實可以看作是數(shù)據(jù)。
換個角度來看,如果狀態(tài)不是共享的,或者不是可修改的,也就不存在線程安全問題,進而可以推理出保證線程安全的兩個辦法:
- 封裝:通過封裝,我們可以將對象內(nèi)部狀態(tài)隱藏、保護起來。
- 不可變:final 和 immutable 就是這個道理,Java 語言目前還沒有真正意義上的原生不可變,但是未來也許會引入。
線程安全需要保證幾個基本特性:
- 原子性,簡單說就是相關(guān)操作不會中途被其他線程干擾,一般通過同步機制實現(xiàn)。
- 可見性,是一個線程修改了某個共享變量,其狀態(tài)能夠立即被其他線程知曉,通常被解釋為將線程本地狀態(tài)反映到主內(nèi)存上,volatile 就是負責(zé)保證可見性的。
- 有序性,是保證線程內(nèi)串行語義,避免指令重排等。
可能有點晦澀,那么我們看看下面的代碼段,分析一下原子性需求體現(xiàn)在哪里。這個例子通過取兩次數(shù)值然后進行對比,來模擬兩次對共享狀態(tài)的操作。
你可以編譯并執(zhí)行,可以看到,僅僅是兩個線程的低度并發(fā),就非常容易碰到 former 和 latter 不相等的情況。這是因為,在兩次取值的過程中,其他線程可能已經(jīng)修改了 sharedState。
public class ThreadSafeSample { public int sharedState; public void nonSafeAction() { while (sharedState < 100000) { int former = sharedState++; int latter = sharedState; if (former != latter - 1) { System.out.printf("Observed data race, former is " + former + ", " + "latter is " + latter); } } } public static void main(String[] args) throws InterruptedException { ThreadSafeSample sample = new ThreadSafeSample(); Thread threadA = new Thread(){ public void run(){ sample.nonSafeAction(); } }; Thread threadB = new Thread(){ public void run(){ sample.nonSafeAction(); } }; threadA.start(); threadB.start(); threadA.join(); threadB.join(); } }
以下是某次運行結(jié)果:
Observed data race, former is 9851, latter is 9853
將兩次賦值過程用 synchronized 保護起來,使用 this 作為互斥單元,就可以避免別的線程并發(fā)的去修改 sharedState。
synchronized (this) { int former = sharedState ++; int latter = sharedState; // … }
如果用 javap 反編譯,可以看到類似片段,利用 monitorenter/monitorexit 對實現(xiàn)了同步的語義:
11: astore_1 12: monitorenter 13: aload_0 14: dup 15: getfield #2 // Field sharedState:I 18: dup_x1 … 56: monitorexit
代碼中使用 synchronized 非常便利,如果用來修飾靜態(tài)方法,其等同于利用下面代碼將方法體囊括進來:
synchronized (ClassName.class) {}
再來看看 ReentrantLock。你可能好奇什么是再入?它是表示當(dāng)一個線程試圖獲取一個它已經(jīng)獲取的鎖時,這個獲取動作就自動成功,這是對鎖獲取粒度的一個概念,也就是鎖的持有是以線程為單位而不是基于調(diào)用次數(shù)。Java 鎖實現(xiàn)強調(diào)再入性是為了和 pthread 的行為進行區(qū)分。
再入鎖可以設(shè)置公平性(fairness),我們可在創(chuàng)建再入鎖時選擇是否是公平的。
ReentrantLock fairLock = new ReentrantLock(true);
這里所謂的公平性是指在競爭場景中,當(dāng)公平性為真時,會傾向于將鎖賦予等待時間最久的線程。公平性是減少線程“饑餓”(個別線程長期等待鎖,但始終無法獲取)情況發(fā)生的一個辦法。
如果使用 synchronized,我們根本無法進行公平性的選擇,其永遠是不公平的,這也是主流操作系統(tǒng)線程調(diào)度的選擇。通用場景中,公平性未必有想象中的那么重要,Java 默認的調(diào)度策略很少會導(dǎo)致 “饑餓”發(fā)生。與此同時,若要保證公平性則會引入額外開銷,自然會導(dǎo)致一定的吞吐量下降。所以,我建議只有當(dāng)你的程序確實有公平性需要的時候,才有必要指定它。
我們再從日常編碼的角度學(xué)習(xí)下再入鎖。為保證鎖釋放,每一個 lock() 動作,我建議都立即對應(yīng)一個 try-catch-finally,典型的代碼結(jié)構(gòu)如下,這是個良好的習(xí)慣。
ReentrantLock fairLock = new ReentrantLock(true);// 這里是演示創(chuàng)建公平鎖,一般情況不需要。 fairLock.lock(); try { // do something } finally { fairLock.unlock(); }
ReentrantLock 相比 synchronized,因為可以像普通對象一樣使用,所以可以利用其提供的各種便利方法,進行精細的同步操作,甚至是實現(xiàn) synchronized 難以表達的用例,如:
- 帶超時的獲取鎖嘗試。
- 可以判斷是否有線程,或者某個特定線程,在排隊等待獲取鎖。
- 可以響應(yīng)中斷請求。
- ...
這里我特別想強調(diào)條件變量(java.util.concurrent.Condition),如果說 ReentrantLock 是 synchronized 的替代選擇,Condition 則是將 wait、notify、notifyAll 等操作轉(zhuǎn)化為相應(yīng)的對象,將復(fù)雜而晦澀的同步操作轉(zhuǎn)變?yōu)橹庇^可控的對象行為。
條件變量最為典型的應(yīng)用場景就是標(biāo)準(zhǔn)類庫中的 ArrayBlockingQueue 等。
參考下面的源碼,首先,通過再入鎖獲取條件變量:
/** Condition for waiting takes */ private final Condition notEmpty; /** Condition for waiting puts */ private final Condition notFull; public ArrayBlockingQueue(int capacity, boolean fair) { if (capacity <= 0) throw new IllegalArgumentException(); this.items = new Object[capacity]; lock = new ReentrantLock(fair); notEmpty = lock.newCondition(); notFull = lock.newCondition(); }
兩個條件變量是從同一再入鎖創(chuàng)建出來,然后使用在特定操作中,如下面的 take 方法,判斷和等待條件滿足:
public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { while (count == 0) notEmpty.await(); return dequeue(); } finally { lock.unlock(); } }
當(dāng)隊列為空時,試圖 take 的線程的正確行為應(yīng)該是等待入隊發(fā)生,而不是直接返回,這是 BlockingQueue 的語義,使用條件 notEmpty 就可以優(yōu)雅地實現(xiàn)這一邏輯。
那么,怎么保證入隊觸發(fā)后續(xù) take 操作呢?請看 enqueue 實現(xiàn):
private void enqueue(E e) { // assert lock.isHeldByCurrentThread(); // assert lock.getHoldCount() == 1; // assert items[putIndex] == null; final Object[] items = this.items; items[putIndex] = e; if (++putIndex == items.length) putIndex = 0; count++; notEmpty.signal(); // 通知等待的線程,非空條件已經(jīng)滿足 }
通過 signal/await 的組合,完成了條件判斷和通知等待線程,非常順暢就完成了狀態(tài)流轉(zhuǎn)。
注意,signal 和 await 成對調(diào)用非常重要,不然假設(shè)只有 await 動作,線程會一直等待直到被打斷(interrupt)。
從性能角度,synchronized 早期的實現(xiàn)比較低效,對比 ReentrantLock,大多數(shù)場景性能都相差較大。
但是在 Java 6 中對其進行了非常多的改進,可以參考性能對比,在高競爭情況下,ReentrantLock 仍然有一定優(yōu)勢。
我在下一講進行詳細分析,會更有助于理解性能差異產(chǎn)生的內(nèi)在原因。
在大多數(shù)情況下,無需糾結(jié)于性能,還是考慮代碼書寫結(jié)構(gòu)的便利性、可維護性等。
后記
以上就是 Java:synchronized 和 ReentrantLock 有什么區(qū)別呢? 的所有內(nèi)容了;
介紹了什么是線程安全,對比和分析了 synchronized 和 ReentrantLock,并針對條件變量等方面結(jié)合案例代碼進行了介紹。
到此這篇關(guān)于Java中的synchronized和ReentrantLock的區(qū)別詳細解讀的文章就介紹到這了,更多相關(guān)synchronized和ReentrantLock區(qū)別內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot整合netty框架實現(xiàn)站內(nèi)信
Netty 是一個基于NIO的客戶、服務(wù)器端編程框架,使用Netty 可以確保你快速和簡單的開發(fā)出一個網(wǎng)絡(luò)應(yīng)用,這篇文章主要介紹了springboot整合netty框架的方式小結(jié),需要的朋友可以參考下2022-12-12springboot異步處理@NotBlank或@NotNull注釋校驗不生效問題
這篇文章主要介紹了springboot異步處理@NotBlank或@NotNull注釋校驗不生效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01