高併發學習之04線程間通信

1.線程間通信

線程開始運行,擁有自己的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執
行,直到終止。但是,每個運行中的線程,如果僅僅是孤立地運行,那麼沒有一點兒價值,或者說價值很少,如果多個線程能夠相互配合完成工作,這將會帶來巨大的價值。在JMM內存模型中我們說道JMM本身是爲了解決併發編程中三大問題:原子性、可見性、有序性。而這三大問題本質上就是多線程間通信問題。

1.1 線程間協同

要想實現多個線程的協同:如線程執行的先後順序、獲取某個線程執行結果等等。涉及到線程之間相互通信,大致分爲四類:

  • 文件共享
  • 網絡共享
  • 共享變量
  • jdk提供的線程協調API:suspend/resume 、wait/notify、park/unpark

文件、網絡共享這裏就不說了!共享變量會通過關鍵字(volatile)細說。這裏討論下jdk提供的API。

1.2 suspend/resume

大家對於CD機肯定不會陌生,如果把它播放音樂比作一個線程的運作,那麼對音樂播放
做出的暫停、恢復和停止操作對應在線程Thread的API就是suspend()、resume()和stop()。

 /**
 * 包子店
 */
 public static Object shop = null;
 public void suspendResumeTest() throws Exception {
        // 啓動線程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果沒包子,則進入等待
                System.out.println("1、進入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("2、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之後,生產一個包子
        Thread.sleep(3000L);
        shop = new Object();
        consumerThread.resume();
   }    

輸出結果:
1、進入等待
2、買到包子,回家
3、通知消費者
這段代碼很簡單 啓動一個消費者線程,如果店鋪有包子就購買,沒有包子則掛起等待,主線程休眠三秒後開始生產包子後喚起消費線程。這段代碼開着沒有問題,但是它有個隱患,消費者線程在掛起的時候是沒有釋放資源的,容易引起死鎖!舉個例子:

 public void suspendResumeDeadLockTest() throws Exception {
        // 啓動線程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果沒包子,則進入等待
                System.out.println("1、進入等待");
                // 當前線程拿到鎖,然後掛起
                synchronized (this) {
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("2、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之後,生產一個包子
        Thread.sleep(3000L);
        shop = new Object();
        // 爭取到鎖以後,再恢復consumerThread
        synchronized (this) {
            consumerThread.resume();
        }
        System.out.println("3、通知消費者");
    }

這段代碼是不會有結果的,在消費者進入等待過程中,沒有釋放鎖,主線程永遠拿不到鎖!正因爲suspend()、resume()和stop()方法帶來的副作用,這些方法才被標註爲不建議使用的過期方法,而暫停和恢復操作可以用後面提到的等待/通知機制來替代。

1.3 wait/notify

等待/通知的相關方法
等待/通知機制,是指一個線程A調用了對象O的wait()方法進入等待狀態,而另一個線程B
調用了對象O的notify()或者notifyAll()方法,線程A收到通知後從對象O的wait()方法返回,進而執行後續操作。上述兩個線程通過對象O來完成交互,而對象上的wait()和notify/notifyAll()的關係就如同開關信號一樣,用來完成等待方和通知方之間的交互工作。

public static Object shop = null;
public void waitNotifyTest() throws Exception {
        // 啓動線程
        new Thread(() -> {
            if (shop == null) { // 如果沒包子,則進入等待
                synchronized (this) {
                    try {
                        System.out.println("1、進入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、買到包子,回家");
        }).start();
        // 3秒之後,生產一個包子
        Thread.sleep(3000L);
        shop = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消費者");
        }
    }

調用結果:
1、進入等待
3、通知消費者
2、買到包子,回家
消費者獲取到鎖之後判斷沒有包子,就掛起等待,同時釋放鎖,主線程生產獲取資源後生產包子,通知消費者消費。通過上面代碼可以看出來消費者線程wait時釋放了鎖資源。
當然 wait和notify也有需要注意的地方。

 /**
     * 會導致程序永久等待的wait/notify
     */
    public void waitNotifyDeadLockTest() throws Exception {
        // 啓動線程
        new Thread(() -> {
            if (shop == null) { // 如果沒包子,則進入等待
                try {
                    Thread.sleep(5000L);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                synchronized (this) {
                    try {
                        System.out.println("1、進入等待");
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println("2、買到包子,回家");
        }).start();
        // 3秒之後,生產一個包子
        Thread.sleep(3000L);
        shop = new Object();
        synchronized (this) {
            this.notifyAll();
            System.out.println("3、通知消費者");
        }
    }

消費者休眠5秒,主線程生產者休眠3秒,當主線程休眠結束通知消費者線程的時候,是喚不起消費者的。
通過上面兩個例子,可以看出來wait、notify或者notifyAll,需要注意如下幾點:

  • 使用wait()、notify()和notifyAll()時需要先對調用對象加鎖。
  • 調用wait()方法後,線程狀態由RUNNING變爲WAITING,並將當前線程放置到對象的等待隊列。
  • notify()或notifyAll()方法調用後,等待線程依舊不會從wait()返回,需要調用notify()或notifAll()的線程釋放鎖之後,等待線程纔有機會從wait()返回。
  • notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態由WAITING變爲BLOCKED。
  • 從wait()方法返回的前提是獲得了調用對象的鎖。

從上述細節中可以看到,等待/通知機制依託於同步機制,其目的就是確保等待線程從
wait()方法返回時能夠感知到通知線程對變量做出的修改。wait、notify運行過程
WaitThread首先獲取了對象的鎖,然後調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入等待狀態。由於WaitThread釋放了對象的鎖,NotifyThread隨後獲取了對象的鎖,並調用對象的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變爲阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。

1.4 wait/notify的經典範式

通過1.3 wait、notify的事例我們可以提煉出等待/通知的經典範式,該範式分爲兩部分,分
別針對等待方(消費者)和通知方(生產者)。
等待方遵循如下原則。
1)獲取對象的鎖。
2)如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
3)條件滿足則執行對應的邏輯。

synchronized(對象) {
	while(條件不滿足) {
		對象.wait();
	}
	對應的處理邏輯
}

通知方遵循如下原則。
1)獲得對象的鎖。
2)改變條件。
3)通知所有等待在對象上的線程。

synchronized(對象) {
	改變條件
	對象.notifyAll();
}
1.6 park/unpark 的使用

無論是wait/notify 或者是已經過時的suspend/resume ,在使用上都有過多的限制和需要注意的地方。有沒有一個用的更爽的方式,答案當然是有的啊,在J.U.C的包中LockSupport提供了一種更爲簡潔的方式:park/unpark。

public static Object shop = null;
public void parkUnparkTest() throws Exception {
        // 啓動線程
        Thread consumerThread = new Thread(() -> {
            if (shop == null) { // 如果沒包子,則進入等待
                System.out.println("1、進入等待");
                LockSupport.park();
            }
            System.out.println("2、買到包子,回家");
        });
        consumerThread.start();
        // 3秒之後,生產一個包子
        Thread.sleep(3000L);
        shop = new Object();
        LockSupport.unpark(consumerThread);
        System.out.println("3、通知消費者");
    }

park 和unpark不要求調用順序,多次調用unpark之後在調用park,線程會直接運行。但不會疊加,也就是說連續多次調用park方法,第一次會拿到“許可”直接運行,後續調用會進入等待。
LockSupport提供的阻塞和喚醒方法

1.7 Thread.join()的使用

如果一個線程A執行了thread.join()語句,其含義是:當前線程A等待thread線程終止之後才
從thread.join()返回。線程Thread除了提供join()方法之外,還提供了join(long millis)join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果線程thread在給定的超時時間裏沒有終止,那麼將會從該超時方法中返回。
看下面事例代碼:
在下面所示的例子中,創建了10個線程,編號0~9,每個線程調用前一個線程的join()方法,也就是線程0結束了,線程1才能從join()方法中返回,而線程0需要等待main線程結束。

public class Join {
    public static void main(String[] args) throws Exception {
        Thread previous = Thread.currentThread();
        for (int i = 0; i < 10; i++) {
			// 每個線程擁有前一個線程的引用,需要等待前一個線程終止,才能從等待中返回
            Thread thread = new Thread(new Domino(previous), String.valueOf(i));
            thread.start();
            previous = thread;
        }
        TimeUnit.SECONDS.sleep(5);
        System.out.println(Thread.currentThread().getName() + " terminate.");
    }

    static class Domino implements Runnable {
        private Thread thread;

        public Domino(Thread thread) {
            this.thread = thread;
        }

        public void run() {
            try {
                thread.join();
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + " terminate.");
        }
    }
}

輸出如下:

main terminate.
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.

從上述輸出可以看到,每個線程終止的前提是前驅線程的終止,每個線程等待前驅線程終止後,才從join()方法返回,這裏涉及了等待/通知機制(等待前驅線程結束,接收前驅線程結束通知)。

1.8 ThreadLocal的使用

ThreadLocal,即線程變量,是一個以ThreadLocal對象爲鍵、任意對象爲值的存儲結構。這個結構被附帶在線程上,也就是說一個線程可以根據一個ThreadLocal對象查詢到綁定在這個線程上的一個值。
可以通過set(T)方法來設置一個值,在當前線程下再通過get()方法獲取到原先設置的值。
說白了,JVM維護了一個Map<Thread,T>,每個線程就是一個map裏面的key,如果想使用value,必須通過線程這個key來獲取。

public class Demo7 {
	/** threadLocal變量,每個線程都有一個副本,互不干擾 */
	public static ThreadLocal<String> value = new ThreadLocal<>();

	/**
	 * threadlocal測試
	 * 
	 * @throws Exception
	 */
	public void threadLocalTest() throws Exception {

		// threadlocal線程封閉示例
		value.set("這是主線程設置的123"); // 主線程設置值
		String v = value.get();
		System.out.println("線程1執行之前,主線程取到的值:" + v);

		new Thread(new Runnable() {
			@Override
			public void run() {
				String v = value.get();
				System.out.println("線程1取到的值:" + v);
				// 設置 threadLocal
				value.set("這是線程1設置的456");

				v = value.get();
				System.out.println("重新設置之後,線程1取到的值:" + v);
				System.out.println("線程1執行結束");
			}
		}).start();

		Thread.sleep(5000L); // 等待所有線程執行結束

		v = value.get();
		System.out.println("線程1執行之後,主線程取到的值:" + v);

	}

	public static void main(String[] args) throws Exception {
		new Demo7().threadLocalTest();
	}
}

執行結果如下:

線程1執行之前,主線程取到的值:這是主線程設置的123
線程1取到的值:null
重新設置之後,線程1取到的值:這是線程1設置的456
線程1執行結束
線程1執行之後,主線程取到的值:這是主線程設置的123
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章