在多執行緒系統中,彼此之間的通訊協作非常重要,下面來聊聊執行緒間通訊的幾種方式。
wait/notify
想像一個場景,A、B兩個執行緒操作一個共享List物件,A對List進行add操作,B執行緒等待List的size=500時就列印記錄日誌,這要怎麼處理呢?
一個辦法就是,B執行緒while (true) { if(List.size == 500) {列印日誌} },這樣兩個執行緒之間就有了通訊,B執行緒不斷通過輪訓來檢測 List.size == 500 這個條件。
這樣可以實現我們的需求,但是也帶來了問題:CPU把資源浪費了B執行緒的輪詢操作上,因為while操作並不釋放CPU資源,導致了CPU會一直在這個執行緒中做判斷操作。
這要非常浪費CPU資源,所以就需要有一種機制來實現減少CPU的資源浪費,而且還可以實現在多個執行緒間通訊,它就是“wait/notify”機制。
定義兩個執行緒類:
public class MyThread1_1 extends Thread { private Object lock; public MyThread1_1(Object lock) { this.lock = lock; } public void run() { try { synchronized (lock) { System.out.println(Thread.currentThread().getName() + "開始------wait time = " + System.currentTimeMillis()); lock.wait(); System.out.println(Thread.currentThread().getName() + "開始------sleep time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "結束------sleep time = " + System.currentTimeMillis()); System.out.println(Thread.currentThread().getName() + "結束------wait time = " + System.currentTimeMillis()); } } catch (InterruptedException e) { e.printStackTrace(); } } }
public class MyThread1_2 extends Thread { private Object lock; public MyThread1_2(Object lock) { this.lock = lock; } public void run() { try { synchronized (lock) { System.out.println(Thread.currentThread().getName() + "開始------notify time = " + System.currentTimeMillis()); lock.notify(); System.out.println(Thread.currentThread().getName() + "開始------sleep time = " + System.currentTimeMillis()); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "結束------sleep time = " + System.currentTimeMillis()); System.out.println(Thread.currentThread().getName() + "結束------notify time = " + System.currentTimeMillis()); } } catch (Exception e) { e.printStackTrace(); } } }
測試方法,myThread1先執行,然後sleep 一秒後,myThread2再執行
@Test public void test1() throws InterruptedException { Object object = new Object(); MyThread1_1 myThread1_1 = new MyThread1_1(object); MyThread1_2 myThread1_2 = new MyThread1_2(object); myThread1_1.start(); Thread.sleep(1000); myThread1_2.start(); myThread1_1.join(); myThread1_2.join(); }
執行結果:
Thread-0開始------wait time = 1639464183921 Thread-1開始------notify time = 1639464184925 Thread-1開始------sleep time = 1639464184925 Thread-1結束------sleep time = 1639464186928 Thread-1結束------notify time = 1639464186928 Thread-0開始------sleep time = 1639464186928 Thread-0結束------sleep time = 1639464188931 Thread-0結束------wait time = 1639464188931
可以看到第一行和第二行 開始執行之間只間隔了1s,說明wait方法確實進入等待,
而且沒有繼續執行wait後面的sleep 2秒,而是執行了notify方法,說明wait方法可以使呼叫該方法的執行緒釋放共享資源的鎖,然後從執行狀態退出,進入等待佇列,直到被再次喚醒。
第二行和第五行間隔2秒鐘,說明notify方法不會釋放共享資源的鎖。
第6行 說明notify執行完後,喚醒了剛才wait的執行緒,從而繼續執行後面的sleep方法。
說明notify方法可以隨機喚醒等待佇列中等待同一共享資源的“一個”執行緒,並使該執行緒退出等待佇列,進入可執行狀態,也就是notify()方法僅通知“一個”執行緒。
另外還有notifyAll()方法可以使所有正在等待佇列中等待同一共享資源的“全部”執行緒從等待狀態退出,進入可執行狀態。
此時,優先順序最高的那個執行緒最先執行,但也有可能是隨機執行,因為這要取決於JVM虛擬機器的實現。
方法join
前面的測試方法中幾乎都使用了join方法,那麼這個方法到底起到什麼作用呢?
在很多情況下,主執行緒建立並啟動子執行緒,如果子執行緒中要進行大量的耗時運算,主執行緒往往將早於子執行緒結束之前結束,
所以在主執行緒中使用join方法的作用就是讓主執行緒等待子執行緒執行緒物件銷燬。
/** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * * <p> This implementation uses a loop of {@code this.wait} calls * conditioned on {@code this.isAlive}. As a thread terminates the * {@code this.notifyAll} method is invoked. It is recommended that * applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. * * @param millis * the time to wait in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ 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; } } }
看下jdk API的原始碼可以看到,其實join內部使用的還是wait方法進行等待,
join(long millis)方法的一個重點是要區分出和sleep(long millis)方法的區別:
sleep(long millis)不釋放鎖,join(long millis)釋放鎖,因為join方法內部使用的是wait(),因此會釋放鎖。join()其實就是join(0)而已。
ThreadLocal類
ThreadLocal不是用來解決共享物件的多執行緒訪問問題的,而是實現每一個執行緒都維護自己的共享變數,起到執行緒隔離的作用。
關於ThreadLocal原始碼分析可以參考這篇文章:https://www.cnblogs.com/xrq730/p/4854813.html。
下面看個ThreadLocal的例子:
public class Tools { public static ThreadLocal<Object> tl = new ThreadLocal<Object>(); }
兩個執行緒類,分別向ThreadLocal裡設定值
public class MyThread1_1 extends Thread { @Override public void run() { try { for (int i = 0; i < 10; i++) { Tools.tl.set("ThreadA" + (i + 1)); System.out.println("ThreadA get Value=" + Tools.tl.get()); Thread.sleep(200); } } catch (InterruptedException e) { e.printStackTrace(); } } }
public class MyThread1_2 extends Thread { @Override public void run() { try { for (int i = 0; i < 10; i++) { Tools.tl.set("ThreadB" + (i + 1)); System.out.println("ThreadB get Value=" + Tools.tl.get()); Thread.sleep(200); } } catch (InterruptedException e) { e.printStackTrace(); } } }
@Test public void test1() { try { MyThread1_1 a = new MyThread1_1(); MyThread1_2 b = new MyThread1_2(); a.start(); b.start(); a.join(); b.join(); } catch (Exception e) { e.printStackTrace(); } }
執行結果:
ThreadB get Value=ThreadB1 ThreadA get Value=ThreadA1 ThreadA get Value=ThreadA2 ThreadB get Value=ThreadB2 ThreadA get Value=ThreadA3 ThreadB get Value=ThreadB3 ThreadA get Value=ThreadA4 ThreadB get Value=ThreadB4 ThreadB get Value=ThreadB5 ThreadA get Value=ThreadA5 ThreadB get Value=ThreadB6 ThreadA get Value=ThreadA6 ThreadB get Value=ThreadB7 ThreadA get Value=ThreadA7 ThreadB get Value=ThreadB8 ThreadA get Value=ThreadA8 ThreadA get Value=ThreadA9 ThreadB get Value=ThreadB9 ThreadB get Value=ThreadB10 ThreadA get Value=ThreadA10
可以看到兩個執行緒取出的值沒有重複也沒有互相影響,其實它內部變化的只是執行緒本身的 ThreadLocalMap。
感興趣的還可以去看看 InheritableThreadLocal,它可以在子執行緒中取得父執行緒繼承下來的值。
參考文獻
1:《Java併發程式設計的藝術》
2:《Java多執行緒程式設計核心技術》