Java併發程式設計實戰(5)- 執行緒生命週期

李潘 發表於 2021-01-15

在這篇文章中,我們來聊一下執行緒的生命週期。

概述

執行緒是作業系統中的一個概念,在Java中,它是實現併發程式的主要手段。

Java中的執行緒,本質上就是作業系統中的執行緒。

作業系統中的執行緒有“生老病死”,專業說法就是生命週期,雖然不同的開發語言對於作業系統的執行緒做了不同的封裝,但是對於執行緒生命週期來說,基本上是大同小異的。

我們在學習執行緒的生命週期時,只要能理解生命週期中各個節點的狀態轉換機制就可以了。

作業系統中的執行緒生命週期

作業系統中的執行緒有5種狀態,可以用下面的圖進行描述,該圖也被稱為五態模型。
Java併發程式設計實戰(5)- 執行緒生命週期

執行緒的五種狀態描述如下:

  • 初始狀態。這是指執行緒已經被建立,但是還不允許分配CPU執行。這個狀態屬於程式語言特有的,也就是說,這裡所謂的被建立,僅僅是在程式語言層面上被建立,在作業系統層面上,執行緒是還沒有被建立的。
  • 可執行狀態。這是指執行緒可以分配CPU執行。在這個狀態下,作業系統已經建立了執行緒,正在等待分配CPU。
  • 執行狀態。這是指CPU有空閒,作業系統將其分配給一個處於可執行狀態的執行緒,被分配到CPU的執行緒,就可以正常執行執行緒中的邏輯了。
  • 休眠狀態。 這是指處於執行狀態的執行緒呼叫了一個阻塞API或者等待某個事件。在休眠狀態下,執行緒會釋放CPU使用權,處於休眠狀態的執行緒,是永遠沒有機會獲得CPU使用權的。 當等待的事件出現了,執行緒就會從休眠狀態轉換到可執行狀態,等待CPU重新分配。
  • 終止狀態。這是指執行緒執行完成或者丟擲異常。終止狀態的執行緒不會切換到其他任何狀態,這也意味著進行終止狀態的執行緒的生命週期結束了。

Java中的執行緒生命週期

Java中的執行緒生命週期,基於作業系統的執行緒生命週期進行了定製,它包括六種狀態:

  • NEW(初始化狀態)
  • RUNNABLE(可執行/執行狀態)
  • BLOCKED(阻塞狀態)
  • WAITING(無時限等待)
  • TIMED_WAITING(有時限等待)
  • TERMINATED(終止狀態)

Java中的執行緒生命週期可以用下圖來描述。
Java併發程式設計實戰(5)- 執行緒生命週期

和作業系統執行緒生命週期相比,Java中的執行緒生命週期主要有以下2個改動:

  • Java執行緒中對可執行狀態和執行狀態進行了合併。
  • Java執行緒中的休眠狀態被細化為:阻塞狀態、無時限等待和有時限等待。

Java執行緒狀態轉換

Java執行緒狀態中的阻塞、無時限等待和有時限等待可以理解為執行緒導致休眠狀態的三種原因,我們來看一下這些狀態之間是怎麼轉換的。

執行狀態和阻塞狀態之間的轉換

在Java中,只有一種情況會出現這種狀態轉換:執行緒等待synchronized隱式鎖。synchronized修飾的方法、程式碼塊同一時刻只允許一個執行緒執行,其他執行緒只能等待,在這種情況下,等待的執行緒會從執行狀態轉換到阻塞狀態,而當等待的執行緒獲得synchronized鎖後,狀態會從阻塞狀態轉換為執行狀態。

執行緒呼叫阻塞式API時,會切換到阻塞狀態嗎?

在作業系統層面,執行緒是會切換到休眠狀態的,但是在JVM層面,Java執行緒的狀態不會切換,也就說Java執行緒依然是執行狀態。JVM不關心作業系統排程相關的狀態。在JVM看來,等待CPU使用權和等待I/O沒有區別,都是在等待某個資源,所以都屬於可執行/執行狀態。

我們平時說的Java呼叫阻塞式API時,執行緒會被阻塞,我們指的是作業系統執行緒狀態,而不是Java執行緒狀態,這一點需要分清楚。

執行狀態和無時限等待狀態的切換

以下三種情況會觸發執行狀態和無時限等待狀態的切換。

  • 獲得synchronized鎖的執行緒,呼叫了無引數的Object.wait()方法。
  • 呼叫無引數的Thread.join()方法。
  • 呼叫LockSupport.park()方法。

執行狀態和有時限等待狀態的切換

有時限等待和無時限等待的主要區別,在於觸發條件中新增了超時引數。

以下五種情況會觸發執行狀態和有時限等待狀態的切換。

  • 呼叫帶超時引數的Thread.sleep(long millis)方法。
  • 獲得synchronized鎖的執行緒,呼叫帶超時引數的Object.wait(long timeout)方法。
  • 呼叫帶超時引數的Thread.join(long millis)方法。
  • 呼叫帶超時引數的LocakSupport.parkNanos(Object blocker, long deadline)方法。
  • 呼叫帶超時引數的LockSupport.parkUntil(long deadlinie)方法。

初始化狀態和執行狀態的切換

Java剛建立出來的Thread物件就是初始化狀態,有兩種可以建立執行緒的方法:

  • 繼承Thread類
  • 實現Runnable介面

初始化狀態的執行緒,並不會被作業系統排程,因此不會被執行。在呼叫執行緒物件的start()方法後,執行緒就會從初始化狀態切換到執行狀態。

執行狀態和終止狀態的切換

執行緒在以下兩種情況時會自動切換到終止狀態:

  • 正常執行完run()方法
  • run()方法中丟擲異常

手動終止執行緒

我們有2種方法終止執行緒:

  • 呼叫stop()方法
  • 呼叫interrupt()方法

我們不推薦使用stop()方法,在JDK中,它已經被標記為Deprecated。我們推薦使用interrupt()方法來終止執行緒。

stop()方法和interrupt()方法的區別:

  • stop()方法會直接殺死執行緒,不給執行緒喘息的機會,如果此時執行緒持有鎖,那麼這個鎖不會被釋放,其他執行緒也沒有辦法獲取這個鎖。
  • interrupt()方法只是通知該執行緒,執行緒有機會執行一些後續操作,同時也可以無視這個通知。

被呼叫了interrupt()方法的執行緒,有以下2種方式接收通知:

  • 異常,處於有時限等待或者無時限等待狀態的執行緒, 在被呼叫interrupt()方法後,執行緒會返回執行狀態,但同時會丟擲InterruptedException。
  • 主動監測,執行緒可以呼叫isInterrupted()方法,來判斷自己是不是被中斷了。

使用jstack檢視多執行緒狀態

在檢視了Java執行緒生命週期中的狀態以及狀態之間的切換後,我們來使用jstack來檢視一下真實執行的執行緒的狀態。

我們以一個死鎖的程式為例,來說明如何使用jstack。

我們在解釋互斥鎖和死鎖的時候,寫了一些死鎖示例,程式碼如下。

public class BankTransferDemo {
	
	public void transfer(BankAccount sourceAccount, BankAccount targetAccount, double amount) {
		synchronized(sourceAccount) {
			synchronized(targetAccount) {
				if (sourceAccount.getBalance() > amount) {
					System.out.println("Start transfer.");
					System.out.println(String.format("Before transfer, source balance:%s, target balance:%s", sourceAccount.getBalance(), targetAccount.getBalance()));
					sourceAccount.setBalance(sourceAccount.getBalance() - amount);
					targetAccount.setBalance(targetAccount.getBalance() + amount);
					System.out.println(String.format("After transfer, source balance:%s, target balance:%s", sourceAccount.getBalance(), targetAccount.getBalance()));
				}
			}
		}
	}
}
	public static void main(String[] args) throws InterruptedException {
		BankAccount sourceAccount = new BankAccount();
		sourceAccount.setId(1);
		sourceAccount.setBalance(50000);
		
		BankAccount targetAccount = new BankAccount();
		targetAccount.setId(2);
		targetAccount.setBalance(20000);
		
		BankTransferDemo obj = new BankTransferDemo();
		
		Thread t1 = new Thread(() ->{
			for (int i = 0; i < 10000; i++) {
				obj.transfer(sourceAccount, targetAccount, 1);
			}
		});
		
		Thread t2 = new Thread(() ->{
			for (int i = 0; i < 10000; i++) {
				obj.transfer(targetAccount, sourceAccount, 1);
			}
		});
		
		t1.start();
		t2.start();
		
		t1.join();
		t2.join();
		
		System.out.println("Finished.");
	}

上述程式碼在執行過程中,因為資源爭搶的原因,最後會進入死鎖狀態,下面我們來看一下如何使用jstack來獲取具體資訊。

(base) ➜  ~ jstack -l 63044

請注意上述的63044是執行的pid,執行程式多次產生的pid是不一樣的。

jstack的返回結果如下。

2021-01-15 19:56:28
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.251-b08 mixed mode):

"RMI TCP Accept-0" #14 daemon prio=9 os_prio=31 tid=0x00007fb1d80b6000 nid=0x5803 runnable [0x00007000059d8000]
   java.lang.Thread.State: RUNNABLE
	at java.net.PlainSocketImpl.socketAccept(Native Method)
	at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
	at java.net.ServerSocket.implAccept(ServerSocket.java:545)
	at java.net.ServerSocket.accept(ServerSocket.java:513)
	at sun.management.jmxremote.LocalRMIServerSocketFactory$1.accept(LocalRMIServerSocketFactory.java:52)
	at sun.rmi.transport.tcp.TCPTransport$AcceptLoop.executeAcceptLoop(TCPTransport.java:405)
	at sun.rmi.transport.tcp.TCPTransport$AcceptLoop.run(TCPTransport.java:377)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

"Attach Listener" #12 daemon prio=9 os_prio=31 tid=0x00007fb1db03d800 nid=0x3617 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"Thread-1" #11 prio=5 os_prio=31 tid=0x00007fb1db04e800 nid=0xa603 waiting for monitor entry [0x00007000057d2000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.concurrency.demo.BankTransferDemo.transfer(BankTransferDemo.java:8)
	- waiting to lock <0x000000076ab76ef0> (a com.concurrency.demo.BankAccount)
	- locked <0x000000076ab76f10> (a com.concurrency.demo.BankAccount)
	at com.concurrency.demo.BankTransferDemo.lambda$1(BankTransferDemo.java:38)
	at com.concurrency.demo.BankTransferDemo$$Lambda$2/1044036744.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fb1d896e000 nid=0xa703 waiting for monitor entry [0x00007000056cf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.concurrency.demo.BankTransferDemo.transfer(BankTransferDemo.java:8)
	- waiting to lock <0x000000076ab76f10> (a com.concurrency.demo.BankAccount)
	- locked <0x000000076ab76ef0> (a com.concurrency.demo.BankAccount)
	at com.concurrency.demo.BankTransferDemo.lambda$0(BankTransferDemo.java:32)
	at com.concurrency.demo.BankTransferDemo$$Lambda$1/135721597.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007fb1de809000 nid=0x5503 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C1 CompilerThread3" #8 daemon prio=9 os_prio=31 tid=0x00007fb1df80a800 nid=0x3b03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C2 CompilerThread2" #7 daemon prio=9 os_prio=31 tid=0x00007fb1df80a000 nid=0x3a03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C2 CompilerThread1" #6 daemon prio=9 os_prio=31 tid=0x00007fb1df809000 nid=0x3e03 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"C2 CompilerThread0" #5 daemon prio=9 os_prio=31 tid=0x00007fb1df008800 nid=0x3803 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fb1de808800 nid=0x4103 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

   Locked ownable synchronizers:
	- None

"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007fb1d8810800 nid=0x3203 in Object.wait() [0x0000700004db1000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
	- locked <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

   Locked ownable synchronizers:
	- None

"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007fb1d900b000 nid=0x3103 in Object.wait() [0x0000700004cae000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

   Locked ownable synchronizers:
	- None

"main" #1 prio=5 os_prio=31 tid=0x00007fb1db809000 nid=0x1003 in Object.wait() [0x000070000408a000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x000000076ab770c0> (a java.lang.Thread)
	at java.lang.Thread.join(Thread.java:1252)
	- locked <0x000000076ab770c0> (a java.lang.Thread)
	at java.lang.Thread.join(Thread.java:1326)
	at com.concurrency.demo.BankTransferDemo.main(BankTransferDemo.java:45)

   Locked ownable synchronizers:
	- None

"VM Thread" os_prio=31 tid=0x00007fb1db821000 nid=0x4c03 runnable

"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007fb1db809800 nid=0x1f07 runnable

"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007fb1d8008800 nid=0x1b03 runnable

"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007fb1db009000 nid=0x1d03 runnable

"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007fb1db009800 nid=0x2a03 runnable

"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x00007fb1db00a000 nid=0x2c03 runnable

"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x00007fb1db00a800 nid=0x2d03 runnable

"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x00007fb1db80a000 nid=0x5203 runnable

"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x00007fb1db00b800 nid=0x5003 runnable

"GC task thread#8 (ParallelGC)" os_prio=31 tid=0x00007fb1db00c000 nid=0x4f03 runnable

"GC task thread#9 (ParallelGC)" os_prio=31 tid=0x00007fb1d900a800 nid=0x4d03 runnable

"VM Periodic Task Thread" os_prio=31 tid=0x00007fb1d8028800 nid=0xa803 waiting on condition

JNI global references: 333


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fb1db8270a8 (object 0x000000076ab76ef0, a com.concurrency.demo.BankAccount),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fb1db827158 (object 0x000000076ab76f10, a com.concurrency.demo.BankAccount),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at com.concurrency.demo.BankTransferDemo.transfer(BankTransferDemo.java:8)
	- waiting to lock <0x000000076ab76ef0> (a com.concurrency.demo.BankAccount)
	- locked <0x000000076ab76f10> (a com.concurrency.demo.BankAccount)
	at com.concurrency.demo.BankTransferDemo.lambda$1(BankTransferDemo.java:38)
	at com.concurrency.demo.BankTransferDemo$$Lambda$2/1044036744.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at com.concurrency.demo.BankTransferDemo.transfer(BankTransferDemo.java:8)
	- waiting to lock <0x000000076ab76f10> (a com.concurrency.demo.BankAccount)
	- locked <0x000000076ab76ef0> (a com.concurrency.demo.BankAccount)
	at com.concurrency.demo.BankTransferDemo.lambda$0(BankTransferDemo.java:32)
	at com.concurrency.demo.BankTransferDemo$$Lambda$1/135721597.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

我們從中可以看到執行緒的狀態有RUNNABLE,WAITING,BLOCKED,例如:

"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fb1d896e000 nid=0xa703 waiting for monitor entry [0x00007000056cf000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at com.concurrency.demo.BankTransferDemo.transfer(BankTransferDemo.java:8)
	- waiting to lock <0x000000076ab76f10> (a com.concurrency.demo.BankAccount)
	- locked <0x000000076ab76ef0> (a com.concurrency.demo.BankAccount)
	at com.concurrency.demo.BankTransferDemo.lambda$0(BankTransferDemo.java:32)
	at com.concurrency.demo.BankTransferDemo$$Lambda$1/135721597.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

   Locked ownable synchronizers:
	- None

下面是死鎖的相關資訊:

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007fb1db8270a8 (object 0x000000076ab76ef0, a com.concurrency.demo.BankAccount),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007fb1db827158 (object 0x000000076ab76f10, a com.concurrency.demo.BankAccount),
  which is held by "Thread-1"

從上面的描述中,我們可以清楚的看到2個執行緒在互相等待對方持有的鎖物件。

jstack是一個非常實用的工具,我會在後面找機會詳細的說明如何使用它和其他相關工具。