高性能編程——多線程併發編程Java基礎篇之線程通信

線程通信的方式

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

  1. 文件共享
  2. 網絡共享
  3. 共享變量
  4. jdk提供的線程協調API:
    細分爲:suspend/resume、wait/notify、park/unpark

文件共享

文件共享的意思就是一個線程把數據寫入一個文件(例如a.txt)中,而另外一個線程從這個文件中讀取數據的過程,例如下圖:
在這裏插入圖片描述

網絡共享

下次講網絡編程的時候會詳解。

變量共享

就是一個線程把數據寫入內存,而另一個線程去讀取該數據的過程。
在這裏插入圖片描述

線程協作-JDK API

這組API可以使用一個非常經典的使用場景:生產者 - 消費者模型來展現:
即線程1去買包子,沒有包子,則不再執行。線程-2生產出包子,通知線程1繼續執行(也可以用購買火車票來模擬):

被棄用的suspend和resume

調用suspend掛起目標線程,通過resume可以恢復線程執行。下面用suspend和resume來演示一下買包子的過程:

public static void main(String[] args) throws InterruptedException {
        //啓動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {
                System.out.println("1、還沒有包子,進入等待");
                Thread.currentThread().suspend();
            }
            System.out.println("2、買到包子,回家");
            baozidian=null;
        });

        consumerThread.start();
        //停止3秒,模擬做包子的過程
        Thread.sleep(3000L);
        baozidian=new Object();
        //喚醒線程
        consumerThread.resume();
        System.out.println("3、通知消費者");
    }

看上去也沒什麼問題,但是爲什麼啓用呢?因爲很容易出現死鎖的情況?再來看一段代碼。

//啓動線程
        Thread consumerThread = new Thread(() -> {
            if (baozidian == null) {
                System.out.println("1、還沒有包子,進入等待");
                synchronized(this) {
                    Thread.currentThread().suspend();
                }
            }
            System.out.println("2、買到包子,回家");
            baozidian=null;
        });

        consumerThread.start();
        //停止3秒,模擬做包子的過程
        Thread.sleep(3000L);
        baozidian=new Object();
        //喚醒線程
        synchronized (this) {
            consumerThread.resume();
        }
        System.out.println("3、通知消費者");

注意:此處不是在main方法中執行,因爲靜態方法無法使用this作爲鎖,一般都是用反射類來做鎖。而兩個this都是同一個實例,因爲lambada表達式中並不會將其視爲匿名內部類中的方法,而是該類的方法

死鎖成因

從上面的代碼可以明顯的看到兩個鎖對象是同一個,但是當線程suspend之前已經取得了鎖對象,而在resume之前還沒釋放該對象,所以想要釋放this鎖對象就必須執行完suspend的代碼塊,但是想要執行完該代碼塊必須resume,可是想要resume就必須釋放this鎖,三者是一個連環,但是一個都執行不了,造成了死鎖。

wait/notify機制

這兩個方法只能由同一對象鎖的持有者線程調用,也就是說寫在同步代碼塊裏,否則的話就拋出IllegalMonitorStateException異常。

wait方法導致當前線程等待,加入該對象的等待集合中,並且放棄當前持有的對象鎖。notify/notifyAll方法喚醒一個或所有正在等待這個對象鎖的線程。

注意:wait雖然會自動解鎖,但是對順序有要求,如果在notify被調用後纔開始wait的話,線程將永遠處於WAITING狀態

詳細代碼

public void waitNotifyTest() throws InterruptedException {

        Thread comsumerThread = new Thread(() -> {
            if(baozidian==null){
                System.out.println("1、沒有包子,開始等待");
                synchronized (this) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                System.out.println("2、包子已經做好,買走");
                }
            }
        });
        comsumerThread.start();
        //三秒後生產出一個包子
        Thread.sleep(3000l);
        synchronized (this) {
            this.notify();
        }
        System.out.println("3、通知消費者");
    }

上述代碼就是wait和notify的使用了,注意鎖是什麼就是什麼。這樣做可以避免線程同步帶來的死鎖問題了,但是還是有可能會出現線程被永久掛起的情況,如:

public void waitNotifyDeadLockTest() throws Exception {
		// 啓動線程
		new Thread(() -> {
			if (baozidian == 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);
		baozidian = new Object();
		synchronized (this) {
			this.notifyAll();
			System.out.println("3、通知消費者");
		}
	}

這段代碼很明顯就是會導致notifyAll之後才wait,就永久掛起了。

park/unpark機制

線程如果調用park則表示等待“許可”,unpark方法爲指定線程提供“許可(permit)”。

不要求park和unpark方法的調用順序。

代碼示例

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

死鎖演示

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

僞喚醒

其實我們之前代碼中用if語句來做判斷是否進入等待狀態是有問題的,這是一種錯誤的寫法。官方建議應該在循環中檢查等待條件,因爲處於等待狀態的線程可能會被僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。

僞喚醒是指線程並非因爲notify、notifyall、unpark等api調用而喚醒,是更底層的原因。

總結

每一種API都有導致線程的使用中出現問題,需要足夠了解才能避免出現死鎖和永久掛起現象。

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