JAVA 線程通信和線程封閉

線程通信:以去Bank 取錢爲例

wait/notify

只能由同一個對象鎖的持有者線程調用,寫在同步代碼塊裏面。
wait():使當前線程等待,加入該對象的等待集合中,並釋放當前對象的鎖。wait() 後面的代碼是不執行的!
notify/notifyAll: 喚醒一個或所有正在等待這個對象鎖的線程。

代碼示例:

 public void waitNotifyNormalTest() throws InterruptedException {

        new Thread(() -> {
            try {
                synchronized (this) {
                    while (obj == null) {//防止僞喚醒,條件判斷放入對象鎖中
                        System.out.println("1、木有錢,進入等待");
                        wait();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("2、取到錢");

        }).start();

        //主線程休眠,子線程先執行,進入wait(),並讓出鎖:this
        Thread.sleep(3000L);

        synchronized (this) {
            obj = new Object();
            notifyAll();
            System.out.println("3、通知消費者");
        }
    }

注意:雖然wait會自動解鎖,但是對調用wait(),notify()/notifyAll() 有順序要求,如果在notify() 調用只有才調用的wait(),線程會永遠處於WAITING 狀態,代碼示例如下:


    /**
     * wait/notify 中先執行notify,在執行wait,無法喚醒等待的線程進入死鎖狀態
     *
     * @throws InterruptedException
     */
    public void waitNotifyDeadLockTest() throws InterruptedException {
        new Thread(() -> {
            while (obj == null) {
                try {
                    //子線程休眠5s,時間長於主線程休眠時間,因此主線程先拿到鎖,先執行notifyAll()
                    Thread.sleep(5000L);
                    synchronized (this) {
                        System.out.println("1、木有錢,進入等待");
                        wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("2、取到錢");

        }).start();

        //主線程休眠3s
        Thread.sleep(3000L);

        synchronized (this) {
            obj = new Object();
            notifyAll();
            System.out.println("3、通知消費者");
        }

    }

注意此處一定爲JDK 8 lamda 表達式的寫法 ,如果寫爲如下代碼,則在run() 中的this與主函數的this不爲同一個對象,也有違背wait(),notify()/notifyAll() 的使用條件

 public void waitNotifyDeadLockTest() throws InterruptedException {
         new Thread(new Runnable() {
            @Override
            public void run() {
                while (obj == null) {
                    try {
                        //子線程休眠5s,時間長於主線程休眠時間,因此主線程先拿到鎖,先執行notifyAll()
                        Thread.sleep(5000L);
                        synchronized (this) {
                            System.out.println("1、木有錢,進入等待");
                            wait();
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("2、取到錢");
                }
            }
        }).start();

        //主線程休眠3s
        Thread.sleep(3000L);

        synchronized (this) {
            obj = new Object();
            notifyAll();
            System.out.println("3、通知消費者");
        }

    }

park/unpark

線程調用park則需等待,unpark則喚醒。

  1. park()/unpark() 無調用順序要求;
  2. 多次調用unpark() 後,不會疊加,再調用park() ,線程會直接運行而不是等待;
  3. 在多次調用unpark() 後,再多次調用park(),第一次會直接運行,後續的調用會進入等待;
  4. 需要注意的是:如果在同步代碼塊中調用park() 則不會釋放鎖,因此使用時請注意!
    正常使用park(),unpark()的示例:
public void parkUnparkNormalTest() throws InterruptedException {

        Thread consumerThread = new Thread(() -> {
            while (obj == null) {
                System.out.println("1.木有錢,進入等待");
                LockSupport.park();
            }

            System.out.println("2.取到錢,回家!");
        });

        consumerThread.start();
        Thread.sleep(3000L);
        obj = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3.通知消費者");
    }

使用park(),unpark() 出現死鎖的場景:

 /**
     * 如果未使用lamda表達式的寫法,在重寫run() 中調用的this並非局部變量的this
     *
     * @throws InterruptedException
     */
    public void parkUnparkDeadLockTest() throws InterruptedException {

        Thread consumerThread = new Thread(() -> {

            if (obj == null) { // 如果木有錢,進入等待
                System.out.println("1、木有錢,進入等待");
                // 當前線程拿到鎖,然後掛起
                synchronized (this) {
                    LockSupport.park();
                }
            }
            System.out.println("2、取到錢,回家");

        });

        consumerThread.start();
        Thread.sleep(3000L);
        obj = new Object();
        synchronized (this) {
            LockSupport.unpark(consumerThread);
            System.out.println("3.通知消費者");
        }
    }

僞喚醒

僞喚醒是指線程並非因爲notify,notifyAll,unpark 等API調用而喚醒,而是更底層的原因導致。
使用場景:在前面的parkUnparkDeadLockTest() 中子線程使用if 語句來判斷是否進入等待狀態可能會遇到僞喚醒問題,因此應該爲在循環中檢查等待條件 ,原因是處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。

線程封閉

多線程訪問共享可變數據時,涉及到線程間的數據同步問題。然而線程封閉是指:將數據封閉在各自的線程中從而避免使用同步的技術。線程封閉具體現爲:ThreadLocal,局部變量。

ThreadLocal

ThreadLocal 是JAVA 中一種特殊的變量
它是一個線程級別的變量,每個線程都有一個ThreadLocal,在併發模式下絕對安全的變量。
它會自動在每一個線程上創建一個T的副本,副本之間彼此獨立,互不影響。可以使用ThreadLocal存儲一些參數,以便在線程中多個方法使用,用來代替方法傳參的做法。
相當於一個Map<Thread,T> 每個線程要用這個T的時候,就用當前線程取Map對應的值。
ThreadLocal使用方式如下:

public class ThreadLocalTest {

    /**
     * 每個線程都有一個獨立的副本,互不干擾
     */
    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        new ThreadLocalTest().test();

    }

    public void test() throws InterruptedException {

        threadLocal.set("大家好,我是主線程");
        String value = threadLocal.get();
        System.out.println("線程執行之前,主線程取到的值:" + value);

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子線程第一次獲取的值爲:" + threadLocal.get());

                threadLocal.set("Hello,我是子線程!");

                System.out.println("子線程第二次獲取的值爲:" + threadLocal.get());
            }
        }).start();


        Thread.sleep(5000L);

        System.out.println("線程執行結束,主線程獲取的值爲:" + threadLocal.get());

    }
}

輸出結果爲:

線程執行之前,主線程取到的值:大家好,我是主線程
子線程第一次獲取的值爲:null
子線程第二次獲取的值爲:Hello,我是子線程!
線程執行結束,主線程獲取的值爲:大家好,我是主線程

局部變量/ 棧封閉

之前在JVM執行原理你是否已經get中講到線程獨佔部分的本地方法棧就是屬於棧封閉。局部變量的固有屬性之一就是封閉在線程中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章