轉自:http://blog.csdn.net/vking_wang/article/details/9952063 (【Java線程】鎖機制:synchronized、Lock、Condition)
一.ReentrantReadWriteLock(讀寫鎖)的使用
Lock比傳統線程模型中的synchronized方式更加面向對象,與生活中的鎖類似,鎖本身也應該是一個對象。兩個線程執行的代碼片段要實作同步互斥的效果,它們必須用同一個Lock對象。
讀寫鎖:分為讀鎖和寫鎖,多個讀鎖不互斥(共享讀鎖),讀鎖與寫鎖互斥(互斥寫鎖),這是由jvm自己控制的,你隻要上好相應的鎖即可。如果你的代碼隻讀資料,可以很多人同時讀,但不能同時寫,那就上讀鎖;如果你的代碼修改資料,隻能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
ReentrantReadWriteLock會使用兩把鎖來解決問題,一個讀鎖,一個寫鎖
線程進入讀鎖的前提條件:
沒有其他線程的寫鎖,
沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個
線程進入寫鎖的前提條件:
沒有其他線程的讀鎖
沒有其他線程的寫鎖
提到ReentrantReadWriteLock,首先要做的是與ReentrantLock劃清界限。它和後者都是單獨的實作,彼此之間沒有繼承或實作的關系。
然後就是總結這個鎖機制的特性了:
(a).重入方面其内部的WriteLock可以擷取ReadLock,但是反過來ReadLock想要獲得WriteLock則永遠都不要想。
(b).WriteLock可以降級為ReadLock,順序是:先獲得WriteLock再獲得ReadLock,然後釋放WriteLock,這時候線程将保持Readlock的持有。反過來ReadLock想要更新為WriteLock則不可能,為什麼?參看(a)
(c).ReadLock可以被多個線程持有并且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最為重要,因為對于高讀取頻率而相對較低寫入的資料結構,使用此類鎖同步機制則可以提高并發量。
(d).不管是ReadLock還是WriteLock都支援Interrupt,語義與ReentrantLock一緻。
(e).WriteLock支援Condition并且與ReentrantLock語義一緻,而ReadLock則不能使用Condition,否則抛出UnsupportedOperationException異常。
---------- 以上摘抄自讀寫鎖的使用http://www.cnblogs.com/liuling/p/2013-8-21-03.html
ReentrantReadWriteLock 與 synchronized 的差別:
1.拆分讀寫鎖場景. 提高并發量:
ReadLock可以被多個線程持有并且在作用時排斥任何的WriteLock,而WriteLock則是完全的互斥。這一特性最為重要,因為對于高讀取頻率而相對較低寫入的資料結構,使用此類鎖同步機制則可以提高并發量。
2.不管是ReadLock還是WriteLock都支援Interrupt,語義與ReentrantLock一緻。
3.寫鎖支援Condition重入.
使用ReentrantReadWriteLock
1,放在成員變量的位置.
2,如果聲明為static 則表示是類鎖.對執行個體對象共享.
如果沒有聲明為static 則表示是對象鎖. 對單列對象共享.對多個new出來的對象不共享.
3,使用時聲明為final. 表示不允許修改引用指向.
4,一把對象鎖在多個同步代碼中的使用如下.
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuIzMykDOmNWZzYTO50SO4MWOtMGMmNTL1YGZw0iN0YDM3ITYl9CX1ITO08CX0ITMw8CX05WZth2YhRHdh9CXkF2bsBXdvwVbvNmLllXZ0lmLywGZvw1LcpDc0RHaiojIsJye.png)
二. ReentrantLock(重入鎖)的使用
synchronized原語和ReentrantLock在一般情況下沒有什麼差別,但是在非常複雜的同步應用中,請考慮使用ReentrantLock,
特别是遇到下面幾種種需求的時候。
1.某個線程在等待一個鎖的控制權的這段時間需要中斷
2.需要分開處理一些wait-notify,ReentrantLock裡面的Condition應用,能夠控制notify哪個線程
3.具有公平鎖功能,每個到來的線程都将排隊等候
先說第一種情況
ReentrantLock的lock機制有2種,忽略中斷鎖和響應中斷鎖,這給我們帶來了很大的靈活性。比如:如果A、B2個線程去競争鎖,A線程得到了鎖,B線程等待,但是A線程這個時候實在有太多事情要處理,就是一直不傳回,B線程可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。
這個時候ReentrantLock就提供了2種機制,
第一,B線程中斷自己(或者别的線程中斷它),但是ReentrantLock不去響應,繼續讓B線程等待,你再怎麼中斷,我全當耳邊風(synchronized原語就是如此);
第二,B線程中斷自己(或者别的線程中斷它),ReentrantLock處理了這個中斷,并且不再等待這個鎖的到來,完全放棄。
ReentrantLock是一個互斥的同步器,其實作了接口Lock,裡面的功能函數主要有:
1. lock() -- 阻塞模式擷取資源
2. lockInterruptibly() -- 可中斷模式擷取資源
3. tryLock() -- 嘗試擷取資源
4. tryLock(time) -- 在一段時間内嘗試擷取資源
5. unlock() -- 釋放資源
ReentrantLock實作Lock有兩種模式即公平模式和不公平模式
Concurrent包下的同步器都是基于AQS架構,在ReentrantLock裡面會看到這樣三個類
-----------------------------------------------------------------------
static abstract class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) { ... }
protected final boolean tryRelease(int releases) { ... }
}
-----------------------------------------------------------------------
final static class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) { ... }
final void lock() { ... }
}
-----------------------------------------------------------------------
final static class FairSync extends Sync {
final void lock() { ... }
protected final boolean tryAcquire(int acquires) { ... }
}
-----------------------------------------------------------------------
再回歸到ReentrantLock對Lock的實作上
0. ReentrantLock執行個體化
ReentrantLock有個屬性sync,實際上對Lock接口的實作都是包裝了一下這個sync的實作
如果是公平模式則建立一個FairSync對象,否則建立一個NonfairSync對象,預設是不公平模式
1. lock() 調用sync.lock()
公平模式下:直接走AQS的acquire函數,此函數的邏輯走一次tryAcquire,如果成功
線程拜托同步器的控制,否則加入NODE連結清單,進入acquireQueued的tryAcquire,休眠,被喚醒的輪回
不公平模式下和公平模式下邏輯大體上是一樣的,不同點有兩個:
a. 在執行tryAcquire之前的操作,不公平模式會直接compareAndSetState(0, 1)原子性的設定AQS的資源
0表示目前沒有線程占據資源,則直接搶占資源,不管AQS的NODE連結清單的FIFO原則
b. tryAcquire的原理不一樣,不公平模式的tryAcquire隻看compareAndSetState(0, 1)能否成功
而公平模式還會加一個條件就是此線程對于的NODE是不是NODE連結清單的第一個
c. 由于tryAcquire的實作不一樣,而公平模式和不公平模式在lock期間走的邏輯是一樣的(AQS的acquireQueued的邏輯)
d. 對于一個線程在擷取到資源後再調用lock會導緻AQS的資源做累加操作,同理線程要徹底的釋放資源就必須同樣
次數的調用unlock來做對應的累減操作,因為對應ReentrantLock來說tryAcquire成功一個必須的條件就是compareAndSetState(0, 1)
e. 由于acquireQueued過程中屏蔽了線程中斷,隻是線上程拜托同步器控制後,如果記錄線程在此期間被中斷過則标記線程的
中斷狀态
2. lockInterruptibly() 調用sync.acquireInterruptibly(1),上一篇文章講過AQS的核心函數,這個過程和acquireQueued
是一樣的,隻不過在阻塞期間如果被标記中斷則線程在park期間被喚醒,然後直接退出那個輪回,抛出中斷異常
由于公平模式和不公平模式下對tryAcquire的實作不一樣導緻lockInterruptibly邏輯也是不一樣
3. tryLock() 函數隻是嘗試性的去擷取一下鎖,跟tryAcquire一樣,這兩種模式下走的代碼一樣都是公平模式下的代碼
4. tryLock(time) 調用sync.tryAcquireNanos(time),上一篇文章講過AQS的核心函數,這個過程和acquireQueued一樣,
a. 在阻塞前會先計算阻塞的時間,進入休眠
b. 如果被中斷則會判斷時間是否到了
1. 如果沒到則且被其他線程設定了中斷标志,退出那個輪回,抛出中斷異常,如果沒有被設定中斷标記則是前一個線程
釋放了資源再喚醒了它,其繼續走那個輪回,輪回中,如果tryAcquire成功則擺脫了同步器的控制,否則回到a
2. 如果時間到了則退出輪回,擷取資源失敗
5. unlock() 調用sync.release(1),上一篇文章講過AQS的核心函數,release函數會調用Sync實作的tryRelease函數來判斷
釋放資源是否成功,即Sync.tryRelease函數,其邏輯過程是
a. 首先判斷目前占據資源的線程是不是調用者,如果不是會抛出異常IllegalMonitorStateException
b. 如果是則進行AQS資源的減1邏輯,如果再減1後AQS資源變成0則表示調用線程測得放棄了此鎖,傳回給release的值的TRUE,
release會喚醒下一個線程
-----------------------------------------------------------------------
整體來看ReentrantLock互斥鎖的實作大緻是
1. 自己實作AQS的tryAcquire和tryRelease邏輯,tryAcquire表示嘗試去擷取鎖,tryRelease表示嘗試去釋放鎖
2. ReentrantLock對lock(),trylock(),trylock(time),unlock()的實作都是使用AQS的架構,然後AQS的架構又傳回調用
ReentrantLock實作的tryAcquire和tryRelease來對線程是否擷取鎖和釋放鎖成功做出依據判斷
---------以上摘抄自重入鎖的使用 http://blog.csdn.net/eclipser1987/article/details/7301828
第二種情況: 線程間通信Condition
Condition可以替代傳統的線程間通信,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。
——為什麼方法名不直接叫wait()/notify()/nofityAll()?因為Object的這幾個方法是final的,不可重寫!
傳統線程的通信方式,Condition都可以實作。
注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。
Condition的強大之處在于它可以為多個線程間建立不同的Condition
看JDK文檔中的一個例子:
假定有一個綁定的緩沖區,它支援 put 和 take 方法。如果試圖在空的緩沖區上執行 take 操作,則在某一個項變得可用之前,線程将一直阻塞;如果試圖在滿的緩沖區上執行 put 操作,則在有空間變得可用之前,線程将一直阻塞。
我們喜歡在單獨的等待 set 中儲存put 線程和take 線程,這樣就可以在緩沖區中的項或空間變得可用時利用最佳規劃,一次隻通知一個線程。
可以使用兩個Condition 執行個體來做到這一點。
——其實就是java.util.concurrent.ArrayBlockingQueue的功能
- 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; //寫索引
- int takeptr; //讀索引
- int 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();//解除鎖定
- }
- }
優點:
假設緩存隊列中已經存滿,那麼阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。
那麼假設隻有一個Condition會有什麼效果呢?緩存隊列中已經存滿,這個Lock不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那麼線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。
----- 以上摘抄自線程間通信 http://blog.csdn.net/vking_wang/article/details/9952063