淺談Java並發中ReentrantLock鎖應該怎麼用
重入鎖可以替代關鍵字 synchronized 。
在 JDK5.0 的早期版本中,重入鎖的性能遠遠優於關鍵字 synchronized ,
但從 JDK6.0 開始, JDK 在關鍵字 synchronized 上做瞭大量的優化,使得兩者的性能差距並不大。
重入鎖使用 ReentrantLock 實現
1、重入鎖
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockDemo implements Runnable { public static ReentrantLock lock = new ReentrantLock(); public static int i = 0; @Override public void run() { for (int j = 0; j < 10000000; j++) { lock.lock(); lock.lock(); try { i++; } finally { lock.unlock(); lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { ReentrantLockDemo tl = new ReentrantLockDemo(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
控制臺打印
20000000
說明
一個線程連續兩次獲得同一把鎖是允許的。
如果不允許這麼操作,那麼同一個線程在第 2 次獲得鎖時,將會和自己產生死鎖。
程序就會“卡死”在第 2 次申請鎖的過程中。
但需要註意的是,如果同一個線程多次獲得鎖,那麼在釋放鎖的時候,也必須釋放相同次數。
如果釋放鎖的次數多瞭,那麼會得到一個 java.lang.IllegalMonitorStateException 異常,反之,如果釋放鎖的次數少瞭,那麼相當於線程還持有這個鎖,因此,其他線程也無法進入臨界區。
2、中斷響應
對於關鍵字 synchronized 來說,如果一個線程在等待鎖,那麼結果隻有兩種情況,要麼它獲得這把鎖繼續執行,要麼它就保持等待。
而使用重入鎖,則提供另外一種可能,那就是線程可以被中斷。
也就是在等待鎖的過程中,程序可以根據需要取消對鎖的請求。
有些時候,這麼做是非常有必要的。
比如,你和朋友約好一起去打球,如果你等瞭半個小時朋友還沒有到,你突然接到一個電話,說由於突發情況,朋友不能如約前來瞭,那麼你一定掃興地打道回府瞭。
中斷正是提供瞭一套類似的機制。
如果一個線程正在等待鎖,那麼它依然可以收到一個通知,被告知無須等待,可以停止工作瞭。
這種情況對於處理死鎖是有一定幫助的。
下面的代碼產生瞭一個死鎖,但得益於鎖中斷,我們可以很輕易地解決這個死鎖。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class IntLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加鎖順序,方便構造死鎖 * * @param lock */ public IntLock(int lock) { this.lock = lock; } @Override public void run() { try { if (lock == 1) { lock1.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { } lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { } lock1.lockInterruptibly(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) lock1.unlock(); if (lock2.isHeldByCurrentThread()) lock2.unlock(); System.out.println(Thread.currentThread().getId() + ":線程退出"); } } public static void main(String[] args) throws InterruptedException { IntLock r1 = new IntLock(1); IntLock r2 = new IntLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); Thread.sleep(1000); //中斷其中一個線程 t2.interrupt(); } }
控制臺輸出
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.shockang.study.java.concurrent.lock.IntLock.run(IntLock.java:35)
at java.lang.Thread.run(Thread.java:748)
11:線程退出
12:線程退出
說明
線程 t1 和 t2 啟動後, t1 先占用 lock1 ,再占用 lock2。
t2 先占用 lock2 ,再請求 lock1。
因此,很容易形成 t1 和 t2 之間的相互等待。
在這裡,對鎖的請求,統一使用 lockInterruptibly() 方法。
這是一個可以對中斷進行響應的鎖申請動作,即在等待鎖的過程中,可以響應中斷。
在代碼第 56 行,主線程 main 處於休眠狀態,此時,這兩個線程處於死鎖的狀態。
在代碼第 58 行,由於 t2 線程被中斷,故 t2 會放棄對 lock1 的申請,同時釋放已獲得的 lock2 。
這個操作導致 t1 線程可以順利得到 lock2 而繼續執行下去。
3、鎖申請等待限時
除瞭等待外部通知之外,要避免死鎖還有另外一種方法,那就是限時等待。
依然以約朋友打球為例,如果朋友退退不來,又無法聯系到他,那麼在等待 1 到 2 個小時後,我想大部分人都會掃興離去。
對線程來說也是這樣。
通常,我們無法判斷為什麼一個線程退遲拿不到鎖。
也許是因為死鎖瞭,也許是因為產生瞭饑餓。
如果給定一個等待時間,讓線程自動放棄,那麼對系統來說是有意義的。
我們可以使用 tryLock() 方法進行一次限時的等待。
tryLock(long, TimeUnit)
下面這段代碼展示瞭限時等待鎖的使用。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class TimeLock implements Runnable { public static ReentrantLock lock = new ReentrantLock(); @Override public void run() { try { if (lock.tryLock(5, TimeUnit.SECONDS)) { Thread.sleep(6000); } else { System.out.println("get lock failed"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) { TimeLock tl = new TimeLock(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); } }
控制臺打印
get lock failed
Exception in thread “Thread-1” java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
at com.shockang.study.java.concurrent.lock.TimeLock.run(TimeLock.java:20)
at java.lang.Thread.run(Thread.java:748)
說明
在這裡, tryLock() 方法接收兩個參數,一個表示等待時長,另外一個表示計時單位。
這裡的單位設置為秒,時長為 5 ,表示線程在這個鎖請求中最多等待 5 秒。
如果超過 5 秒還沒有得到鎖,就會返回 false 。
如果成功獲得鎖,則返回 true 。
在本例中,由於占用鎖的線程會持有鎖長達 6 秒,故另一個線程無法在 5 秒的等待時間內獲得鎖,因此請求鎖會失敗。
tryLock()
ReentrantLock.tryLock() 方法也可以不帶參數直接運行。
在這種情況下,當前線程會嘗試獲得鎖,如果鎖並未被其他線程占用,則申請鎖會成功,並立即返回 true 。
如果鎖被其他線程占用,則當前線程不會進行等待,而是立即返回 false 。
這種模式不會引起線程等待,因此也不會產生死鎖。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class TryLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public TryLock(int lock) { this.lock = lock; } @Override public void run() { if (lock == 1) { while (true) { if (lock1.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock2.tryLock()) { try { System.out.println(Thread.currentThread() .getId() + ":My Job done"); return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } } else { while (true) { if (lock2.tryLock()) { try { try { Thread.sleep(500); } catch (InterruptedException e) { } if (lock1.tryLock()) { try { System.out.println(Thread.currentThread() .getId() + ":My Job done"); return; } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } } } public static void main(String[] args) throws InterruptedException { TryLock r1 = new TryLock(1); TryLock r2 = new TryLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } }
控制臺輸出
11:My Job done
12:My Job done
說明
上述代碼采用瞭非常容易死鎖的加鎖順序。
也就是先讓 t1 獲得 lock1 ,再讓 2 獲得 lock2 ,接著做反向請求,讓 t1 申請 lock2 , t2 申請 lock1 。
在一般情況下,這會導致 t1 和 2 相互等待。
待,從而引起死鎖。
但是使用 tryLock() 方法後,這種情況就大大改善瞭。
由於線程不會傻傻地等待,而是不停地嘗試,因此,隻要執行足夠長的時間,線程總是會得到所有需要的資源,從而正常執行(這裡以線程同時獲得 lock1 和 lock2 兩把鎖,作為其可以正常執行的條件)。
在同時獲得 lock1 和 lock2 後,線程就打印出標志著任務完成的信息“ My Job done”。
4、公平鎖
在大多數情況下,鎖的申請都是非公平的。
也就是說,線程 1 首先請求瞭鎖 A ,接著線程 2 也請求瞭鎖 A 。
那麼當鎖 A 可用時,是線程 1 可以獲得鎖還是線程 2 可以獲得鎖呢?
這是不一定的,系統隻是會從這個鎖的等待隊列中隨機挑選一個。
因此不能保證其公平性。
這就好比買票不排隊,大傢都圍在售票窗口前,售票員忙得焦頭爛額,也顧不及誰先誰後,隨便找個人出票就完事瞭。
而公平的鎖,則不是這樣,它會按照時間的先後順序,保證先到者先得,後到者後得。
公平鎖的一大特點是:它不會產生饑餓現象。
關於線程饑餓請參考我的博客——死鎖、活鎖和饑餓是什麼意思?
隻要你排隊,最終還是可以等到資源的。
如果我們使用 synchronized 關鍵字進行鎖控制,那麼產生的鎖就是非公平的。
而重入鎖允許我們對其公平性進行設置。
它的構造函數如下:
/** * 使用給定的公平策略創建一個 ReentrantLock 的實例。 * * @param fair 如果此鎖應使用公平排序策略為 true */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
當參數 fair 為 true 時,表示鎖是公平的。
公平鎖看起來很優美,但是要實現公平鎖必然要求系統維護一個有序隊列,因此公平鎖的實現成本比較高,性能卻非常低下,因此,在默認情況下,鎖是非公平的。
如果沒有特別的需求,則不需要使用公平鎖。
公平鎖和非公平鎖在線程調度表現上也是非常不一樣的。
下面的代碼可以很好地突出公平鎖的特點。
package com.shockang.study.java.concurrent.lock; import java.util.concurrent.locks.ReentrantLock; public class FairLock implements Runnable { public static ReentrantLock fairLock = new ReentrantLock(true); @Override public void run() { while (true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getName() + " 獲得鎖"); } finally { fairLock.unlock(); } } } public static void main(String[] args) throws InterruptedException { FairLock r1 = new FairLock(); Thread t1 = new Thread(r1, "Thread_t1"); Thread t2 = new Thread(r1, "Thread_t2"); t1.start(); t2.start(); } }
控制臺輸出
獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t2 獲得鎖
Thread_t1 獲得鎖
Thread_t1 獲得鎖
# 省略
說明
由於代碼會產生大量輸出,這裡隻截取部分進行說明。
在這個輸出中,很明顯可以看到,兩個線程基本上是交替獲得鎖的,幾乎不會發生同一個線程連續多次獲得鎖的可能,從而保證瞭公平性。
如果設置瞭 false,則會根據系統的調度,一個線程會傾向於再次獲取已經持有的鎖,這種分配方式是高效的,但是無公平性可言。
源碼(JDK8)
/**
* 一種可重入互斥鎖,其基本行為和語義與使用同步方法和語句訪問的隱式監視鎖(即 synchronized)相同,但具有擴展功能。
*
* 可重入鎖屬於上次成功鎖定但尚未解鎖它的線程。
*
* 當鎖不屬於另一個線程時,調用鎖的線程將返回,並成功獲取鎖。
*
* 如果當前線程已經擁有鎖,則該方法將立即返回。這可以使用 isHeldByCurrentThread 和 getHoldCount 方法進行檢查。
*
* 此類的構造函數接受可選的公平性參數。
*
* 當設置為 true 時,在競爭狀態下,鎖有利於向等待時間最長的線程授予訪問權限。否則,此鎖不保證任何特定的訪問順序。
*
* 使用由多線程訪問的公平鎖的程序可能顯示較低的總吞吐量
*
* (即,較慢;通常比使用默認設置的要慢得多,但是在獲得鎖和保證不饑餓的時間上有較小的差異。
*
* 但是請註意,鎖的公平性並不能保證線程調度的公平性。
*
* 因此,使用公平鎖的多個線程中的一個線程可以連續多次獲得公平鎖,而其他活動線程則沒有進行並且當前沒有持有該鎖。
*
* 還要註意,untimed tryLock() 方法不支持公平性設置。
*
* 如果鎖可用,即使其他線程正在等待,它也會成功。
*
* 建議的做法是總是在調用之後立即使用try塊鎖定,最典型的是在構建之前/之後,例如:
*
* class X {
* private final ReentrantLock lock = new ReentrantLock();
* // ...
*
* public void m() {
* lock.lock(); // block until condition holds
* try {
* // ... method body
* } finally {
* lock.unlock()
* }
* }
* }}
*
* 除瞭實現鎖接口之外,這個類還定義瞭許多公共和受保護的方法來檢查鎖的狀態。
*
* 其中一些方法隻對 instrumentation 和 monitoring 有用。
*
* 此類的序列化與內置鎖的行為相同:反序列化的鎖處於未鎖定狀態,而與序列化時的狀態無關。
*
* 此鎖最多支持同一線程的2147483647個遞歸鎖。嘗試超過此限制會導致鎖定方法拋出錯誤。
*
* @since 1.5
* @author Doug Lea
*/
public class ReentrantLock implements Lock, java.io.Serializable
到此這篇關於淺談Java並發中ReentrantLock鎖應該怎麼用的文章就介紹到這瞭,更多相關ReentrantLock鎖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java並發編程之ReentrantLock可重入鎖的實例代碼
- 一文帶你搞懂Java中Synchronized和Lock的原理與使用
- Java 輪詢鎖使用時遇到問題解決方案
- Java中ReentrantLock4種常見的坑
- ReentrantLock獲取鎖釋放鎖的流程示例分析