多執行緒詳解(1)——執行緒基本概念

Deziko發表於2018-01-28

0. 簡介

這個系列開始來講解 Java 多執行緒的知識,這節就先講解多執行緒的基本知識。

1. 程式與執行緒

1.1 什麼是程式?

程式就是在執行過程中的程式,就好像手機執行中的微信,QQ,這些就叫做程式。

1.2 什麼是執行緒?

執行緒就是程式的執行單元,就好像一個音樂軟體可以聽音樂,下載音樂,這些任務都是由執行緒來完成的。

1.3 程式與執行緒的關係

  • 一個程式可以擁有多個執行緒,一個執行緒必須要有一個父程式
  • 執行緒之間共享父程式的共享資源,相互之間協同完成程式所要完成的任務
  • 一個執行緒可以建立和撤銷另一個執行緒,同一個程式的多個執行緒之間可以併發執行

2. 如何建立執行緒

Java 中建立執行緒的方法有三種,以下來逐一詳細講解。

2.1 繼承 Thread 類建立執行緒

使用繼承 Thread 類建立執行緒的步驟如下:

  1. 新建一個類繼承 Thread 類,並重寫 Thread 類的 run() 方法。
  2. 建立 Thread 子類的例項。
  3. 呼叫該子類例項的 start() 方法啟動該執行緒。

程式碼舉例如下:

public class ThreadDemo extends Thread {
	
	// 1. 新建一個類繼承 Thread 類,並重寫 Thread 類的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Thread");
	}
	
	public static void main(String[] args) {
		
		// 2. 建立 Thread 子類的例項。
		ThreadDemo threadDemo = new ThreadDemo();
		// 3. 呼叫該子類例項的 start() 方法啟動該執行緒。
		threadDemo.start();
		
	}

}

複製程式碼

列印結果如下:

Hello Thread
複製程式碼

2.2 實現 Runnable 介面建立執行緒

使用實現 Runnable 介面建立執行緒步驟是:

  1. 建立一個類實現 Runnable 介面,並重寫該介面的 run() 方法。
  2. 建立該實現類的例項。
  3. 將該例項傳入 Thread(Runnable r) 構造方法中建立 Thread 例項。
  4. 呼叫該 Thread 執行緒物件的 start() 方法。

程式碼舉例如下:


public class RunnableDemo implements Runnable {

	// 1. 建立一個類實現 Runnable 介面,並重寫該介面的 run() 方法。
	@Override
	public void run() {
		System.out.println("Hello Runnable");
	}

	
	public static void main(String[] args) {
		
		// 2. 建立該實現類的例項。
		RunnableDemo runnableDemo = new RunnableDemo();
		
		// 3. 將該例項傳入 Thread(Runnable r) 構造方法中建立 Thread 例項。
		Thread thread = new Thread(runnableDemo);
		
		// 4. 呼叫該 Thread 執行緒物件的 start() 方法。
		thread.start();
		
	}
	

}

複製程式碼

列印結果如下:

Hello Runnable
複製程式碼

2.3 使用 Callable 和 FutureTask 建立執行緒

使用這種方法建立的執行緒可以獲取一個返回值,使用實現 Callable 和 FutureTask 建立執行緒步驟是:

  1. 建立一個類實現 Callable 介面,並重寫 call() 方法。
  2. 建立該 Callable 介面實現類的例項。
  3. 將 Callable 的實現類例項傳入 FutureTask(Callable callable) 構造方法中建立 FutureTask 例項。
  4. 將 FutureTask 例項傳入 Thread(Runnable r) 構造方法中建立 Thread 例項。
  5. 呼叫該 Thread 執行緒物件的 start() 方法。
  6. 呼叫 FutureTask 例項物件的 get() 方法獲取返回值。

程式碼舉例如下:

public class CallableDemo implements Callable<String> {

	// 1. 建立一個類實現 Callable 介面,並重寫 call() 方法。
	@Override
	public String call() throws Exception {
		System.out.println("CallableDemo is Running");
		return "Hello Callable";
	}
	
	public static void main(String[] args) {
		
		// 2. 建立該 Callable 介面實現類的例項。
		CallableDemo callableDemo = new CallableDemo();
		
		// 3. 將 Callable 的實現類例項傳入 FutureTask(Callable<V> callable) 構造方法中建立 FutureTask 例項。
		FutureTask<String> futureTask = new FutureTask<>(callableDemo);
		
		// 4. 將 FutureTask 例項傳入 Thread(Runnable r) 構造方法中建立 Thread 例項。
		Thread thread = new Thread(futureTask);
		
		// 5. 呼叫該 Thread 執行緒物件的 start() 方法。
		thread.start();
		
		// 6. 呼叫 FutureTask 例項物件的 get() 方法獲取返回值。
		try {
			System.out.println(futureTask.get());
		} catch (Exception e) {
			e.printStackTrace();
		}
		
	}

}
複製程式碼

列印結果如下:

CallableDemo is Running
Hello Callable
複製程式碼

3. 執行緒的生命週期

當一個執行緒開啟之後,它會遵循一定的生命週期,它要經過新建,就緒,執行,阻塞和死亡這五種狀態,理解執行緒的生命週期有助於理解後面的相關的執行緒知識。

3.1 新建狀態

這個狀態的意思就是執行緒剛剛被建立出來,這時候的執行緒並沒有任何執行緒的動態特徵。

3.2 就緒狀態

當執行緒物件呼叫 start() 方法後,該執行緒就處於就緒狀態。處於這個狀態中的執行緒並沒有開始執行,只是表示這個執行緒可以執行了。

3.3 執行狀態

處於就緒狀態的執行緒獲得了 CPU 後,開始執行 run() 方法,這個執行緒就處於執行狀態。

3.4 阻塞狀態

當執行緒被暫停後,這個執行緒就處於阻塞狀態。

3.5 死亡狀態

當執行緒被停止後,這個執行緒就處於死亡狀態。

其實掌握多執行緒最主要的就是要熟悉控制執行緒的狀態,讓各個執行緒能更好的為我們的服務,下面就來講解控制執行緒的方法。

4. 控制執行緒

4.1 sleep()

4.1.1 執行緒生命週期的變化

sleep()

4.1.2 方法預覽

public static native void sleep(long millis)
public static void sleep(long millis, int nanos)
複製程式碼

該方法的意思就是讓正在執行狀態的執行緒到阻塞狀態,而這個時間就是執行緒處於阻塞狀態的時間。millis 是毫秒的意思,nanos 是毫微秒。

4.1.3 程式碼舉例

public class SleepDemo {
	
	public static void main(String[] args) throws Exception {
		
		for(int i = 0; i < 10; i++) {
			System.out.println("Hello Thread Sleep");
			Thread.sleep(1000);
		}
		
	}

}
複製程式碼

以上程式碼執行後每隔一秒就輸出 Hello Thread Sleep。

4.2 執行緒優先順序

4.2.1 方法預覽

public final void setPriority(int newPriority)
public final int getPriority()
複製程式碼

從方法名就可以知道,以上兩個方法分別就是設定和獲得優先順序的。值得注意的是優先順序是在 1~10 範圍內,也可以使用以下三個靜態變數設定:

  • MAX_PRIORITY:優先順序為 10
  • NORM_PRIORITY:優先順序為 5
  • MIN_PRIORITY:優先順序為 1

4.3 yield()

4.3.1 執行緒生命週期的變化

yield()

4.3.2 方法預覽

public static native void yield();
複製程式碼

這個方法的意思就是讓正在執行的執行緒回到就緒狀態,並不會阻塞執行緒。可能會發生一種情況就是,該執行緒呼叫了 yield() 方法後,執行緒排程器又會繼續呼叫該執行緒。 這個方法要注意的是它只會讓步給比它優先順序高的或者和它優先順序相同並處在就緒狀態的執行緒。

4.3.3 程式碼舉例

public class YieldDemo extends Thread {

	@Override
	public void run() {

		for (int i = 0; i < 50; i++) {
			System.out.println(getName() + " " + i);

			if (i == 20) {
				Thread.yield();
			}
		}

	}

	public static void main(String[] args) {

		YieldDemo yieldDemo1 = new YieldDemo();
		YieldDemo yieldDemo2 = new YieldDemo();

		yieldDemo1.start();
		yieldDemo2.start();

	}

}

複製程式碼

程式碼輸出結果:

Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-1 5
Thread-1 6
Thread-1 7
Thread-1 8
Thread-1 9
Thread-1 10
Thread-1 11
Thread-1 12
Thread-1 13
Thread-1 14
Thread-0 0
Thread-0 1
Thread-0 2
Thread-1 15
Thread-0 3
Thread-1 16
Thread-1 17
Thread-1 18
Thread-1 19
Thread-1 20
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-1 21
Thread-0 8
Thread-1 22
Thread-0 9
Thread-1 23
Thread-0 10
Thread-0 11
Thread-0 12
Thread-1 24
Thread-1 25
Thread-0 13
Thread-1 26
Thread-1 27
Thread-0 14
Thread-0 15
Thread-1 28
Thread-0 16
Thread-0 17
Thread-0 18
Thread-1 29
Thread-0 19
Thread-0 20
Thread-1 30
Thread-1 31
Thread-0 21
Thread-1 32
Thread-1 33
Thread-1 34
Thread-1 35
Thread-1 36
Thread-1 37
Thread-1 38
Thread-1 39
Thread-1 40
Thread-1 41
Thread-1 42
Thread-1 43
Thread-1 44
Thread-1 45
Thread-1 46
Thread-1 47
Thread-1 48
Thread-1 49
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
Thread-0 34
Thread-0 35
Thread-0 36
Thread-0 37
Thread-0 38
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49
複製程式碼

從列印結果就可以看到列印 Thread-1 20的時候,下一個執行的就是 Thread-0 4。列印 Thread-20 的時候,下一個執行的就是 Thread-1 30。 但是要說明的是,不是每次的列印結果都是一樣的,因為前面說過執行緒呼叫 yield() 方法後,執行緒排程器有可能會繼續啟動該執行緒。

4.4 join()

4.4.1 執行緒生命週期的變化

join()

4.4.2 方法預覽

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException
複製程式碼

這個方法其實要有兩個執行緒,也就是一個執行緒的執行緒執行體中有另一個執行緒在呼叫 join() 方法。舉個例子,Thread1 的 run() 方法執行體中有 Thread2 在呼叫 join(),這時候 Thread1 就會被阻塞,必須要等到 Thread2 的執行緒執行完成或者 join() 方法的時間到後才會繼續執行。

4.4.3 join() 程式碼舉例


public class JoinDemo extends Thread {
	
	@Override
	public void run() {
		for(int i = 0; i < 50; i++) {
			
			System.out.println(getName() + " " + i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		JoinDemo joinDemo = new JoinDemo();
		
		for(int i = 0; i < 50; i++) {
			
			if(i == 20) {
				
				joinDemo.start();
				joinDemo.join();
				
			}
			
			System.out.println(Thread.currentThread().getName() + " " + i);
			
		}
		
		
	}

}

複製程式碼

程式碼輸出的結果:

main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
Thread-0 34
Thread-0 35
Thread-0 36
Thread-0 37
Thread-0 38
Thread-0 39
Thread-0 40
Thread-0 41
Thread-0 42
Thread-0 43
Thread-0 44
Thread-0 45
Thread-0 46
Thread-0 47
Thread-0 48
Thread-0 49
main 21
main 22
main 23
main 24
main 25
main 26
main 27
main 28
main 29
main 30
main 31
main 32
main 33
main 34
main 35
main 36
main 37
main 38
main 39
main 40
main 41
main 42
main 43
main 44
main 45
main 46
main 47
main 48
main 49

複製程式碼

以上的程式碼其實一個兩個執行緒,一個是 Thread-0,另一個就是 main,main 就是主執行緒的意思。從列印結果可以看到,主執行緒執行到 main 20 的時候,就開始執行 Thread-0 0,直到 Thread-0 執行完畢,main 才繼續執行。

4.4.4 join(long millis) 程式碼舉例

public class JoinDemo extends Thread {
	
	@Override
	public void run() {
		for(int i = 0; i < 50; i++) {
			
			System.out.println(getName() + " " + i);
		}
	}
	
	public static void main(String[] args) throws Exception {
		
		JoinDemo joinDemo = new JoinDemo();
		
		for(int i = 0; i < 50; i++) {
			
			System.out.println(Thread.currentThread().getName() + " " + i);
			
			if(i == 20) {
				
				joinDemo.start();
				joinDemo.join(1);
				
			}
			
		}
		
		
	}

}
複製程式碼

列印結果:

main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
Thread-0 6
Thread-0 7
Thread-0 8
Thread-0 9
Thread-0 10
Thread-0 11
Thread-0 12
Thread-0 13
Thread-0 14
Thread-0 15
Thread-0 16
Thread-0 17
Thread-0 18
Thread-0 19
Thread-0 20
Thread-0 21
Thread-0 22
Thread-0 23
Thread-0 24
Thread-0 25
Thread-0 26
Thread-0 27
Thread-0 28
Thread-0 29
Thread-0 30
Thread-0 31
Thread-0 32
Thread-0 33
main 21
main 22
Thread-0 34
main 23
Thread-0 35
Thread-0 36
main 24
main 25
main 26
Thread-0 37
main 27
Thread-0 38
main 28
Thread-0 39
main 29
Thread-0 40
main 30
Thread-0 41
main 31
Thread-0 42
main 32
main 33
main 34
main 35
Thread-0 43
Thread-0 44
Thread-0 45
main 36
Thread-0 46
main 37
main 38
main 39
main 40
main 41
main 42
main 43
main 44
main 45
main 46
main 47
main 48
main 49
Thread-0 47
Thread-0 48
Thread-0 49
複製程式碼

其實這個的程式碼和 4.4.3 節的程式碼基本一樣,就是將 join() 改成 join(1) ,可以看到 main 並沒有等到 Thread-0 執行完就開始重新執行了。

4.5 後臺執行緒

4.5.1 方法預覽

public final void setDaemon(boolean on)
public final boolean isDaemon()
複製程式碼

這個方法就是將執行緒設定為後臺執行緒,後臺執行緒的特點就是當前臺執行緒全部執行結束後,後臺執行緒就會隨之結束。此方法設定為 true 時,就是將執行緒設定為後臺執行緒。 而 isDaemon() 就是返回此執行緒是否為後臺執行緒。

4.5.2 程式碼舉例


public class DaemonDemo extends Thread {

	@Override
	public void run() {
		for(int i = 0; i < 100; i++) {
			System.out.println(getName() + " "+ isDaemon() + " " + i);
		}
	}
	
	public static void main(String[] args) {
		
		DaemonDemo daemonDemo = new DaemonDemo();
		
		daemonDemo.setDaemon(true);
		
		daemonDemo.start();
		
		for(int i = 0; i < 10; i++) {
			System.out.println(Thread.currentThread().getName() + " " + i);
		}
		
	}
	
}

複製程式碼

列印結果:

main 0
main 1
main 2
main 3
main 4
Thread-0 true 0
main 5
main 6
main 7
main 8
main 9
Thread-0 true 1
Thread-0 true 2
Thread-0 true 3
Thread-0 true 4
Thread-0 true 5
Thread-0 true 6
Thread-0 true 7
Thread-0 true 8
Thread-0 true 9
Thread-0 true 10
Thread-0 true 11
複製程式碼

從列印結果可以看到 main 執行完後,Thread-0 沒有執行完畢就結束了。

5. 一些注意點

5.1 sleep() 和 yield() 區別

作用處 sleep() yield()
給其他執行緒執行機會 會給其他執行緒執行機會,不會理會其他執行緒的優先順序 只會給優先順序相同,或者優先順序更高的執行緒執行機會
影響當前執行緒的狀態 從阻塞到就緒狀態 直接進入就緒狀態
異常 需要丟擲 InterruptedException 不需要丟擲任何異常

多執行緒系列文章:

多執行緒詳解(2)——不得不知的幾個概念

相關文章