多線程詳解(1)——線程基本概念

0. 簡介

這個系列開始來說解 Java 多線程的知識,這節就先講解多線程的基本知識。bash

1. 進程與線程

1.1 什麼是進程?

進程就是在運行過程當中的程序,就好像手機運行中的微信,QQ,這些就叫作進程。微信

1.2 什麼是線程?

線程就是進程的執行單元,就好像一個音樂軟件能夠聽音樂,下載音樂,這些任務都是由線程來完成的。多線程

1.3 進程與線程的關係

  • 一個進程能夠擁有多個線程,一個線程必需要有一個父進程
  • 線程之間共享父進程的共享資源,相互之間協同完成進程所要完成的任務
  • 一個線程能夠建立和撤銷另外一個線程,同一個進程的多個線程之間能夠併發執行

2. 如何建立線程

Java 中建立線程的方法有三種,如下來逐一詳細講解。併發

2.1 繼承 Thread 類建立線程

使用繼承 Thread 類建立線程的步驟以下:ide

  1. 新建一個類繼承 Thread 類,並重寫 Thread 類的 run() 方法。
  2. 建立 Thread 子類的實例。
  3. 調用該子類實例的 start() 方法啓動該線程。

代碼舉例以下:post

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();
		
	}

}

複製代碼

打印結果以下:ui

Hello Thread
複製代碼

2.2 實現 Runnable 接口建立線程

使用實現 Runnable 接口建立線程步驟是:spa

  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();
		
	}
	

}

複製代碼

打印結果以下:code

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)——不得不知的幾個概念

相關文章
相關標籤/搜索