Java並發編程實戰(5)- 線程生命周期
在這篇文章中,我們來聊一下線程的生命周期。
概述
線程是操作系統中的一個概念,在Java中,它是實現並發程序的主要手段。
Java中的線程,本質上就是操作系統中的線程。
操作系統中的線程有「生老病死」,專業說法就是生命周期,雖然不同的開發語言對於操作系統的線程做了不同的封裝,但是對於線程生命周期來說,基本上是大同小異的。
我們在學習線程的生命周期時,只要能理解生命周期中各個節點的狀態轉換機制就可以了。
操作系統中的線程生命周期
操作系統中的線程有5種狀態,可以用下面的圖進行描述,該圖也被稱為五態模型。
線程的五種狀態描述如下:
- 初始狀態。這是指線程已經被創建,但是還不允許分配CPU執行。這個狀態屬於編程語言特有的,也就是說,這裡所謂的被創建,僅僅是在編程語言層面上被創建,在操作系統層面上,線程是還沒有被創建的。
- 可運行狀態。這是指線程可以分配CPU執行。在這個狀態下,操作系統已經創建了線程,正在等待分配CPU。
- 運行狀態。這是指CPU有空閑,操作系統將其分配給一個處於可運行狀態的線程,被分配到CPU的線程,就可以正常執行線程中的邏輯了。
- 休眠狀態。 這是指處於運行狀態的線程調用了一個阻塞API或者等待某個事件。在休眠狀態下,線程會釋放CPU使用權,處於休眠狀態的線程,是永遠沒有機會獲得CPU使用權的。 當等待的事件出現了,線程就會從休眠狀態轉換到可運行狀態,等待CPU重新分配。
- 終止狀態。這是指線程執行完成或者拋出異常。終止狀態的線程不會切換到其他任何狀態,這也意味着進行終止狀態的線程的生命周期結束了。
Java中的線程生命周期
Java中的線程生命周期,基於操作系統的線程生命周期進行了定製,它包括六種狀態:
- NEW(初始化狀態)
- RUNNABLE(可運行/運行狀態)
- BLOCKED(阻塞狀態)
- WAITING(無時限等待)
- TIMED_WAITING(有時限等待)
- TERMINATED(終止狀態)
Java中的線程生命周期可以用下圖來描述。
和操作系統線程生命周期相比,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是一個非常實用的工具,我會在後面找機會詳細的說明如何使用它和其他相關工具。