java多執行緒5:執行緒間的通訊

讓我發會呆發表於2021-12-14

在多執行緒系統中,彼此之間的通訊協作非常重要,下面來聊聊執行緒間通訊的幾種方式。

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多執行緒程式設計核心技術》

 

相關文章