線程通信的原理

線程開始運行,擁有自己的的棧空間,就如同一個腳本一樣,按照既定的代碼一步一步地執行,直到終止。但是,每個運行中的線程,如果僅僅是孤立的運行,那麼沒有一點兒價值,或者說價值很少。如果多個線程能夠相互配合完成工作,這將會帶來巨大的價值。

volatile 和 synchronized 關鍵字

Java 支持多個線程同時訪問一個對象或者對象的成員變量,由於每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量分配的內存是在共享內存中的,但是每個執行的線程還是可以擁有一份拷貝,這樣做的目的是加速程序的執行,這是現代多核處理器的一個顯著特性),所以程序在執行過程中,一個線程看到的變量並不一定是最新的。

volatile

關鍵字volatile可以用來修飾字段(成員變量),就是告知程序任何對該變量的訪問均需要從共享內存中獲取,而對它的改變必須同步刷新回共享內存,它能保證所有線程對變量訪問的可見性。
舉個例子,定義一個表示程序是否運行的成員變量boolean on = true,那麼另一個線程可能對它執行關閉動作(on = false),這裏涉及多個線程對變量的訪問,因此需要將其定義成爲volatile boolean on = true,這樣其他線程對它進行改變時,可以讓所有線程感知到變化,因爲所有對on變量的訪問和修改都需要以共享內存爲準。但是,過多的使用volatile 是不惜要的,因爲它會降低程序執行的效率

synchronized

關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一時刻,只能有一個線程處於方法或者同步塊中,它保證了線程對變量訪問的可見性和排他性。
在如下代碼清單中,使用了同步塊和同步方法,通過使用javap工具查看生成的class文件信息來分析synchronized關鍵字的實現細節

public class synchronized{
	public static void main(String[] args){
	// 對synchronized class 對象進行加鎖
		synchronized (Synchronized.class){
		}
		//靜態同步方法,對Synchronized Class對象進行枷鎖
		m();
	}
	public static synchronized void m(){
	};
}

在Synchronized.class 統計目錄執行javap-v Synchronized.class 部分相關輸出如下所示:

public static void main(java.lang.String[]);
	//方法修飾符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
	Code:
		stack=2, locals=1, args_size=1
		01dc    #1 //class com/murdock/books/multithread/book/Synchronized
		2: dup
		3: monitorenter //monitorenter:監視器進入,獲取鎖
		4: monitorenter //monitorexit:監視器退出,釋放鎖
		5: invokestatic  #16 //Method m:V
		8: return
		public static synchronized void m();
		//方法修飾符,表示:public static synchronized
		flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
			Code:
				stack=0,locals=0,args_size=0
				0: return

上面class信息中,對於同步快的實現使用了monitorenter和monitorexit指令,而同步方法則是依靠方法修飾符上的ACC_SYNCHRONIZED 來完成的。無論採用哪種方式,其本質是對一個對象的監視器(monitor)進行獲取,而這個獲取是排他的,也就是同一時刻只能有一個線程獲取到有synchronized所保護對象的監視器。
任意一個對象都擁有自己的監視器,當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先獲取到該對象的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的線程將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。
在這裏插入圖片描述
圖1-1 對象、監視器、同步隊列和執行線程之間的關係

如圖1-1可以看出,任意線程對Object(Object 由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變爲BLOCKED。當訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放鎖操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對監視器的獲取。

等待/通知機制

一個線程修改了一個對象的值,而另一個線程感知到了變化,然後進行相應的操作,整個過程開始於一個線程,而最終執行又是另一個線程。前者是生產者,後者是消費者,這種模式隔離了“做什麼(what)和怎麼做(How)”,在功能層面上實現瞭解耦,體系結構上具備了良好的伸縮性,但是在Java語言中如何實現類似的功能呢?

簡單的辦法是讓消費者線程不斷的循環檢查變量是否符合預期,如下面代碼所示,在while循環中設置不滿足的條件,如果條件滿足則退出while循環,從而完成消費者的工作

	while (value != desire){
		Thread.sleep(1000);
	}
	doSomething();

上面這段僞代碼在條件不滿足時就睡眠一段時間,這樣做的目的是防止過快的“無效”嘗試,這總方式看似能夠解實現所需的功能,但是卻存在如下問題。

  1. 難以確保及時性。在睡眠時,基本不消耗處理器資源,但是如果睡得過久,就不能及時發現條件已經變化,也就是及時性難以保證。
  2. 難以減低開銷。如果降低睡眠的時間,比如休閒1毫秒,這樣消費者能更加迅速地發現條件變化,但是卻可能消耗更多的處理器資源,造成了無端的浪費。

以上兩個問題,看似矛盾難以調和,但是Java通過內置的等待/通知機制能夠很好的解決這個矛盾並實現所需的功能。

等待/通知的相關方法是任意Java對象都是具備的,因爲這些方法被定義在所有對象的超類java.lang.Object 方法和描述如下表所示:

方法名稱 描述
notify() 通知一個在對象上等待的線程,使其從wait()方法返回,而返回的前提是該線程獲取到了對象的鎖
notifyAll() 通知所有等待在該對象上的線程
wait() 調用該方法的線程進入WAITING狀態,只有等待另外線程的通知或被中斷纔會返回,需要注意,調用wait()方法後,會釋放對象的鎖
wait(long) 超時等待一段時間,這裏的參數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回
wait(long,int) 對於超時時間更細粒度的控制,可以達到納秒

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

如下代碼,創建了兩個線程 WaitTHreadNotifyThread,前者檢查flag值是否爲false。如果符合要求,進行後續操作,否則在lock上等待,後者在睡眠了一段時間後對lock進行通知,示例如下:

public class WaitNotify{
	static boolean flag = true;
	static Object lock = new Object();;
	
	public static void main(String[] args) throws Exception{
		Thread waitThread = new Thread(new Wait(),"WaitThread");
		waitThread.start();
		TimeUnit.SECONDS.sleep(1);
		Thread notifyThread = new Thread(new Notify(),"NotifyThread");
		notifyThread.start();
	}
	
	static class Wait implements Runnable{
		public void run{
			//加鎖,擁有lock的Monitor
			synchronized(lock){
				//當條件不滿足時,繼續wait,同時釋放了lock的鎖
				while(flag){
					System.out.println(Thread.currentThread()+"flag is true,wait@"+new SimpleDateFormat("HH:mm:ss").format(new Date()));
					lock.wait();
				}
			}
			//條件滿足時,完成工作
			System.out.pringln(Thread.currentThread()+"flag is false. running@"+new SimpleDateFormat("HH:mm:ss").format(new Date()));
		}
	}
	static class Notify implements Runnable{
		 public void run(){
			//加鎖,擁有lock的Monitor
			synchronized(lock){
				//獲取lock的鎖,然後進行通知,通知時不會釋放lock的鎖
				//知道當前線程釋放了lock後,WaitThread才能從wait方法中返回
				System.out.pringln(Thread.currentThread()+" hold lock.notify@"+new SimpleDateFormat("HH:mm:ss").format(new Date()));
				lock.notifyAll();
				SleepUtils.second(5);
			}
			synchronized(lock){
				System.out.println(Thread.currentThread()+"hold lock again. sleep@"+new SimpleDateFormat("HH:mm:ss").format(new Data()));
			}
		 }
	}
}
輸出如下:(輸出內容可能不容,主要去唄在時間上)
1.Thread[WaitThread,5,main] flag is true.wait @ 17:12:03
2.Thread[NotifyThread,5,main] hold lock.notify @ 17:12:04
3.Thread[NotifyThread,5,main] hold lock again. sleep @ 17:12:05
4.Thread[WaitThread,5,main] flag is false. running @ 17:12:06

上述第三行和第四行輸出的順序可能會互換,而上述例子主要說明了調用wait()、norify()、以及notifyAll() 時需要注意的細節,如下:

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

從上述細節中可以看到,等待/通知機制依託於同步機制,其目的就是確保等待線程從wait()方法返回時能夠感知到線程對變量做出的修改

在這裏插入圖片描述
如上圖所示,WaitThread首先獲取了對象的鎖,然後調用對象的wait()方法,從而放棄了鎖並進入了對象的等待隊列WaitQueue中,進入等待狀態。由於WaitThread釋放了對象的鎖,NotifyThread隨後獲取了對象的鎖,並調用對象的Notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變爲阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()返回繼續執行

注意:只要同步隊列內等待的線程未獲取到對象鎖,該(等待)線程始終未從wait()方法返回

生產經典模式

以上示例中可以提煉出等待/通知的經典範式,該範式分爲兩部分,分別針對等待方(消費者)和通知方(生產者)
等待方遵循如下原則。

  1. 獲取對象的鎖
  2. 如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件
  3. 條件滿足則執行對應的邏輯。
    對應的僞代碼如下:
    synchronized(對象){
    while(條件不滿足){
    對象.wait();
    }
    對應的處理邏輯
    }

通知方遵循如下原則

  1. 獲得對象的鎖
  2. 改變條件
  3. 通知所有等待在對象上的線程
    對應的僞代碼如下:
    synchronized(對象){
    改變條件
    對象.notifyAll();
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章