天天看點

Java并發程式設計實戰(5)- 線程生命周期概述作業系統中的線程生命周期Java中的線程生命周期Java線程狀态轉換使用jstack檢視多線程狀态

在這篇文章中,我們來聊一下線程的生命周期。

文章目錄

  • 概述
  • 作業系統中的線程生命周期
  • Java中的線程生命周期
  • Java線程狀态轉換
    • 運作狀态和阻塞狀态之間的轉換
    • 運作狀态和無時限等待狀态的切換
    • 運作狀态和有時限等待狀态的切換
    • 初始化狀态和運作狀态的切換
    • 運作狀态和終止狀态的切換
      • 手動終止線程
  • 使用jstack檢視多線程狀态

概述

線程是作業系統中的一個概念,在Java中,它是實作并發程式的主要手段。

Java中的線程,本質上就是作業系統中的線程。

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

我們在學習線程的生命周期時,隻要能了解生命周期中各個節點的狀态轉換機制就可以了。

作業系統中的線程生命周期

作業系統中的線程有5種狀态,可以用下面的圖進行描述,該圖也被稱為五态模型。

Java并發程式設計實戰(5)- 線程生命周期概述作業系統中的線程生命周期Java中的線程生命周期Java線程狀态轉換使用jstack檢視多線程狀态

線程的五種狀态描述如下:

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

Java中的線程生命周期

Java中的線程生命周期,基于作業系統的線程生命周期進行了定制,它包括六種狀态:

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

Java中的線程生命周期可以用下圖來描述。

Java并發程式設計實戰(5)- 線程生命周期概述作業系統中的線程生命周期Java中的線程生命周期Java線程狀态轉換使用jstack檢視多線程狀态

和作業系統線程生命周期相比,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是一個非常實用的工具,我會在後面找機會詳細的說明如何使用它已經其他相關工具。