Java 多线程 —— ReentrantLock 与 Condition
引言
ReentrantLock 是 JUC 下的一個功能強勁的鎖工具,支持公平鎖、非公平鎖,以及多等待隊列的 Condition 。
也常常被稱為“手動鎖”。本篇博客主要分析它的使用方法以及 Condition 實現的一個生產者消費者模式。
一、可替代 synchronized 的手動鎖
?ReentrantLock是Lock接口的一個實現,可以用于替代synchronized。
使用ReentrantLock可以完成類似synchronized(this)的功能,需要注意的是,就算線程已經執行完畢,Lock也不會自動釋放鎖,必須要手動釋放鎖。
與synchronized不同的是:使用synchronized鎖如果遇到異常,JVM會自動釋放鎖,但是Lock必須手動釋放鎖,因此經常在finally中進行鎖的釋放。
1.1 模擬 synchronized
public class ReentrantLockDemo {Lock lock = new ReentrantLock();void m1() {try {lock.lock();for (int i = 0; i < 10; i++) {TimeUnit.SECONDS.sleep(1);System.out.print(i + " ");}} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();// 比較一下未釋放鎖前后的執行區別}}void m2() {lock.lock();System.out.println("m2...");lock.unlock();}public static void main(String[] args) {ReentrantLockDemo r1 = new ReentrantLockDemo();new Thread(r1::m1).start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}new Thread(r1::m2).start();} }執行結果:
1.2 tryLock嘗試鎖定
將m2()方法修改成如下形式,再次執行,觀察結果:
/*** 使用tryLock進行嘗試鎖定,不管鎖定與否,方法都將繼續執行, 可以根據tryLock的返回值來判斷是否鎖定,* 也可以指定tryLock的時間,由于tryLock(time)拋出異常,所以注意unLock的處理,必須放在finally中。 <br>* 作者: mht<br>* 時間:2018年9月15日-下午10:07:42<br>*/void m2() { // boolean locked = lock.tryLock(); // System.out.println("m2..." + locked); // if (locked) lock.unlock();boolean locked = false;try {locked = lock.tryLock(3, TimeUnit.SECONDS);System.out.print("m2 resume... ");} catch (InterruptedException e) {e.printStackTrace();} finally {if (locked)lock.unlock();}}執行結果(修改m2后繼續執行第一段代碼中的main方法):
Lock 的tryLock()方法支持有參和無參,根據實際需要可以指定具體的等待時間,或不進行等待(如注釋掉的代碼)。
1.3 允許被打斷
在多線程共同請求同一個Lock時,有時會希望某個線程能夠被打斷,從而終止等待的狀態。
Lock.lockInterruptibly()方法同樣可以請求一個可用的鎖對象,如果鎖對象不可用,則進入阻塞狀態。但是與Lock.lock()不同的是,當程序中調用Thread.interrupt()方法打斷線程時,前者會做出響應,而后者并不會。當然,即便是獲得了鎖對象,線程依然可以被打斷,但是要記住在finally塊中釋放被打斷線程中持有的鎖對象。
public class InterruptWaitingDemo {public static void main(String[] args) {Lock lock = new ReentrantLock();Thread t1 = new Thread(() -> {try {lock.lock();System.out.println("t1 start");TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);System.out.println("t1 end");} catch (InterruptedException e) {System.out.println("interrupted!");} finally {lock.unlock();}}, "t1");t1.start();Thread t2 = new Thread(() -> {try {lock.lockInterruptibly();System.out.println("t2 start");TimeUnit.SECONDS.sleep(5);System.out.println("t2 end");} catch (InterruptedException e) {System.out.println("Interrupted!");} finally {System.out.println("finally:lock = " + lock.tryLock());lock.unlock();}}, "t2");t2.start();try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}t2.interrupt();// 打斷線程 t2,不論線程是否獲得鎖。} }上述代碼中,線程t1持有lock對象,并一直鎖定,t2無法得到這把鎖,如果在線程t2中以原來的lock()方法請求鎖的話,在線程外部無法打斷該線程,但是使用lockInterruptibly()允許線程在外部被打斷。
執行結果:
線程被打斷后捕捉到 InterruptedException 異常,而上圖是以輸出“Interrupted!”字符串的形式提現,圖中的異常并不是被打斷的異常,而是未獲取鎖的情況下執行lock.unlock()拋出的異常。
1.4 實現公平鎖
公平鎖指的是,哪個線程等待時間長就優先獲得鎖。synchronized屬于非公平鎖,這意味著鎖被釋放后其他線程會隨機獲得synchronized鎖。
ReentrantLock可以實現公平鎖的需求,例如下面代碼所示:
public class FairLockDemo extends Thread {/** 參數為true為公平鎖 */private static ReentrantLock lock = new ReentrantLock(true);@Overridepublic void run() {for (int i = 0; i < 10; i++) {try {lock.lock();System.out.println(Thread.currentThread().getName() + "獲得鎖");} finally {lock.unlock();}}}public static void main(String[] args) {FairLockDemo demo = new FairLockDemo();Thread th1 = new Thread(demo);Thread th2 = new Thread(demo);th1.start();th2.start();} }執行結果:
二、Condition 條件隊列
Lock.newCondition 可以得到一個條件隊列對象 —— Condition。
Condition 意為“條件”,它是為滿足不同前提條件才能執行操作的線程提供的條件隊列。
事實上,synchronized 內置鎖也有一個等待隊列,但內置的條件隊列存在著一些缺陷:每個內置鎖只能有一個條件隊列,對于一些不同的同步方法,如 put、take,它們雖然都需要鎖,但對于一些有界緩存(如ArrayBlockingQueue 等),它們執行操作的前提條件一定是不同的。
與內置條件隊列不同的是,對于每個Lock,可以有任意數量的 Condition 對象,同時,Condition 對象繼承了相關 Lock 對象的公平性,對于公平的鎖,線程會依照 FIFO 的順序從 Condition.await 中釋放。
在 Condition 對象中,與 wait、notify 和 notifyAll 方法對應的分別是 await、signal 和 signalAll。
《Java 并發編程實戰》
與內置鎖和條件隊列一樣,當使用顯式的 Lock 和 Condition 時,也必須滿足鎖、條件謂詞和條件變量之間的三元關系。在條件謂詞中包含的變量必須由 Lock 來保護,并且在檢查條件謂詞以及調用 await 和 signal 時,必須持有 Lock 對象。
signal 比 signalAll 更高效,它能極大地減少在每次緩存操作中發生的上下文切換與鎖請求的次數。
以下程序實現了一個生產者消費者的情景,兩個生產者線程,和10個消費者線程:
public class T_Condition<T> {final private LinkedList<T> list = new LinkedList<>();final private int MAX;private int size = 0;public T_Condition(int max) {this.MAX = max;}private Lock lock = new ReentrantLock();private Condition producer = lock.newCondition();private Condition consumer = lock.newCondition();public void put(T t) {try {lock.lock();while (list.size() == MAX) {producer.await();}list.addFirst(t);size++;System.out.println(Thread.currentThread().getName() + " put 了1個元素,當前容量:" + size + "/" + MAX);// 通知消費者線程進行消費consumer.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}public T get() {T t = null;try {lock.lock();while (list.size() == 0) {consumer.await();}t = list.pollLast();size--;System.out.println(Thread.currentThread().getName() + " get 了1個元素,當前容量:" + size + "/" + MAX);producer.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}return t;}public static void main(String[] args) {T_Condition<String> container = new T_Condition<String>(10);for (int i = 0; i < 2; i++) {new Thread(() -> {while (true) {container.put(new String());}}, "producer" + i).start();}for (int i = 0; i < 10; i++) {new Thread(() -> {while (true) {container.get();}}, "consumer" + i).start();}} }總結
1、ReentrantLock是Lock接口經常用到的實現類,它可以代替synchronized實現相同的功能,但是需要手動調用 lock.unlock()釋放鎖。這也是與synchronized的區別最大的區別。
2、手動鎖在使用上更加靈活,操作性更強一些。可以嘗試使用 tryLock() 和 lockInterruptibly()?等操作自定義鎖的操作。
3、ReentrantLock可以實現公平鎖,但是在性能上會有些折扣。
總結
以上是生活随笔為你收集整理的Java 多线程 —— ReentrantLock 与 Condition的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java8————Lambda表达式(二
- 下一篇: Java 多线程 —— AQS 原理