1. 程式人生 > 實用技巧 >【Java 併發程式設計系列】:執行緒基礎

【Java 併發程式設計系列】:執行緒基礎

什麼是執行緒

執行緒是程序中的一個實體,執行緒本身是不會獨立存在。程序是程式碼在資料集合上的一次執行活動, 是系統進行資源分配和排程的基本單位,執行緒則是程序的一個執行路徑, 一個程序中至少有一個執行緒,程序中的多個執行緒共享程序的資源。

作業系統在分配資源時是把資源分配給程序的, 但是CPU 資源比較特殊, 它是被分配到執行緒的, 因為真正要佔用CPU 執行的是執行緒,所以也說執行緒是CPU 分配的基本單位。

執行緒建立與執行

Java 中有三種執行緒建立方式,分別為:

  1. 通過繼承Thread類,重寫run方法
  2. 通過實現Runnable介面的run 方法
  3. 通過使用FutureTask 方式, 實現Callable介面的call 方法
  • 繼承Thread類,重寫run方法
public class ThreadTest {

	// 繼承Thread類並重寫run方法
	public static class MyThread extends Thread {
		@Override
		public void run() {
			System.out.println("I am a child thread");
		}
	}

	public static void main(String[] args) {

		// 建立執行緒
		Singleton.ThreadTest.MyThread thread = new Singleton.ThreadTest.MyThread();

		// 啟動執行緒
		thread.start();
	}
}

如上程式碼中,MyThread類繼承Thread類,並重寫run() 方法。建立MyThread例項後呼叫start方法啟動執行緒。值得注意的是,呼叫start 方法後執行緒不是馬上執行,而是處於就緒狀態,等待獲取CPU 資源後才會處於執行狀態,run 方法執行完畢後,執行緒處於終止狀態。

使用繼承方式的好處是, 在run 方法內獲取當前執行緒直接使用this 就可以了,無須使用Thread.currentThread() 方法;不好的地方是Java 不支援多繼承,如果繼承了Thread 類,那麼就不能再繼承其他類。另外任務與程式碼沒有分離, 當多個執行緒執行一樣的任務時需要多份任務程式碼,而Runable 則沒有這個限制。

  • 實現Runnable介面的run 方法
public class RunnableTask implements Runnable {

	@Override
	public void run() {
		System.out.println("I am a child thread");
	}

	public static void main(String[] args) {

		RunnableTask task = new RunnableTask();
		new Thread(task).start();
		new Thread(task).start();
	}
}

如上面程式碼所示,兩個執行緒共用一個task 程式碼邏輯,如果需要,可以給RunnableTask 新增引數進行任務區分。另外,RunnableTask 可以繼承其他類。但是上面介紹的兩種方式 都有一個缺點,就是任務沒有返回值。

  • 使用FutureTask 方式, 實現Callable介面的call 方法
public class CallerTask implements Callable<String> {

	@Override
	public String call() throws Exception {
		return "caller task";
	}

	public static void main(String[] args) throws ExecutionException, InterruptedException {
		// 建立非同步任務
		FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
		
		// 啟動執行緒
		new Thread(futureTask).start();
		
		// 等待任務執行完畢並返回結果
		String result = futureTask.get();
		System.out.println(result);
	}
}

如上程式碼中的CallerTask 類實現了Callable 介面的call() 方法。建立FutureTask 物件(建構函式為CallerTask 的例項),然後使用建立的FutrueTask 物件作為任務建立了一個執行緒並且啟動它, 最後通過futureTask.get() 等待任務執行完畢並返回結果。

執行緒等待與通知

Java 中的Object 類是所有類的父類,鑑於繼承機制, Java 把所有類都需要的方法放到了Object 類裡面,其中就包含通知與等待系列函式。

wait() 函式

當一個執行緒呼叫一個共享變數的wait() 方法時, 該呼叫執行緒會被阻塞掛起, 直到發生下面幾件事情之一才返回:

  1. 其他執行緒呼叫了該共享物件的notify() 或者notifyAll() 方法
  2. 其他執行緒呼叫了該執行緒的interrupt() 方法, 該執行緒丟擲InterruptedException 異常返回

需要注意的是,如果呼叫wait() 方法的執行緒沒有事先獲取該物件的監視器鎖,則呼叫wait() 方法時呼叫執行緒會丟擲IllegalMonitorStateException 異常。

執行緒通過以下方法獲取共享變數的監視器鎖:

  1. 執行synchronized 同步程式碼塊時, 使用該共享變數作為引數。
synchronized(共享變數) {
    // do something
}
  1. 呼叫該共享變數的方法,並且該方法使用了synchronized 修飾。
synchronized void method(int a, int b) {
    // do something
}

另外需要注意的是,一個執行緒可以從掛起狀態變為可以執行狀態(也就是被喚醒),即使該執行緒沒有被其他執行緒呼叫notify()、notifyAll() 方法進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒。

(虛假喚醒在應用實踐中很少發生),防患做法是不停地去測試該執行緒被喚醒的條件是否滿足,不滿足則繼續等待,也就是說在一個迴圈中呼叫wait() 方法進行防範。退出迴圈的條件是滿足了喚醒該執行緒的條件。

synchronized(obj) {
    while(條件不滿足) {
        obj.wait();
    }
}

wait(long timeout) 函式

如果一個執行緒呼叫共享物件的該方法掛起後,沒有在指定的timeout ms時間內被其他執行緒呼叫該共享變數的 notify() 或者notifyAll() 方法喚醒,那麼該函式會因為超時而返回。

wait(long timeout, int nanos) 函式

nanos 納秒,在nanos > 0 時使引數timeout 遞增1。

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
              "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

notify() 函式

一個執行緒呼叫共享物件的notify() 方法後,會隨機喚醒一個在該共享變數上呼叫wait 系列方法後被掛起的執行緒。被喚醒的變數只有在獲取到共享變數監視器鎖後才能繼續執行。

類似wait 系列方法,只有當前執行緒獲取到了共享變數的監視器鎖後,才可以呼叫共享變數的notify() 方法,否則會丟擲IllegalMonitorStateException 異常。

notifyAll() 函式

notifyAll() 方法會喚醒所有在該共享變數上由於呼叫wait 系列方法而被掛起的執行緒。

等待執行緒執行終止的join 方法

掛起呼叫執行緒,直到被呼叫執行緒結束執行,呼叫執行緒才會繼續執行。

讓執行緒睡眠的sleep 方法

當一個執行中的執行緒呼叫了Thread 的sleep 方法後,呼叫執行緒會暫時讓出指定時間的執行權,也就是在這期間不參與CPU 的排程,但是該執行緒所擁有的監視器資源,比如鎖還是持有不讓出的。指定的睡眠時間到了後該函式會正常返回,執行緒就處於就緒狀態,然後參與CPU 的排程。

如果在睡眠期間其他執行緒呼叫了該執行緒的interrupt()方法中斷了該執行緒,則該執行緒會在呼叫sleep 方法的地方丟擲InterruptedException 異常而返回。

讓出CPU 執行權的yield 方法

當一個執行緒呼叫Thread 的yield 方法時, 當前執行緒會讓出CPU 使用權,然後處於就緒狀態,執行緒排程器會從執行緒就緒佇列裡面獲取一個執行緒優先順序最高的執行緒,當然也有可能會排程到剛剛讓出CPU 的那個執行緒來獲取CPU 執行權。

執行緒中斷

Java 中的執行緒中斷是一種執行緒間的協作模式,通過設定執行緒的中斷標誌並不能直接終止該執行緒的執行,而是被中斷的執行緒根據中斷狀態自行處理。

  • void interrupt() : 中斷執行緒
  • boolean isInterrupted() : 檢測當前執行緒是否被中斷
public boolean isInterrupted() {
    // 傳遞false,說明不清除中斷標誌
    return isInterrupted(false);
}
  • boolean interrupted() : 檢測當前執行緒是否被中斷,與isInterrupted不同的是,該方法如果發現當前執行緒被中斷,會清除中斷標誌,並且該方法是static 方法,可以通過Thread 類直接呼叫。
public static boolean interrupted() {
    // 清除中斷標誌
    return currentThread().isInterrupted(true);
}

執行緒狀態與狀態轉換

執行緒狀態

Java語言定義了6種執行緒狀態,在給定的一個時刻,執行緒只能處於其中的一個狀態。這6種狀態分別是:

狀態名稱 說明
NEW 初始狀態,執行緒被構建但是還沒有呼叫start()方法
RUNNABLE 執行狀態,包括作業系統執行緒狀態中的執行(Running)和就緒(Ready),執行緒正在執行或等待作業系統為其分配執行時間。
WAITING 等待狀態,等待被其他執行緒顯式喚醒(通知或中斷)
TIME_WAITING 超時等待狀態,無須等待被其他執行緒顯式喚醒,在一定時間之後由系統自動喚醒
BLOCKED 阻塞狀態,表示執行緒阻塞於鎖
TERMINATED 終止狀態,執行緒已執行完畢

執行緒狀態轉換

執行緒上下文切換

CPU 一般是使用時間片輪轉方式讓執行緒輪詢佔用,所以當前執行緒CPU 時間片用完後,就會處於就緒狀態並讓出CPU ,等下次輪到自己的時候再執行,這就是上下文切換。(通過程式計數器記錄執行緒讓出CPU 時的執行地址,待再次分配到時間片時執行緒就從自己私有的計數器指定地址繼續執行。另外需要注意的是,如果執行的是native 方法,那麼pc 計數器記錄的是undefined 地址,只有執行的是Java 程式碼時pc 計數器記錄的才是下一條指令的地址。)

執行緒上下文切換時機有:

  • 當前執行緒的CPU 時間片使用完處於就緒狀態時
  • 當前執行緒被其他執行緒中斷時

執行緒死鎖

什麼是執行緒死鎖

死鎖是指兩個或兩個以上的執行緒在執行過程中,因爭奪資源而造成的互相等待的現象,在無外力作用的情況下,這些執行緒會一直相互等待而無法繼續執行下去。

死鎖的產生必須具備以下四個條件:

  • 互斥條件: 指執行緒對己經獲取到的資源進行排它性使用,即該資源同時只由一個執行緒佔用。
  • 請求並持有條件: 指一個執行緒己經持有了至少一個資源但又要請求己被其他執行緒佔有的資源時,當前執行緒會被阻塞但並不釋放己獲取的資源。
  • 不可剝奪條件: 指執行緒獲取到的資源在自己使用完之前不能被其他執行緒搶佔, 只有在自己使用完畢後才由自己釋放該資源。
  • 環路等待條件: 指在發生死鎖時, 必然存在一個執行緒 - 資源的環形鏈。

如何避免執行緒死鎖

要避免死鎖,只需要破壞掉至少一個構造死鎖的必要條件即可,但只有請求並持有和環路等待條件是可以被破壞的。保持資源申請的有序性可以避免死鎖。

守護執行緒與使用者執行緒

Java 中的執行緒分為兩類,分別為daemon 執行緒(守護執行緒)和user 執行緒(使用者執行緒)。在JVM 啟動時會呼叫main 函式, main 函式所在的執行緒就是一個使用者執行緒,在JVM 內部啟動了很多守護執行緒, 比如垃圾回收執行緒。

守護執行緒與使用者執行緒區別

當最後一個非守護執行緒結束時, JVM 會正常退出,而不管當前是否有守護執行緒,也就是說守護執行緒是否結束並不影響JVM 的退出。即只要有一個使用者執行緒還沒結束, 正常情況下JVM 就不會退出。

下面程式碼中演示如何建立守護執行緒:

public static void main(String[] args) {

	Thread daemonThread = new Thread(() -> {
		System.out.println("I am a daemon thread")
	});

    // 設定為守護執行緒
	daemonThread.setDaemon(true);
	daemonThread.start();
}