我是阿福,公衆號 JavaClub做者,一個在後端技術路上摸盤滾打的程序員,在進階的路上,共勉!
文章已收錄在 JavaSharing 中,包含Java技術文章,面試指南,資源分享。
掌握的技術點以下:java
什麼是等待/通知機制git
等待/通知機制在咱們生活中比比皆是,好比在就餐時就會出現,以下圖所示:
程序員
這個過程就出現了「等待/通知」機制。github
使用專業術語講:面試
等待/通知機制,是指線程A調用了對象O的wait()方法進入等待狀態,而線程B調用了對象O的notify()/notifyAll()方法,線程A收到通知後退出等待隊列,進入可運行狀態,進而執行後續操做。上述兩個線程經過對象O來完成交互,而對象上的wait()方法和notify()/notifyAll()方法的關係就如同開關信號同樣,用來完成等待方和通知方之間的交互工做。編程
等待/通知機制的實現後端
wait()方法的做用:多線程
是使當前線程進入阻塞狀態,同時在調用wait()方法以前線程必須得到該對象的對象級別鎖,即只能在同步方法或同步代碼塊中調用wait()方法。在執行wait()方法以後,當前線程釋放鎖。併發
notify() notifyAll()方法的做用:dom
就是用來通知那些等待該對象的對象鎖的其餘線程,若是有多個線程等待,則由線程規劃器隨機挑選其中一個呈wait()狀態的線程,對其發起通知notify,並使它獲取該對象的對象鎖。
須要說明的是:在執行notify()方法以後,當前線程不會立刻釋放該對象鎖,呈wait()狀態的線程也不能立刻獲取該對象鎖,要等到執行notify()方法的線程將程序執行完,也就是退出synchronized
代碼塊,當前線程纔會釋放鎖,而呈wait()狀態所在的線程才能夠獲取對象鎖。
強調notify(),notifyAll()也是在同步方法或者是同步代碼塊中調用,即在調用以前必須得到該對象的對象級別鎖。
用一句話總結一下wait和notify: wait使線程中止運行,而notify使中止的線程繼續運行。
下面代碼實現一個示例:
建立MyList.java,代碼以下:
public class MyList { private static List list=new ArrayList(); public static void add(){ list.add("anyString"); } public static int size(){ return list.size(); } }
自定義線程類 MyThread1.java
, MyThread2.java
,MyThread3.java
代碼以下:
public class MyThread1 extends Thread { private Object lock; public MyThread1(Object lock) { this.lock = lock; } @Override public void run() { try { synchronized (lock) { if (MyList.size() != 5) { System.out.println("開始 wait time=" + System.currentTimeMillis()); lock.wait(); System.out.println("結束 wait time=" + System.currentTimeMillis()); } } } catch (InterruptedException e) { e.printStackTrace(); } } } public class MyThread2 extends Thread { private Object lock; public MyThread2(Object lock) { this.lock = lock; } @Override public void run() { synchronized (lock) { for (int i = 0; i < 10; i++) { MyList.add(); if (MyList.size() == 5) { lock.notify(); System.out.println(" 已發出通知"); } System.out.println("添加了" + (i + 1) + " 個元素!!"); } } } }
建立測試類 Test.java
public class Test { public static void main(String[] args) { Object lock=new Object(); MyThread1 myThread1=new MyThread1(lock); myThread1.start(); MyThread2 myThread2=new MyThread2(lock); myThread2.start(); } }
程序代碼運行結果以下:
開始 wait time=1618832467129
添加了1 個元素!!
添加了2 個元素!!
添加了3 個元素!!
添加了4 個元素!!
已發出通知
添加了5 個元素!!
添加了6 個元素!!
添加了7 個元素!!
添加了8 個元素!!
添加了9 個元素!!
添加了10 個元素!!
結束 wait time=1618832467130
從運行的結果來看,這也說明notify()方法執行後不是當即釋放鎖。
線程生命週期轉換圖
線程的狀態
線程從建立,運行到結束老是處於五種狀態之一:新建狀態,就緒狀態,運行狀態,阻塞狀態,死亡狀態。
阻塞的狀況分三種:
(1)、等待阻塞:運行的線程執行wait()方法,該線程會釋放佔用的全部資源,JVM會把該線程放入「等待池中」。進入這個狀態後是不能自動喚醒的,必須依靠其餘線程調用notify()或者notifyAll()方法才能被喚醒。
(2)、同步阻塞:運行的線程在獲取對象的(synchronized)同步鎖時,若該同步鎖被其餘線程佔用,則JVM會吧該線程放入「鎖池」中。
(3)、其餘阻塞:經過調用線程的sleep()或者join()或發出了I/O請求時,線程會進入到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程從新回到就緒狀態。
阻塞線程方法的說明:
wait () , sleep()的區別:
一、 sleep()睡眠時,保持對象鎖,仍然佔有該鎖,而wait()釋放對象鎖.
二、 wait只能在同步方法和同步代碼塊裏面使用,而sleep能夠在任何地方使用。
三、 sleep必須捕獲異常,而wait不須要捕獲異常
生產者消費者問題(Producer-consumer problem),也稱有限緩衝問題(Bounded-buffer problem),是一個多線程同步問題的經典案例。生產者生成必定量的數據放到緩衝區中,而後重複此過程;與此同時,消費者也在緩衝區消耗這些數據。生產者和消費者之間必須保持同步,要保證生產者不會在緩衝區滿時放入數據,消費者也不會在緩衝區空時消耗數據。不夠完善的解決方法容易出現死鎖的狀況,此時進程都在等待喚醒。
解決生產者/消費者問題的方法可分爲兩類
(1)採用某種機制保護生產者和消費者之間的同步;
(2)在生產者和消費者之間創建一個管道。第一種方式有較高的效率,而且易於實現,代碼的可控制性較好,屬於經常使用的模式。第二種管道緩衝區不易控制,被傳輸數據對象不易於封裝等,實用性不強。所以本文只介紹同步機制實現的生產者/消費者問題。
同步問題核心在於
如何保證同一資源被多個線程併發訪問時的完整性。經常使用的同步方法是採用信號或加鎖機制,保證資源在任意時刻至多被一個線程訪問。Java語言在多線程編程上實現了徹底對象化,提供了對同步機制的良好支持。在Java中一共有四種方法支持同步,其中前三個是同步方法,一個是管道方法。
(1)wait() / notify()方法
(2)await() / signal()方法
(3)BlockingQueue阻塞隊列方法
(4)PipedInputStream / PipedOutputStream
下面咱們經過 wait() / notify()方法實現生產者和消費者模式:
代碼場景:
當緩衝區已滿時,生產者線程中止執行,放棄鎖,使本身處於等狀態,讓其餘線程執行;
當緩衝區已空時,消費者線程中止執行,放棄鎖,使本身處於等狀態,讓其餘線程執行。
當生產者向緩衝區放入一個產品時,向其餘等待的線程發出可執行的通知,同時放棄鎖,使本身處於等待狀態;
當消費者從緩衝區取出一個產品時,向其餘等待的線程發出可執行的通知,同時放棄鎖,使本身處於等待狀態。
代碼實現:
建立倉庫Storage.java
代碼:
public class Storage { // 倉庫容量 private final int MAX_SIZE = 10; // 倉庫存儲的載體 private LinkedList<Object> list = new LinkedList<>(); public void produce() { synchronized (list) { while (list.size() + 1 > MAX_SIZE) { System.out.println("【生產者" + Thread.currentThread().getName() + "】倉庫已滿"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(new Object()); System.out.println("【生產者" + Thread.currentThread().getName() + "】生產一個產品,現庫存" + list.size()); list.notifyAll(); } } public void consume() { synchronized (list) { while (list.size() == 0) { System.out.println("【消費者" + Thread.currentThread().getName() + "】倉庫爲空"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.remove(); System.out.println("【消費者" + Thread.currentThread().getName() + "】消費一個產品,現庫存" + list.size()); list.notifyAll(); } } }
建立生產者線程Producer.java
,消費者線程Consumer.java
,代碼以下:
public class Producer implements Runnable { private Storage storage; public Producer(){} public Producer(Storage storage){ this.storage = storage; } @Override public void run(){ while(true){ storage.produce(); } } }
public class Consumer implements Runnable{ private Storage storage; public Consumer(){} public Consumer(Storage storage){ this.storage = storage; } @Override public void run(){ while(true){ storage.consume(); } } }
建立測試類TestPc.java
public class TestPc { public static void main(String[] args) { Storage storage = new Storage(); Thread p1 = new Thread(new Producer(storage)); p1.setName("張三"); p1.start(); Thread c1=new Thread(new Consumer(storage)); c1.start(); c1.setName("李四"); } }
程序運行的部分結果:
【消費者李四】消費一個產品,現庫存8
【消費者李四】消費一個產品,現庫存7
【消費者李四】消費一個產品,現庫存6
【消費者李四】消費一個產品,現庫存5
【消費者李四】消費一個產品,現庫存4
【生產者張三】生產一個產品,現庫存5
【生產者張三】生產一個產品,現庫存6
【生產者張三】生產一個產品,現庫存7
【生產者張三】生產一個產品,現庫存8
【生產者張三】生產一個產品,現庫存9
【生產者張三】生產一個產品,現庫存10
【生產者張三】倉庫已滿
【消費者李四】消費一個產品,現庫存9
【消費者李四】消費一個產品,現庫存8
【消費者李四】消費一個產品,現庫存7
在不少狀況下,主線程建立並啓動子線程,若是子線程中進行大量的運算,主線程每每早於子線程結束。這時主線程要等待子線程完成以後再結束。好比子線程處理一個數據,主線程要取得這個數據中的值,就要用到join()方法。
join()方法就是等待線程對象銷燬。
建立測試MyJoinThread.java
代碼:
public class MyJoinThread extends Thread{ @Override public void run() { int secondValue= (int) (Math.random() * 10000); System.out.println(secondValue); try { Thread.sleep(secondValue); } catch (InterruptedException e) { e.printStackTrace(); } } }
建立測試類TestJoin.java
代碼:
public class TestJoin { public static void main(String[] args) throws InterruptedException { MyJoinThread myJoinThread=new MyJoinThread(); myJoinThread.start(); //myJoinThread.join(); System.out.println("我想當myJoinThread對象執行完畢我再執行,答案是不肯定的"); } }
代碼的運行結果:
我想當myJoinThread對象執行完畢我再執行,答案是不肯定的
9618
把myJoinThread.join()代碼註釋去掉運行代碼執行結果以下:
82
我想當myJoinThread對象執行完畢我再執行,答案是不肯定的
因此得出結論是:join()方法使所屬線程對象myJoinThread正常執行run()方法中的任務,而使當前線程main進行無限的阻塞,等待myJoinThread銷燬完再繼續執行main線程後面的代碼。
方法join()具備使線程排隊運行的做用,有點相似同步運行的效果。
join()和synchronized的區別是:join()在內部使用wait()方法進行等待,而synchronized關鍵字使用的是「對象監聽器」的原理作的同步。
方法join()與sleep(long)的區別
方法join(long)的功能在內部使用的是wait(long)方法實現的,所用join(long)方法具備釋放鎖的特色。
方法join(long)源代碼以下:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
從源代碼中能夠了解到,當執行wait(long)方法後,當前線程的鎖被釋放,那麼其餘線程能夠調用此線程中的同步方法了。
而Thread.sleep(long)方法不釋放鎖。
咱們知道變量值的共享可使用public static 變量的形式,若是想實現每個線程都有本身的共享變量該如何解決呢? JDK中提供ThreadLocal正是解決這樣的問題。
類ThreadLocal主要解決的就是爲每一個線程綁定本身的值,能夠將ThreadLocal類比喻成全局存放數據的盒子,盒子中能夠存儲每個線程的私有數據。
建立run.java
類,代碼以下:
public class run { private static ThreadLocal threadLocal=new ThreadLocal(); public static void main(String[] args) { if (threadLocal.get()==null){ System.out.println("從未放過值"); threadLocal.set("個人值"); } System.out.println(Thread.currentThread().getName()+"線程:"+threadLocal.get()); } }
代碼的運行結果:
從未放過值
main線程:個人值
從圖中運行結果來看,第一次調用threadLocal對象的get方法返回爲null,經過調用set()賦值後值打印在控制檯上,類ThreadLocal解決的是變量在不一樣線程間的隔離性,也就是不一樣的線程擁有本身的值,不一樣線程的值能夠存放在ThreadLocal類中進行保存的。
驗證線程變量的隔離性
建立ThreadLocalTest項目,類 Tools.java
代碼以下:
public class Tools { public static ThreadLocal local=new ThreadLocal(); }
建立線程類 MyThread1.java
,MyThread2.java
代碼以下:
public class MyThread1 extends Thread { @Override public void run() { for (int j = 0; j < 5; j++) { Tools.local.set(j+1); System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class MyThread2 extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { Tools.local.set(i+1); System.out.println(Thread.currentThread().getName()+"get value:"+Tools.local.get()); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }
建立run.java
測試類
public class run { public static void main(String[] args) { MyThread1 myThread1=new MyThread1(); myThread1.setName("myThread1線程"); myThread1.start(); MyThread2 myThread2=new MyThread2(); myThread2.setName("myThread2線程"); myThread2.start(); } }
程序運行結果:
myThread1線程get value:1
myThread2線程get value:1
myThread2線程get value:2
myThread1線程get value:2
myThread1線程get value:3
myThread2線程get value:3
myThread2線程get value:4
myThread1線程get value:4
myThread1線程get value:5
myThread2線程get value:5
雖然2個線程都向local中set()數據值,但每一個線程仍是能取到本身的數據。
文章參考:
《Java多線程編程核心技術》
https://blog.csdn.net/ldx1998...
https://blog.csdn.net/MONKEY_...
看到這裏今天的分享就結束了,若是以爲這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~
歡迎關注我的公衆號 「JavaClub」,按期爲你分享一些技術乾貨。