In this article, we’ll talk about the life cycle of threads.
An overview of the
Threads are a concept in operating systems, and in Java, it is the primary means of implementing concurrent programs.
Threads in Java are essentially threads in the operating system.
Operating system threads have “life and death”, the professional term is life cycle, although different development languages for operating system threads do different packaging, but for the thread life cycle, is basically the same.
When we learn about the life cycle of a thread, we just need to understand the state transition mechanism of each node in the life cycle.
Thread life cycle in operating system
Threads in an operating system have five states, which can be described in the following diagram, also known as the five-state model.
The five states of a thread are described as follows:
- Initial state. This is when a thread has been created but is not yet allowed to allocate CPU execution. This state is specific to the programming language, that is, it is created only at the programming language level; at the operating system level, threads are not yet created.
- Running status. This means that threads can allocate CPU execution. In this state, the operating system has created threads and is waiting to allocate cpus.
- Running status. This means that the CPU is free and the operating system allocates it to a thread that is in a runnable state. The thread that is allocated to the CPU can normally execute the logic in the thread.
- A dormant state. This is when a running thread calls a blocking API or waits for an event. In hibernation, threads release CPU usage,A thread in hibernation never gets a chance to use the CPU.When a waiting event occurs, the thread transitions from sleep to runnable, waiting for CPU reallocation.
- Termination status. This is when the thread completes execution or throws an exception. A terminated thread does not switch to any other state, which means that the life cycle of the terminated thread is over.
Thread life cycle in Java
Thread life cycle in Java, customized for the operating system thread life cycle, consists of six states:
- NEW (initialization state)
- RUNNABLE (RUNNABLE/running state)
- The virtual drive is BLOCKED.
- WAITING (WAITING without time limit)
- TIMED_WAITING
- TERMINATED state
The thread life cycle in Java can be described in the following figure.
There are two major changes to the thread lifecycle in Java compared to the operating system thread lifecycle:
- Runnable and running states are merged in Java threads.
- The sleep states in Java threads are refined into blocking, time-limited wait, and time-limited wait.
Java thread state transitions
Blocking, timeless wait, and timebound wait in Java thread state can be understood as the three causes of a thread’s sleep state. Let’s look at how these states transition from one to the other.
Transitions between running and blocking states
In Java, this state transition occurs in only one case: a thread waits for a synchronized implicit lock. Synchronized modified methods and blocks of code allow only one thread to execute at a time, while the other threads must wait. In this case, the waiting thread transitions from the running state to the blocked state, and the waiting thread transitions from the blocked state to the running state when the synchronized lock is acquired.
Does a thread switch to a blocking state when it calls a blocking API?
At the operating system level, threads switch to sleep, but at the JVM level, Java threads do not switch, which means they are still running. The JVM does not care about the operating system scheduling-related state. From the JVM’s point of view, waiting for CPU usage is no different from waiting for I/O; both are waiting for a resource and therefore both are runnable/running.
When we talk about Java calling blocking apis, threads are blocked. We mean operating system thread state, not Java thread state.
Switching between the running state and the timeless wait state
The following three conditions trigger a switch between the running state and the timeless wait state.
- The thread that obtains a synchronized lock calls object.wait () without arguments.
- Call thread.join () without arguments.
- Call the locksupport.park () method.
Switching between running state and time-bound wait state
The main difference between time-bound and time-free wait is that a timeout parameter is added to the trigger condition.
The following five conditions trigger a switch between the running state and the time-bound wait state.
- Call thread.sleep (Long millis) with timeout.
- Call object. wait(long timeout) with the timeout parameter to the thread that acquiresynchronized lock.
- Call thread.join (Long millis) with the timeout parameter.
- Call the locaksupport. parkNanos(Object Blocker, Long Deadline) method with the timeout parameter.
- Call the locksupport. parkUntil(long deadlinie) method with the timeout parameter.
Initialization state and running state switch
Java’s newly created Thread object is initialized. There are two ways to create a Thread:
- Thread class inheritance
- Implement the Runnable interface
A thread in the initialized state is not scheduled by the operating system and therefore will not be executed. After calling the start() method of the thread object, the thread switches from the initialization state to the running state.
Switching between running state and termination state
A thread automatically switches to the terminated state in two cases:
- The run() method completes normally
- An exception is thrown in the run() method
Manually terminate a thread
There are two ways to terminate a thread:
- Call the stop() method
- Call the interrupt() method
We do not recommend using the stop() method, which has been marked Deprecated in the JDK. We recommend using the interrupt() method to terminate a thread.
The difference between the stop() method and interrupt() method:
- The stop() method kills the thread without giving it a chance to catch its breath. If the thread holds the lock, the lock is not released and no other thread can acquire it.
- The interrupt() method simply notifies the thread, which has a chance to do something later, or which it can ignore.
A thread whose interrupt() method is called receives notification in two ways:
- Exception: a thread in a time-bound or no-time-bound wait state that returns to the running state after the interrupt() method is called but throws InterruptedException.
- Active monitoring means that a thread can call isInterrupted() to see if it has been interrupted.
Use jStack to view multithreading status
After looking at the states in the Java thread life cycle and switching between states, let’s use JStack to look at the state of a real running thread.
Let’s take a deadlocked program as an example of how to use JStack.
When explaining mutex and deadlocks, we have written some deadlock examples as follows.
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()));
}
}
}
}
}
Copy the code
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."); }Copy the code
The above code ends up in a deadlock state due to resource contention, so let’s look at how to use JStack to get the details.
(base) ➜ ~ jstack -l 63044
Copy the code
Please note that the above 63044 is a running PID. The PID generated by running the program multiple times is not the same.
Jstack returns the following result.
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.
Copy the code
The thread states are RUNNABLE, WAITING, and BLOCKED, for example:
"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: - NoneCopy the code
Here is information about deadlocks:
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"
Copy the code
From the above description, we can clearly see that two threads are waiting for each other to hold the lock object.
Jstack is a very useful tool, and I’ll explain how to use it and other related tools in more detail later.