1. 程式人生 > >Java併發程式設計基礎

Java併發程式設計基礎

4.1 執行緒簡介

4.1.1 什麼是執行緒

        現代作業系統在執行一個程式時,會為其建立一個程序。例如,啟動一個Java程式,作業系統就會建立一個Java程序。現代作業系統排程的最小單元是執行緒,也叫輕量級程序(LightWeight Process),在一個程序裡可以建立多個執行緒,這些執行緒都擁有各自的計數器、堆疊和區域性變數等屬性,並且能夠訪問共享的記憶體變數。處理器在這些執行緒上高速切換,讓使用者感覺到這些執行緒在同時執行。一個Java程式從main()方法開始執行,然後按照既定的程式碼邏輯執行,看似沒有其他執行緒參與,但實際上Java程式天生就是多執行緒程式,因為執行main()方法的是一個名稱為main的執行緒。下面使用JMX來檢視一個普通的Java程式包含哪些執行緒,如程式碼清單4-1所示:

public class MultiThread{
	public static void main(String[] args) {
		// 獲取Java執行緒管理MXBean
		ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
		// 不需要獲取同步的monitor和synchronizer資訊,僅獲取執行緒和執行緒堆疊資訊
		ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
		// 遍歷執行緒資訊,僅列印執行緒ID和執行緒名稱資訊
		for (ThreadInfo threadInfo : threadInfos) {
			System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.
			getThreadName());
		}
	}
}

輸出如下所示(輸出內容可能不同)

[4] Signal Dispatcher // 分發處理髮送給JVM訊號的執行緒
[3] Finalizer // 呼叫物件finalize方法的執行緒
[2] Reference Handler // 清除Reference的執行緒
[1] main // main執行緒,使用者程式入口

4.1.2 為什麼要使用多執行緒 (1)更多的處理器核心 執行緒是大多數作業系統排程的基本單元,一個程式作為一個程序來執行,程式執行過程中能夠建立多個執行緒,而一個執行緒在一個時刻只能執行在一個處理器核心上。試想一下,一個單執行緒程式在執行時只能使用一個處理器核心,那麼再多的處理器核心加入也無法顯著提升該程式的執行效率。相反,如果該程式使用多執行緒技術,將計算邏輯分配到多個處理器核心上,就會顯著減少程式的處理時間,並且隨著更多處理器核心的加入而變得更有效率。 (2)更快的響應時間 有時我們會編寫一些較為複雜的程式碼(這裡的複雜不是說複雜的演算法,而是複雜的業務邏輯),例如,一筆訂單的建立,它包括插入訂單資料、生成訂單快照、傳送郵件通知賣家和記錄貨品銷售數量等。使用者從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢? 在上面的場景中,可以使用多執行緒技術,即將資料一致性不強的操作派發給其他執行緒處理(也可以使用訊息佇列),如生成訂單快照、傳送郵件等。這樣做的好處是響應使用者請求的執行緒能夠儘可能快地處理完成,縮短了響應時間,提升了使用者體驗。 (3)更好的程式設計模型 4.1.3 執行緒優先順序 4.1.4 執行緒的狀態 Java執行緒在執行的生命週期中可能處於表4-1所示的6種不同的狀態,在給定的一個時刻,執行緒只能處於其中的一個狀態:

java執行緒的狀態
NEW 初始狀態,執行緒被建立,但是還沒有呼叫start()方法
RUNNABLE 執行狀態,Java執行緒將作業系統中的就緒和執行兩種狀態籠統地稱作執行中
BLOCKED 阻塞狀態,表示執行緒阻塞於鎖
WAITING 等待狀態,表示執行緒進入等待狀態,進入改狀態表示當前執行緒需要等待其他執行緒做出一些特定動作(通知或中斷)
TIME_WAITING 超出等待狀態,改狀態不同於waiting,它是可以在指定的時間自行返回的
TERMINATED 終止狀態,表示當前執行緒已經執行完畢

執行緒在自身的生命週期中,並不是固定地處於某個狀態,而是隨著程式碼的執行在不同的狀態之間進行切換,Java執行緒狀態 變遷如圖4-1示:

由圖4-1中可以看到,執行緒建立之後,呼叫start()方法開始執行。當執行緒執行wait()方法之後,執行緒進入等待狀態。進入等待狀態的執行緒需要依靠其他執行緒的通知才能夠返回到執行狀態,而超時等待狀態相當於在等待狀態的基礎上增加了超時限制,也就是超時時間到達時將會返回到執行狀態。當執行緒呼叫同步方法時,在沒有獲取到鎖的情況下,執行緒將會進入到阻塞狀態。執行緒在執行Runnable的run()方法之後將會進入到終止狀態。注意 Java將作業系統中的執行和就緒兩個狀態合併稱為執行狀態。阻塞狀態是執行緒阻塞在進入synchronized關鍵字修飾的方法或程式碼塊(獲取鎖)時的狀態,但是阻塞在java.concurrent包中Lock介面的執行緒狀態卻是等待狀態,因為java.concurrent包中Lock介面對於阻塞的實現均使用了LockSupport類中的相關方法。 4.1.5 Daemon執行緒

4.2 啟動和終止執行緒

通過呼叫執行緒的start()方法進行啟動,隨著run()方法的執行完畢,執行緒也隨之終止,大家對此一定不會陌生,下面將詳細介紹執行緒的啟動和終止。 4.2.1 構造執行緒 4.2.2 啟動執行緒 4.2.3 理解中斷 4.2.4 過期的suspend()、resume()和stop() 4.2.5 安全地終止執行緒

4.3 執行緒間通訊

4.3.1 volatile和synchronized關鍵字        關鍵字volatile可以用來修飾字段(成員變數),就是告知程式任何對該變數的訪問均需要從共享記憶體中獲取,而對它的改變必須同步重新整理回共享記憶體,它能保證所有執行緒對變數訪問的可見性。但是,過多地使用volatile是不必要的,因為它會降低程式執行的效率。        關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性。 以下程式碼使用了同步塊和同步方法,通過使用javap工具檢視生成的class檔案資訊來分析synchronized關鍵字的實現細節,示例如下

Synchronized.java
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
	0: ldc #1 // class com/murdock/books/multithread/book/Synchronized
	2: dup
	3: monitorenter // monitorenter:監視器進入,獲取鎖
	4: monitorexit // 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狀態。 圖4-2描述了物件、物件的監視器、同步佇列和執行執行緒之間的關係:

從圖4-2中可以看到,任意執行緒對Object(Object由synchronized保護)的訪問,首先要獲得Object的監視器。如果獲取失敗,執行緒進入同步佇列,執行緒狀態變為BLOCKED。當訪問Object的前驅(獲得了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步佇列中的執行緒,使其重新嘗試對監視器的獲取。 4.3.2 等待/通知機制 等待/通知的相關方法是任意Java物件都具備的,因為這些方法被定義在所有物件的超類java.lang.Object上,方法如下所示:

以下程式碼建立了兩個執行緒——WaitThread和NotifyThread,前者檢查flag值是否為false,如果符合要求,進行後續操作,否則在lock上等待,後者在睡眠了一段時間後對lock進行通知,示例如下所示。  

WaitNotify.java
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) {
				try {
					System.out.println(Thread.currentThread() + " flag is true. [email protected] " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
					lock.wait();
					} catch (InterruptedException e) {
					}
				}
				// 條件滿足時,完成工作System.out.println(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.println(Thread.currentThread() + " hold lock. notify @ " +new SimpleDateFormat("HH:mm:ss").format(new Date()));
				lock.notifyAll();
				flag = false;
				SleepUtils.second(5);
			}
			// 再次加鎖
			synchronized (lock) {
				System.out.println(Thread.currentThread() + " hold lock again. [email protected] " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
				SleepUtils.second(5);
			}
		}
	}
}

呼叫wait()、notify()以及notifyAll()時需要注意的細節,如下:

  1. 使用wait()、notify()和notifyAll()時需要先對呼叫物件加鎖
  2. 呼叫wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到物件的等待佇列
  3. notify()或notifyAll()方法呼叫後,等待執行緒依舊不會從wait()返回,需要呼叫notify()或notifAll()的執行緒釋放鎖之後,等待執行緒才有機會從wait()返回
  4. notify()方法將等待佇列中的一個等待執行緒從等待佇列中移到同步佇列中,而notifyAll()方法則是將等待佇列中所有的執行緒全部移到同步佇列,被移動的執行緒狀態由WAITING變為BLOCKED
  5. 從wait()方法返回的前提是獲得了呼叫物件的鎖。等待/通知機制依託於同步機制,其目的就是確保等待執行緒從wait()方法返回時能夠感知到通知執行緒對變數做出的修改  
WaitNotify.java執行過程

WaitThread首先獲取了物件的鎖,然後呼叫物件的wait()方法,從而放棄了鎖並進入了物件的等待佇列WaitQueue中,進入等待狀態。由於WaitThread釋放了物件的鎖,NotifyThread隨後獲取了物件的鎖,並呼叫物件的notify()方法,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態變為阻塞狀態。NotifyThread釋放了鎖之後,WaitThread再次獲取到鎖並從wait()方法返回繼續執行。 4.3.3 等待/通知的經典範式 從4.3.2節中的WaitNotify示例中可以提煉出等待/通知的經典範式,該正規化分為兩部分,分別針對等待方(消費者)和通知方(生產者) 等待方遵循如下原則。 1)獲取物件的鎖。 2)如果條件不滿足,那麼呼叫物件的wait()方法,被通知後仍要檢查條件。 3)條件滿足則執行對應的邏輯。 對應的虛擬碼如下。

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

通知方遵循如下原則。 1)獲得物件的鎖。 2)改變條件。 3)通知所有等待在物件上的執行緒。 對應的虛擬碼如下:

synchronized(物件) {
	改變條件
	物件.notifyAll();
}

4.3.4 管道輸入/輸出流 4.3.5 Thread.join()的使用 如果一個執行緒A執行了thread.join()語句,其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回。執行緒Thread除了提供join()方法之外,還提供了join(long millis)和join(longmillis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示,如果執行緒thread在給定的超時時間裡沒有終止,那麼將會從該超時方法中返回。 以下程式碼,建立了10個執行緒,編號0~9,每個執行緒呼叫前一個執行緒的join()方法,也就是執行緒0結束了,執行緒1才能從join()方法中返回,而執行緒0需要等待main執行緒結束。

Join.java
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()方法返回,這裡涉及了等待/通知機制(等待前驅執行緒結束,接收前驅執行緒結束通知) 4.3.6 ThreadLocal的使用 ThreadLocal,即執行緒變數,是一個以ThreadLocal物件為鍵、任意物件為值的儲存結構。這個結構被附帶線上程上,也就是說一個執行緒可以根據一個ThreadLocal物件查詢到繫結在這個執行緒上的一個值,可以通過set(T)方法來設定一個值,在當前執行緒下再通過get()方法獲取到原先設定的值 以下程式碼中,構建了一個常用的Profiler類,它具有begin()和end()兩個方法,而end()方法返回從begin()方法呼叫開始到end()方法被呼叫時的時間差,單位是毫秒。  

Profiler.java
public class Profiler {
	// 第一次get()方法呼叫時會進行初始化(如果set方法沒有呼叫),每個執行緒會呼叫一次
	private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
		protected Long initialValue() {
			return System.currentTimeMillis();
		}
	};
	public static final void begin() {
		TIME_THREADLOCAL.set(System.currentTimeMillis());
	}
	public static final long end() {
		return System.currentTimeMillis() - TIME_THREADLOCAL.get();
	}
	public static void main(String[] args) throws Exception {
		Profiler.begin();
		TimeUnit.SECONDS.sleep(1);
		System.out.println("Cost: " + Profiler.end() + " mills");
	}
}
輸出結果如下所示。
Cost: 1001 mills

Profiler可以被複用在方法呼叫耗時統計的功能上,在方法的入口前執行begin()方法,在方法呼叫後執行end()方法,好處是兩個方法的呼叫不用在一個方法或者類中,比如在AOP(面向方面程式設計)中,可以在方法呼叫前的切入點執行begin()方法,而在方法呼叫後的切入點執行 end()方法,這樣依舊可以獲得方法的執行耗時。

4.4 執行緒應用例項

        開發人員經常會遇到這樣的方法呼叫場景:呼叫一個方法時等待一段時間(一般來說是給定一個時間段),如果該方法能夠在給定的時間段之內得到結果,那麼將結果立刻返回,反之,超時返回預設結果。        前面的章節介紹了等待/通知的經典範式,即加鎖、條件迴圈和處理邏輯3個步驟,而這種正規化無法做到超時等待。而超時等待的加入,只需要對經典範式做出非常小的改動,改動內容如下所示。 假設超時時間段是T,那麼可以推斷出在當前時間now+T之後就會超時,定義如下變數:

  • 等待持續時間:REMAINING=T
  • 超時時間:FUTURE=now+T

這時僅需要wait(REMAINING)即可,在wait(REMAINING)返回之後會將執行:REMAINING=FUTURE–now。如果REMAINING小於等於0,表示已經超時,直接退出,否則將繼續執行wait(REMAINING),上述描述等待超時模式的虛擬碼如下:

// 對當前物件加鎖
public synchronized Object get(long mills) throws InterruptedException {
	long future = System.currentTimeMillis() + mills;
	long remaining = mills;
	// 當超時大於0並且result返回值不滿足要求
	while ((result == null) && remaining > 0) {
		wait(remaining);remaining = future - System.currentTimeMillis();
	}
	return result;
}

可以看出,等待超時模式就是在等待/通知正規化基礎上增加了超時控制,這使得該模式相比原有正規化更具有靈活性,因為即使方法執行時間過長,也不會“永久”阻塞呼叫者,而是會按照呼叫者的要求“按時”返回。 4.4.2 一個簡單的資料庫連線池示例 4.4.3 執行緒池技術及其示例        對於服務端的程式,經常面對的是客戶端傳入的短小(執行時間短、工作內容較為單一)任務,需要服務端快速處理並返回結果。如果服務端每次接受到一個任務,建立一個執行緒,然後進行執行,這在原型階段是個不錯的選擇,但是面對成千上萬的任務遞交進伺服器時,如果還是採用一個任務一個執行緒的方式,那麼將會建立數以萬記的執行緒,這不是一個好的選擇。因為這會使作業系統頻繁的進行執行緒上下文切換,無故增加系統的負載,而執行緒的建立和消亡都是需要耗費系統資源的,也無疑浪費了系統資源。        執行緒池技術能夠很好地解決這個問題,它預先建立了若干數量的執行緒,並且不能由使用者直接對執行緒的建立進行控制,在這個前提下重複使用固定或較為固定數目的執行緒來完成任務的執行。這樣做的好處是,一方面,消除了頻繁建立和消亡執行緒的系統資源開銷,另一方面,面對過量任務的提交能夠平緩的劣化。        執行緒池的本質就是使用了一個執行緒安全的工作佇列連線工作者執行緒和客戶端執行緒,客戶端執行緒將任務放入工作佇列後便返回,而工作者執行緒則不斷地從工作佇列上取出工作並執行。當工作佇列為空時,所有的工作者執行緒均等待在工作佇列上,當有客戶端提交了一個任務之後會通知任意一個工作者執行緒,隨著大量的任務被提交,更多的工作者執行緒會被喚醒。 4.4.4 一個基於執行緒池技術的簡單Web伺服器