ReentrantLock学习
對于并發工作,你需要某種方式來防止兩個任務訪問相同的資源,至少在關鍵階段不能出現這種沖突情況。防止這種沖突的方法就是當資源被一個任務使用時,在其上加鎖。在前面的文章--synchronized學習中,我們學習了Java中內建的同步機制synchronized的基本用法,在本文中,我們來學習Java中另一種鎖ReentrantLock。
?ReentrantLock介紹
ReentrantLock,通常譯為再入鎖,是Java 5中新加入的鎖實現,它與synchronized基本語義相同。再入鎖通過代碼直接調用lock()方法獲取,代碼書寫更加靈活。與此同時,ReentrantLock提供了很多實用的方法,能夠實現很多synchronized無法做到的細節控制,比如可以控制fairness,也就是公平性,或者利用定義條件等。但是,編碼中也需要注意,必須要明確調用unlock()方法釋放鎖,不然就會一直持有該鎖。
?
我們先看一個簡單例子體驗一下:
public class ReLockTest {private Lock lock = new ReentrantLock();public int shareState;public void add() {shareState ++ ;}public void printState() {System.out.println("shareState-->" + shareState); }public static void main(String[] args) throws Exception{ReLockTest reLockTest = new ReLockTest();Thread t1 = new Thread(()->{for(int i = 0; i<10000 ; i++)reLockTest.add();});Thread t2 = new Thread(()->{for(int i = 0; i<10000 ; i++)reLockTest.add();});t1.start();t2.start();t1.join();t2.join();Thread.sleep(2000);reLockTest.printState();} }/** 輸出結果
* shareState-->14012
*/
我們看到輸出結果小于20000,這就是線程不安全,我們加上同步之后再看結果:
?
public class ReLockTest {private Lock lock = new ReentrantLock();public int shareState;public void add() {lock.lock();try {shareState ++ ;}catch(Exception e) {e.printStackTrace();}finally {lock.unlock();}}public void printState() {System.out.println("shareState-->" + shareState); }public static void main(String[] args) throws Exception{ReLockTest reLockTest = new ReLockTest();Thread t1 = new Thread(()->{for(int i = 0; i<10000 ; i++)reLockTest.add();});Thread t2 = new Thread(()->{for(int i = 0; i<10000 ; i++)reLockTest.add();});t1.start();t2.start();t1.join();t2.join();Thread.sleep(2000);reLockTest.printState();} }/** 輸出結果
* shareState-->20000
*/
我們看到在將自增操作上鎖之后,輸出結果就達到預期了。這就是ReentrantLock的基本用法,為了保證鎖釋放,每個lock()動作都對應一個try-catch-finally,這可以說是一個慣用法:
ReentrantLock lock = new ReentrantLock(); lock.lock(); try {// do something } finally {lock.unlock(); }ReentrantLock用法
ReentrantLock相比synchronized,雖然所需的代碼比synchronized關鍵字要多,但也是因為可以像普通對象一樣使用,所以可以利用其提供的各種便利方法,進行精細的同步操作,甚至是synchronized難以表達的用例,如:
帶超時的獲取鎖嘗試
通過tryLock(long timeout, TimeUnit unit)方法實現,這是一個具有超時參數的嘗試申請鎖的方法,阻塞時間不會超過給定的值;如果成功則返回true。
可以指定公平性
在創建ReentrantLock對象時往構造器中傳入true即指定創建公平鎖,這里所謂的公平是指在競爭場景中,當公平性為真時,會傾向于將鎖賦予等待時間最久的線程。公平性是減少線程“饑餓”(個別線程長期等待鎖,但始終無法獲取)情況的發生的一個辦法。
如果使用synchronized,我們根本無法進行公平性的選擇,其永遠是不公平的,這也是主流操作系統線程調度的選擇。通用場景中,公平性未必有想象中的那么重要,Java默認的調度策略很少會導致“饑餓”發生。與此同時,若要保證公平性則會引入額外開銷,自然會導致一定的吞吐量下將。所以,只有當程序確實有公平性需要的時候,才有必要指定它。
可以響應中斷請求
通過lockInterruptibly()獲得鎖,但是會不確定地發生阻塞。如果線程被中斷,拋出一個InterruptedException異常。
可以創建條件變量,將復雜晦澀的同步操作轉變為直觀可控的對象行為
如果說ReentrantLock是synchronized的替代選擇,Conition則是將wait、notify、notifyAll等操作轉化為相應的對象,將復雜而晦澀的同步操作轉變為直觀可控的對象行為。
可以通過Condition上調用await()來掛起一個任務,當外部條件發生變化,你可以通過調用signal()來通知這個任務,從而喚醒一個任務,或者調用signAll()來喚醒所有在這個Condition上被其自身掛起的任務,這是Condition最常見的用法。一個典型的應用場景就是標準類庫中的ArrayBlockingQueue,下面我們結合部分其源碼來分析一下:
首先,在構造函數中初始化lock,在從lock中創建兩個條件變量Condition分別是notEmpty和notFull。
/** Main lock guarding all access */ final ReentrantLock lock;/** 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(); }然后在take方法中,判斷和等待條件滿足:
public E take() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lockInterruptibly();try {while (count == 0)notEmpty.await();return dequeue();} finally {lock.unlock();} }在take方法中,如果隊列為空,ArrayBlockingQueue的本義是獲取對象的線程會阻塞,等待入隊的發生,而不是直接返回,代碼中是通過調用Condition的await方法來實現的,而當有元素入隊之后又是如何觸發被阻塞的take操作的呢?我們看看enqueue:
?
private void enqueue(E x) {// assert lock.getHoldCount() == 1;// assert items[putIndex] == null;final Object[] items = this.items;items[putIndex] = x;if (++putIndex == items.length)putIndex = 0;count++;notEmpty.signal(); // 通知等待的線程 }最后一行代碼中就是通知等待的線程,這行執行結束后,被take阻塞的線程就會繼續執行了。
通過signal/await的組合,完成了條件判斷和通知等待線程,非常順暢就完成了狀態流轉。signal和await成對調用非常重要,不然假設只有await動作,線程會一直等待直到被打斷(interrupt)。
ReentrantLock與Synchronized
使用synchronized關鍵字時,需要寫的代碼量更少,而ReentrantLock的使用往往和try-catch-finally一起配套使用,代碼量增加了。
從性能角度,synchronized早期的實現比較低效,對比ReentrantLock,大多數場景性能都相差較大。但是在Java 6中對其進行了非常多的改進(可以參考synchronized底層實現學習),在高競爭情況下,ReentrantLock仍然有一定優勢。
我們知道synchronized獲取的鎖是monitor對象,而ReentrantLock獲取的鎖是什么呢,是否也是同一把鎖呢?下文中,我們會深入源碼去探究ReentrantLock獲取鎖的實現細節。
轉載于:https://www.cnblogs.com/volcano-liu/p/10262183.html
總結
以上是生活随笔為你收集整理的ReentrantLock学习的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flink编程入门(二)
- 下一篇: size - 列出段节大小和总共大小