Java多線程高並發學習筆記(二)——深入理解ReentrantLock與Condition


鎖的概念

從jdk發行1.5版本之后,在原來synchronize的基礎上,增加了重入鎖ReentrantLock。

本文就不介紹synchronize了,有興趣的同學可以去了解一下,本文重點介紹ReentrantLock。

鎖是什么?

並發編程的時候,比如說有一個業務是讀寫操作,那多個線程執行這個業務就會造成已經寫入的數據又寫一遍,就會造成數據錯亂。

所以需要引入鎖,進行數據同步,強制使得該業務執行的時候只有一個線程在執行,從而保證不會插入多條重復數據。

一些共享資源也是需要加鎖,從而保證數據的一致性。

關於鎖的概念,也就不過多篇幅介紹,有很多概念性的東西,需要自己取找本書狠狠啃一啃,本文主要是給大家介紹如何使用鎖。

 

使用ReentrantLock同步

首先來看第一個實例:用兩個線程來在控制台有序打出1,2,3。

public class FirstReentrantLock { public static void main(String[] args) { Runnable runnable = new ReentrantLockThread(); new Thread(runnable, "a").start(); new Thread(runnable, "b").start(); } } class ReentrantLockThread implements Runnable { @Override public void run() { for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "輸出了: " + i); } } }

 執行FirstReentrantLock ,查看控制台輸出:

 

可以看到,並沒有順序,雜亂無章。

那使用ReentrantLock加入鎖,代碼如下:

package com.chapter2; import java.util.concurrent.locks.ReentrantLock; /** * @author tangj * * 如何使用ReentrantLock */ public class FirstReentrantLock { public static void main(String[] args) { Runnable runnable = new ReentrantLockThread(); new Thread(runnable, "a").start(); new Thread(runnable, "b").start(); } } class ReentrantLockThread implements Runnable { // 創建一個ReentrantLock對象 ReentrantLock lock = new ReentrantLock(); @Override public void run() { try { // 使用lock()方法加鎖  lock.lock(); for (int i = 0; i < 3; i++) { System.out.println(Thread.currentThread().getName() + "輸出了: " + i); } } finally { // 別忘了執行unlock()方法釋放鎖  lock.unlock(); } } }

執行FirstReentrantLock ,查看控制台輸出:

有順序的打印出了0,1,2,0,1,2.

這就是鎖的作用,它是互斥的,當一個線程持有鎖的時候,其他線程只能等待,待該線程執行結束,再通過競爭得到鎖。

 

使用Condition實現線程等待和喚醒

通常在開發並發程序的時候,會碰到需要停止正在執行業務A,來執行另一個業務B,當業務B執行完成后業務A繼續執行。ReentrantLock通過Condtion等待/喚醒這樣的機制.

相比較synchronize的wait()和notify()/notifAll()的機制而言,Condition具有更高的靈活性,這個很關鍵。Conditon可以實現多路通知和選擇性通知。

當使用notify()/notifAll()時,JVM時隨機通知線程的,具有很大的不可控性,所以建議使用Condition。

Condition使用起來也非常方便,只需要注冊到ReentrantLock下面即可。

參考下圖:

 

 接下來,使用Condition來實現等待/喚醒,並且能夠喚醒制定線程

先寫業務代碼:

package com.chapter2.howtocondition; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class MyService { // 實例化一個ReentrantLock對象 private ReentrantLock lock = new ReentrantLock(); // 為線程A注冊一個Condition public Condition conditionA = lock.newCondition(); // 為線程B注冊一個Condition public Condition conditionB = lock.newCondition(); public void awaitA() { try { lock.lock(); System.out.println(Thread.currentThread().getName() + "進入了awaitA方法"); long timeBefore = System.currentTimeMillis(); // 執行conditionA等待  conditionA.await(); long timeAfter = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+"被喚醒"); System.out.println(Thread.currentThread().getName() + "等待了: " + (timeAfter - timeBefore)/1000+"s"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void awaitB() { try { lock.lock(); System.out.println(Thread.currentThread().getName() + "進入了awaitB方法"); long timeBefore = System.currentTimeMillis(); // 執行conditionB等待  conditionB.await(); long timeAfter = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName()+"被喚醒"); System.out.println(Thread.currentThread().getName() + "等待了: " + (timeAfter - timeBefore)/1000+"s"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void signallA() { try { lock.lock(); System.out.println("啟動喚醒程序"); // 喚醒所有注冊conditionA的線程  conditionA.signalAll(); } finally { lock.unlock(); } } public void signallB() { try { lock.lock(); System.out.println("啟動喚醒程序"); // 喚醒所有注冊conditionA的線程  conditionB.signalAll(); } finally { lock.unlock(); } } }

分別實例化了兩個Condition對象,都是使用同一個lock注冊。注意conditionA對象的等待和喚醒只對使用了conditionA的線程有用,同理conditionB對象的等待和喚醒只對使用了conditionB的線程有用。

繼續寫兩個線程的代碼:

package com.chapter2.howtocondition; public class MyServiceThread1 implements Runnable { private MyService service; public MyServiceThread1(MyService service) { this.service = service; } @Override public void run() { service.awaitA(); } }

注意:MyServiceThread1 使用了awaitA()方法,持有的是conditionA!

package com.chapter2.howtocondition; public class MyServiceThread2 implements Runnable { private MyService service; public MyServiceThread2(MyService service) { this.service = service; } @Override public void run() { service.awaitB(); } }

注意:MyServiceThread2 使用了awaitB()方法,持有的是conditionB!

最后看啟動類:

package com.chapter2.howtocondition; public class ApplicationCondition { public static void main(String[] args) throws InterruptedException { MyService service = new MyService(); Runnable runnable1 = new MyServiceThread1(service); Runnable runnable2 = new MyServiceThread2(service); new Thread(runnable1, "a").start(); new Thread(runnable2, "b").start(); // 線程sleep2秒鍾 Thread.sleep(2000); // 喚醒所有持有conditionA的線程  service.signallA(); Thread.sleep(2000); // 喚醒所有持有conditionB的線程  service.signallB(); } }

執行ApplicationCondition ,來看控制台輸出結果:

a和b都進入各自的await()方法。首先執行的是

使用conditionA的線程被喚醒,而后再喚醒使用conditionB的線程。

 學會使用Condition,那來用它實現生產者消費者模式

生產者和消費者

首先來看業務類的實現:

 

package com.chapter2.consumeone; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Service { private Lock lock = new ReentrantLock(); private boolean flag = false; private Condition condition = lock.newCondition(); // 以此為衡量標志 private int number = 1; /** * 生產者生產 */ public void produce() { try { lock.lock(); while (flag == true) { condition.await(); } System.out.println(Thread.currentThread().getName() + "-----生產-----"); number++; System.out.println("number: " + number); System.out.println(); flag = true; // 提醒消費者消費  condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } /** * 消費者消費生產的物品 */ public void consume() { try { lock.lock(); while (flag == false) { condition.await(); } System.out.println(Thread.currentThread().getName() + "-----消費-----"); number--; System.out.println("number: " + number); System.out.println(); flag = false; // 提醒生產者生產  condition.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }

 

 

生產者線程代碼:

package com.chapter2.consumeone; /** * 生產者線程 * * @author tangj * */ public class MyThreadProduce implements Runnable { private Service service; public MyThreadProduce(Service service) { this.service = service; } @Override public void run() { for (;;) { service.produce(); } } }

消費者線程代碼:

package com.chapter2.consumeone; /** * 消費者線程 * * @author tangj * */ public class MyThreadConsume implements Runnable { private Service service; public MyThreadConsume(Service service) { super(); this.service = service; } @Override public void run() { for (;;) { service.consume(); } } }

啟動類:

package com.chapter2.consumeone; public class Application { public static void main(String[] args) { Service service = new Service(); Runnable produce = new MyThreadProduce(service); Runnable consume = new MyThreadConsume(service); new Thread(produce, "生產者 ").start(); new Thread(consume, "消費者 ").start(); } }

執行Application,看控制台的輸出:

 

因為采用了無限循環,生產者線程和消費者線程會一直處於工作狀態,可以看到,生產者線程執行完畢后,消費者線程就會執行,以這樣的交替順序,

而且的number也遵循者生產者生產+1,消費者消費-1的一個狀態。這個就是使用ReentrantLock和Condition來實現的生產者消費者模式。

 

順序執行線程

充分發掘Condition的靈活性,可以用它來實現順序執行線程。

來看業務類代碼:

package com.chapter2.sequencethread; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class Service { // 通過nextThread控制下一個執行的線程 private static int nextThread = 1; private ReentrantLock lock = new ReentrantLock(); // 有三個線程,所有注冊三個Condition Condition conditionA = lock.newCondition(); Condition conditionB = lock.newCondition(); Condition conditionC = lock.newCondition(); public void excuteA() { try { lock.lock(); while (nextThread != 1) { conditionA.await(); } System.out.println(Thread.currentThread().getName() + " 工作"); nextThread = 2; conditionB.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void excuteB() { try { lock.lock(); while (nextThread != 2) { conditionB.await(); } System.out.println(Thread.currentThread().getName() + " 工作"); nextThread = 3; conditionC.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void excuteC() { try { lock.lock(); while (nextThread != 3) { conditionC.await(); } System.out.println(Thread.currentThread().getName() + " 工作"); nextThread = 1; conditionA.signalAll(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }

這里可以看到,注冊了三個Condition,分別用於三個線程的等待和通知。

啟動類代碼:

package com.chapter2.sequencethread; /** * 線程按順序執行 * * @author tangj * */ public class Application { private static Runnable getThreadA(final Service service) { return new Runnable() { @Override public void run() { for (;;) { service.excuteA(); } } }; } private static Runnable getThreadB(final Service service) { return new Runnable() { @Override public void run() { for (;;) { service.excuteB(); } } }; } private static Runnable getThreadC(final Service service) { return new Runnable() { @Override public void run() { for (;;) { service.excuteC(); } } }; } public static void main(String[] args) { Service service = new Service(); Runnable A = getThreadA(service); Runnable B = getThreadB(service); Runnable C = getThreadC(service); new Thread(B, "B").start(); new Thread(A, "A").start(); new Thread(C, "C").start(); } }

 

運行啟動類,查看控制台輸出結果:

A,B,C三個線程一直按照順序執行。

 總結

學會使用鎖是學好多線程的基礎,ReentrantLock相比較關鍵字synchronize而言,更加而且可控,所以還是推薦大家使用ReentrantLock。

 

最后說兩句:

本文所以代碼都更新到我的github中,大家可以去clone或者Fork,我會持續更新的。

點擊這里進入我的Github
喜歡的朋友可以點擊下方的推薦,或者寫個評論我們共同探討Java高並發!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM