Java 語言設計中的一大創新就是:第一個把跨平台線程模型和鎖模型應用到語言中去,Java 語言包括了跨線程的關鍵字synchronized 和 volatile,使用關鍵字和java類庫就能夠簡單的實現線程間的同步。在簡化與平台無關的並發程序開發時,它沒有使並發程序的編寫工作變得繁瑣,反而使它變得更容易了。
在這一章,我們詳細介紹鎖的技術和概念,java中提供了兩種鎖,一個是使用關鍵字的鎖,還有一種類庫提供的鎖。
synchronized關鍵字鎖
synchronized關鍵字能夠作為函數的修飾符,也可作為函數內的語句,這就是常用的同步方法和同步語句塊。如果仔細進行分類,synchronized可作用於instance函數、instance變量、static函數和class (類 變量)身上。
在使用synchronised關鍵字時,我們需注意下面幾點:
1.無論synchronized關鍵字加在方法上還是對象上,它的鎖都是針對的java對象,而不是把一段代碼或函數。
2.每個對象只有一個鎖(lock)與之相關聯。如果要實現多個鎖,需要對應多個方法
3.實現同步需要的要系統開銷非常大,甚至可能造成死鎖。需要特別注意。
在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法,如:
public synchronized void accessVal(int newVal);
synchronized 方法控制對類成員變量的訪問:每個實例對象對應一把鎖,synchronized 方法都必須獲得該實例對象鎖后才能執行,否則所屬線程將被阻塞,方法一旦執行,就獨占該鎖,直到從該方法返回時才由虛擬機將鎖釋放,此后被該實例對象的鎖阻塞的線程方能獲得該鎖。這種機制確保了同一時刻,其所有聲明為 synchronized 的成員函數中至多只有一個能夠執行(因為至多只有一個線程能夠獲得該實例對象的鎖),從而有效地避免了成員變量的訪問沖突。
在 Java 中,每一個類(類對象)也對應一把鎖,這樣我們也可將類靜態成員函數聲明為 synchronized ,以控制對類的靜態成員變量的訪問。
synchronized 方法的缺陷:若將一個執行時間較長的方法聲明為synchronized 將會大大影響性能,因此其他任何 synchronized 方法的調用都需要等待。當然我們可以將訪問類成員變量的代碼放到專門的方法中,並將其聲明為 synchronized,並在其他方法中調用它來解決這一問題,但是 Java 為我們提供了更好的解決辦法,那就是 synchronized 語句塊。
在一個函數里,將synchronized關鍵字修飾一個對象,如:
public void method(SomeObject so) {
synchronized(so) {
//…..
}
}
這時鎖就是so對象,誰拿到這個鎖就有權運行它所控制的那段代碼。當有一個明確的對象作為鎖時,就能夠使用上面的方法加鎖,但當沒有明確的對象作為鎖,我們需要創建一個特別的實例變量來充當鎖。
當讓,類對象也能充當synchronized 塊的鎖,如MYClass.class,語義跟實例對象相同。
對象同步
wait、notify和notifyAll是java基本對象Object的線程同步方法,這些方法被聲明為final native,這些方法不能被子類重寫,下面是他們的定義:
public final void wait() throws InterruptedException
當線程調用此方法時,它就會被操作系統掛起而進入等待狀態,直到被其他線程通過notify()或者notifyAll()喚醒。該方法只能在同步方法中調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。
public final native void wait(long timeout) throws InterruptedException 當線程調用此方法時,它也會像wait()函數一樣被操作系統掛起掛起而進入等待狀態,但是它有兩種方法被喚醒:(一)其他線程通過notify()或者notifyAll()喚醒;(二)經過指定的timeout時間。它也只能在同步方法中調用,否則該方法拋出一個IllegalMonitorStateException異常。
wait()和wait(long timeout)也可以在一種特殊情況下被喚醒:其他線程調用等待線程的interrupt()函數,wait()和wait(long timeout)在拋出異常的情況下被喚醒。
public final native void notify() 隨機選擇一個在該對象上調用wait方法的線程,將其喚醒。該方法也只能在同步方法或同步塊內部調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。
public final native void notifyAll() 喚醒所有在該對象上調用wait方法的線程。該方法也只能在同步方法或同步塊內部調用。如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。
請看這些函數實現信號量的例子:
通常把一個非負整數稱為信號量(Semaphore),表示為:S,可理解為可用的資源數量.,通常假定S >=0,信號量實現同步機制表示為PV原語操作:
P(S): S減一(--S),若S<0,線程進入等待隊列;否則繼續執行
V(S):S加一(++S),若S<=0喚醒處於等待中的一個線程;
public abstract class Semaphore {
private int value = 0;
public Semaphore() {
}
public Semaphore(int initial) {
if (initial >= 0)
value = initial;
else
throw new IllegalArgumentException("initial < 0");
}
public final synchronized void P() throws InterruptedException {
value--;
if(value < 0)
wait();
}
public final synchronized void V() {
value++;
if(value <=0)
notify();
}
}
有一個問題:wait()函數會不會釋放synchronized對象鎖,答案是肯定的。我們拿上面信號量解釋這個問題,假如wait()函數不釋放鎖時,我們假定S=1,當執行P操作時,運行的線程拿到當前信號量的鎖,在減一操作后,調用wait操作,然后當前線程被掛起,此時當另外一個線程執行V操作時,也需要獲得synchronized對象鎖,因為鎖被掛起的線程擁有,執行V操作不能執行notify函數,被掛起的線程也不能被喚醒,這就造成了死鎖。實際上,這種PV操作時能夠正確執行,原因是wait函數釋放了線程擁有的synchronized對象鎖。Wait函數主要做了下面幾件事:
- 將當前線程加入等待隊列
- 釋放線程占有的鎖,然后掛起自己
- 線程喚醒后,將自己移出等待隊列
- 重新獲得對象鎖,然后返回
再有一個問題:在什么樣的情況下,線程會釋放持有的鎖?大約有如下幾種情況:
- 執行完同步代碼塊
- 在執行同步代碼塊的過程中,遇到異常而導致線程終止。
- 在執行同步代碼塊的過程中,執行了鎖所屬對象的wait()方法,這個線程會釋放鎖。
在下面幾種情況下,線程是不會釋放持有的鎖:
- 在執行同步代碼塊的過程中,執行了Thread.sleep()方法,當前線程放棄CPU,開始睡眠,在睡眠中不會釋放鎖
- 在執行同步代碼塊的過程中,執行了Thread.yield()方法,當前線程放棄CPU,但不會釋放鎖。
- 在執行同步代碼塊的過程中,其他線程執行了suspend()方法,當前線程被暫停,但不會釋放鎖。但Thread類的suspend()方法已經被廢棄。
再講完java最基本的鎖和同步技術后,我們來介紹java類庫提供的鎖和同步技術
Lock接口
前面的介紹,可以看出synchronized實現同步是一種相當不錯的方法,那么為什么JAVA團隊還要花許多時間來開發 java.util.concurrent.lock
框架呢?答案是synchronized關鍵字提供的同步比較簡單,功能有一些限制: 它無法中斷一個正在等候獲得鎖的線程,也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖。synchronized同步還要求鎖的釋放只能在與獲得鎖所在的堆棧幀相同的堆棧幀中進行。
java.util.concurrent.lock中的Lock框架是鎖的一個抽象,它允許把鎖的實現作為 Java 類,而不是作為語言的特性來實現。這就為Lock
的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。 ReentrantLock
類實現了 Lock
,它擁有與synchronized
相同的並發性和語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上)
java.util.concurrent.lock接口中定義了下面幾個方法:
void lock(); 典型的加鎖操作,不同的鎖有不同的語義,當線程調用ReentrantLock.lock方法時,鎖計數加一,它在兩種情況下才可獲得鎖:(一)當前線程是鎖的持有者;(二)鎖沒有被其它線程持有。
void lockInterruptibly() throws InterruptedException; 可中斷加鎖操作,與lock操作只有一點不同:允許在等待時可由其它線程調用等待線程的Thread.interrupt方法來中斷等待線程的等待而直接返回,這時不會獲取鎖,而會拋出一個InterruptedException,而lock方法不允許Thread.interrupt中斷,即使檢測到Thread.isInterrupted,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。
boolean tryLock(); 試圖執行加鎖操作,如果當前鎖可以執行加鎖操作,則對其加鎖並且返回true,否則僅僅返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 類似於tryLock方法,也是一種試圖加鎖操作,唯一的一點區別:當鎖不可達時,線程可以等待指定的time時間,經過time后,仍舊不能獲得鎖,則返回false,否則執行加鎖並且返回true。當然,線程也可以被其它線程執行Thread.interrupt方法中斷。
void unlock(); 典型的釋放鎖操作,不同的鎖有不同的語義,當線程調用ReentrantLock.unlock方法時,鎖計數減一,它必須滿足兩種情況才能釋放鎖:(一)當前線程是鎖的持有者;(二)ReentrantLock鎖的計算為0
Condition newCondition(); 創建一個條件變量,此條件可量類似於Object的wait, notify操作,可能實現線程的同步。我們將在后面的條件變量部分,詳細介紹條件變量的操作。
條件變量
Object
提供了 wait()
, notify()
和notifyAll()
些特殊的方法,用來在線程之間進行通信。這些都是些高級的並發性特性,許多開發人員從來沒有用過它們,這可能是件好事,因為很難合適地使用它們。就像 Lock
一樣, Lock
框架包含對 wait
和 notify
的定義,被叫作 條件變量(Condition)
。 與標准的 wait
和notify
方法不同,對於指定的 Lock
,可以有不止一個條件變量與它關聯,這樣就簡化了許多並發算法的開發。例如Javadoc中的條件變量(Condition)
顯示了一個有界緩沖區實現的示例,該示例使用了兩個條件變量“not full”和“not empty”,它比每個 lock 只用一個 wait 設置的實現方式可讀性要好一些。 Condition
的方法與 wait
、 notify
和 notifyAll
方法類似,分別命名為 await
、 signal
和signalAll
,因為它們不能必須實現 Object
上的對應方法。
請看Condition包含的主要方法:
void await() throws InterruptedException; 功能相似於Object的wait方法,線程執行此方法於,會被操作系統掛起而進入等待狀態,直到被signal()和signalAll()喚醒,它是一中可中斷的操作,等待線程被它行程調用Thread.interrupt后,拋出InterruptedException異常。在調用前,線程也必須加鎖,如果當前線程不是鎖的持有者,該方法拋出一個IllegalMonitorStateException異常。
void awaitUninterruptibly(); 不可中斷的等待操作,類似於await,它不能被其它線程中斷喚醒。
long awaitNanos(long nanosTimeout) throws InterruptedException; 類似於await,多了一個納秒級別的時間參數,經過nanosTimeout時間后,線程也會喚醒,有一個大的區別:函數會返回所剩微秒數的一個估計值,如果超時,則返回一個小於等於 0 的值。可以用此值來確定在等待返回但某一等待條件仍不具備的情況下,是否要再次等待,以及再次等待的時間。
boolean await(long time, TimeUnit unit) throws InterruptedException; 類似於awaitNanos操作,都可以經過指定的時間后被喚醒,只是時間的方式有所變量,由TimeUnit指定,可以是納秒,微妙,毫秒,秒,分,等待。此方法在行為上等效於: awaitNanos(unit.toNanos(time)) > 0
boolean awaitUntil(Date deadline) throws InterruptedException; 類似於awaitNanos(long nanosTimeout)和await(long time, TimeUnit unit),調用此線程會造成當前線程在接到信號、被中斷或到達指定最后期限之前一直處於等待狀態
void signal();
喚醒一個等待線程,如果所有的線程都在等待此條件,則選擇其中的一個喚醒
void signalAll(); 喚醒所有等待線程,如果所有的線程都在等待此條件,則喚醒所有線程。
通過下面兩個例子,可以幫助我們理解條件變量的概念
先簡單介紹一下ReentrantLock,它是一個實現Lock接口的類,它的語義跟關鍵字synchronized一樣,是一個可重入式鎖。它的功能比關鍵字synchronized強一點。后面會詳細介紹ReentrantLock
實現一個緩沖池的代碼
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
實現信號量的代碼
public abstract class Semaphore {
private int value = 0;
private final Lock lock = new ReentrantLock();
private Condition signal = lock.newCondition();
public Semaphore() {
}
public Semaphore(int initial) {
if (initial >= 0)
value = initial;
else
throw new IllegalArgumentException("initial < 0");
}
public final synchronized void P() throws InterruptedException {
lock.lock();
try {
value--;
if(value < 0)
signal.wait();
} finally {
lock.unlock();
}
}
public final synchronized void V() {
lock.lock();
try {
value--;
if (value <= 0)
signal.signal();
} finally {
lock.unlock();
}
}
}
ReentrantLock
ReentrantLock 鎖意味着什么呢?簡單來說,它是一個實現上述Lock接口的類型,它還有一個與鎖相關的計數器,如果擁有鎖的某個線程再次得到鎖,那么獲取計數器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這跟 synchronized
的語義是一樣的,如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線程繼續進行,當線程退出第二個 synchronized
塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個synchronized
塊時,才釋放鎖。
下面這段代碼說明了ReentrantLock的用法
Lock lock = new ReentrantLock();
lock.lock();
try {
// code
}
finally {
lock.unlock();
}
從上面的代碼示例時,可以看到 Lock
和 synchronized 有一點明顯的區別:lock 必須在 finally 塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什么,但是實際上,它極為重要。忘記在 finally 塊中釋放鎖,可能會在程序中留下一個巨大的問題,當問題出現時,您要花費很大力氣才有找到源頭。然而使用synchronized同步,JVM 會主動鎖釋放鎖。
ReentrantLock公平鎖(fair)和非公平鎖(unfair)
ReentrantLock提供了兩個構造器,一個帶參數和另外一個不帶參數:參數是 boolean 值,它表明用戶是選用一個公平(fair)鎖,還是一個不公平(unfair)鎖。公平鎖使線程按照請求鎖的順序依次獲得鎖;而不公平鎖則不一定,在不公平種情況下,線程有時可以比先請求鎖的其他線程先得到鎖。
public ReentrantLock(boolean fair)
public ReentrantLock()
為什么我們不讓所有的鎖都公平呢?從競爭關系的角度看,公平是好事。從性能上看,就意味着被爭奪的公平鎖要比不公平鎖的吞吐率更低。作為默認設置,ReentrantLock選擇講公平參數設置為false而是用非公平鎖。 那么同步又如何呢?內置的監控器鎖是公平的嗎?答案依舊是是不公平的,而且永遠都是不公平的。但是沒有人抱怨過線程飢渴,因為 JVM 保證了所有線程最終都會得到它們所等候的鎖。因此默認情況下ReentrantLock是“不公平”的,下面兩組圖清晰的展示了性能:公平是有代價的。如果您需要公平,就必須付出代價,請不要把它作為您的默認選擇。
那么如何選擇ReentrantLock 和 synchronized? 實驗證明表現ReentrantLock 無論在哪方面都比 synchronized 好:所有 synchronized 能做的,它都能做,它擁有與 synchronized 相同的內存和並發性語義,還擁有synchronized所沒有的特性,在高負荷下還擁有更好的性能。那么,我們是不是應當忘記synchronized,不再使用它呢,或者甚至用ReentrantLock重寫我們現有的synchronized代碼?實際上,好幾本介紹 Java 編程方面的書籍在多線程的章節中就采用了這種方法,完全用Lock來做示例,只把 synchronized 當作歷史。但我覺得這是把好事做得太過了。
雖然 ReentrantLock 是個非常好的實現,相對 synchronized 來說,它有一些重要的優勢,但是把完全無視synchronized將絕對是個嚴重的錯誤。一般來說,除非用戶對 Lock 的某個高級特性有明確的需要,或者能夠表明synchronized同步已經成為性能瓶頸,否則還是應當繼續使用 synchronized。對於java.util.concurrent.lock中的鎖來說,synchronized 仍然有一些優勢。比如在使用 synchronized 的時候,不能忘記釋放鎖;在退出 synchronized 塊時,JVM 會為您做這件事。您很容易忘記用 finally 塊釋放鎖,這對程序非常有害。另一個原因是因為,當 JVM 用 synchronized 管理鎖請求和釋放時,JVM 在生成線程轉儲時能夠包括鎖定信息。這些對調試非常有價值,因為它們能標識死鎖或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。而且幾乎每個開發人員都熟悉 synchronized,它可以運行在 JVM 的所有版本中。在 JDK 5.0 成為標准之前,使用 Lock 類將意味着特性不是每個 JVM 都有的,而且不是每個開發人員熟悉的。
既然如此,我們什么時候才應該使用 ReentrantLock 呢?答案非常簡單:在確實需要一些 synchronized 所沒有的特性的時候,比如時間鎖等候、可中斷鎖等候、無塊結構鎖、多個條件變量或者鎖投票。 ReentrantLock 還具有可伸縮性的好處,應當在高度爭用的情況下使用它,但是請記住,大多數 synchronized 塊幾乎沒有出現過高爭用,所以可以把高度爭用放在一邊。建議用 synchronized 開發,直到確實證明 synchronized 不合適,而不要僅僅是假設使用 ReentrantLock “性能會更好”,他們是高級用戶使用的工具。
ReentrantLock加鎖與解鎖分析
下面我們稍微分析一下ReentrantLock是怎么實現的
經過觀察ReentrantLock把所有Lock接口的操作都委派到一個Sync類上,該類繼承了AbstractQueuedSynchronizer:
static abstract class Sync extends AbstractQueuedSynchronizer
Sync又有兩個子類:
final static class NonfairSync extends Sync
final static class FairSync extends Sync
ReentrantLock默認是一種非公平鎖,通過下面的構造函數和lock函數,可以看出鎖的真真實現是NonfairSync
public ReentrantLock() {
sync = new NonfairSync();
}
public void lock() {
sync.lock();
}
NonfairSync.lock主要做兩件事:(1)原子比較並設置計算,如果成功設置,說明鎖是空閑的,當前線程獲得鎖,並把當前線程設置為鎖擁有者;(2)否則,調用acquire方法
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
NonfairSync. Acquire主要做下面幾件事:嘗試以獨占的方式獲得鎖,如果失敗,就把當前線程封裝為一個Node,加入到等待隊列中;如果加入隊列成功,接下來檢查當前線程的節點是否應該等待(掛起),如果當前線程所處節點的前一節點的等待狀態小於0,則通過LockSupport掛起當前線程;無論線程是否被掛起,或者掛起后被激活,都應該返回當前線程的中斷狀態,如果處於中斷狀態,需要中斷當前線程
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
nonfairTryAcquire代碼比較容易理解,嘗試進行加鎖:(1)如果鎖狀態空閑(state=0),且通過原子的比較並設置操作,則當前線程獲得鎖,並把當前線程設置為鎖擁有者; (2)如果鎖狀態空閑,且原子的比較並設置操作失敗,那么返回false,說明嘗試獲得鎖失敗; (3)否則,檢查當前線程與鎖擁有者線程是否相等(可重入鎖),如果相等,增加鎖狀態計數,並返回true; (4)如果不是以上情況,說明鎖已經被其他的線程持有,直接返回false;
addWaiter操作,主要進行節點操作:創建一個新的節點,並將節點加入到等待隊列中。
acquireQueued操作設計的相當完美,從邏輯結構上看,采用無條件的循環語句,感覺如果p == head && tryAcquire(arg)條件不滿足循環將永遠無法結束,當然不會出現死循環,原因在於parkAndCheckInterrupt會把 當前線程掛起,從而阻塞住線程的調用棧
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} catch (RuntimeException ex) {
cancelAcquire(node);
throw ex;
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport.park最終把線程交給系統內核進行阻塞。當然也不是馬上把請求不到鎖的線程進行阻塞,還要檢查該線程 的狀態,比如如果該線程處於Cancel狀態則沒有必要,具體的檢查在shouldParkAfterFailedAcquire中:通過對前一個節點等待狀態分析,來決定是否需要阻塞。
NonfairSync.unlock是一個解鎖的過程,調用release函數進行解鎖,release函數執行:(1)先判斷是否可以解鎖,如果tryRelease返回false,則不解鎖;(2)如果可以解鎖,對頭節點的狀態進行判斷,是否可以喚醒一個線程。
tryRelease語義很明確:如果線程多次鎖定,則進行多次釋放,直至status==0則真正釋放鎖,所謂釋放鎖即設置status為0。
解鎖代碼相對簡單,主要體現在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
在這里介紹LockSupport提供的幾個操作系統阻塞很喚醒線程函數,ReentrantLock的實現依賴這些函數:
public static void park() 使當前線程處於等待狀態,直到被其它系統調用unpark喚醒。
public static void parkNanos(long nanos) 使當前線程處於等待狀態,直到被其它系統調用unpark喚醒或者超過nanos指定的時間。
public static void parkUntil(long deadline) 使當前線程處於等待狀態,直到被其它系統調用unpark喚醒或者超過nanos指定的時間。
public static void park(Object blocker)
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(Object blocker, long deadline)
這三個函數的含義跟上面的park函數意思一樣,只是多了一個參數blocker,blcoker會記錄在Thread的一個parkBlocker屬性中,通過jstack命令可以非常方便的監控具體的阻塞對象。
public static void unpark(Thread thread) 喚醒thread指定的線程
這些lock都可以被其他線程中斷喚醒。
ReentrantReadWriteLock
ReentrantLock也實現了Lock接口,它是一種可讀寫鎖。在通常情況下規定任何“讀/讀”,“寫/讀”,“寫/寫”操作都不能同時發生。但是我們知道一個概念:鎖是有一定的開銷,當並發比較大的時候,鎖的開銷就非常明顯了,所以如果可能的話就盡量少使用鎖。
在現實應用中,一種資源往往被很多線程讀,而僅僅少數線程寫,hbase中很多資源就是這種情況,采用synchronized或者ReentrantLock盡管能夠保證資源讀寫的正確性,但是在多線程環境中,性能已經成為瓶頸,我們需要采用一種新的機制能夠在保證資源被正確讀寫的同時,性能也沒有受到大的影響。ReadWriteLock就是這樣一種鎖,它通常描述的場景是:一個資源能夠被多個讀線程訪問,或者被一個或少數寫線程訪問,但是不能同時存在讀寫線程。也就是說讀寫鎖使用的場合是一個共享資源被大量讀取操作,而只有少量的寫操作。
ReentrantReadWriteLock實現了ReadWriteLock接口,使用兩把鎖來解決問題,一個讀鎖,一個寫鎖,線程進入讀鎖的前提條件:
1. 沒有其他線程的寫鎖,
2. 沒有寫請求
3. 有寫請求,但調用線程和持有鎖的線程是同一個(鎖的降級:允許從寫鎖定降級為讀鎖定,ReentrantReadWriteLock允許先獲取寫鎖,然后獲取讀鎖,最后釋放寫鎖。但是,從讀鎖升級到寫鎖是不可能的)
線程進入寫鎖的前提條件:
1. 沒有其他線程的讀鎖
2. 沒有其他線程的寫鎖
下面列出ReadWriteLock接口
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock並不是Lock的子接口,而是獲取鎖的接口:readLock獲取讀鎖;writeLock獲取寫鎖。獲得鎖后,就可以使用Lock提供的API,使用的方法類似於ReentrantLock:
lock.lock();
try {
// code
}
finally {
lock.unlock();
下面的代碼展示了一個簡單的緩存系統:
public class Cache {
private Map<String, Object> cache = new HashMap<String, Object>();
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public Object getData(String key) {
rwl.readLock().lock(); // 添加讀鎖
Object value = null;
try {
value = cache.get(key);
} finally {
rwl.readLock().unlock(); //釋放讀鎖
}
return value;
}
public void putData(String key, Object value){
rwl.writeLock().lock(); // 添加寫鎖
try{
cache.put(key, value);
}finally{
rwl.writeLock().unlock(); //釋放寫鎖
}
}
}
在這里就不解釋ReentranReadWriteLock內部實現,有興趣的讀者可以閱讀它的源代碼。我們對ReentrantReadWriteLock做一個總結,它與ReentrantLock一樣都是單獨的實現,彼此之間沒有繼承或實現的關系。ReentrantReadWriteLock有如下鎖機制的特性了:
1. 在重入方面,其內部的WriteLock可以獲取ReadLock,但是反過來ReadLock想要獲得WriteLock不可能的。WriteLock可以降級為ReadLock,順序是:先獲得WriteLock再獲得ReadLock,然后釋放WriteLock,這時候線程將保持Readlock的持有。反過來ReadLock想要升級為WriteLock則不可能。
2. ReadLock可以被多個線程持有並且排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最為重要,因為對於高讀取頻率而相對較低寫入的數據結構,使用此類鎖同步機制則可以提高並發量。
3. 不管是ReadLock還是WriteLock都支持Interrupt,語義與ReentrantLock一致。
4. WriteLock支持Condition並且與ReentrantLock語義一致,而ReadLock則不能使用Condition,否則拋出UnsupportedOperationException異常。