多线程基础 - 线程状态的转换
2024-10-28 08:52:07 # Technical # JavaConcurrency

线程状态的实质

线程状态实质上是一个变量的值而已

Thread 类中的一个变量 threadStatus

为方便理解,Thread 中还有一个内部枚举类 State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
* A thread state. A thread can be in one of the following states:
* <ul>
* <li>{@link #NEW}<br>
* A thread that has not yet started is in this state.
* </li>
* <li>{@link #RUNNABLE}<br>
* A thread executing in the Java virtual machine is in this state.
* </li>
* <li>{@link #BLOCKED}<br>
* A thread that is blocked waiting for a monitor lock
* is in this state.
* </li>
* <li>{@link #WAITING}<br>
* A thread that is waiting indefinitely for another thread to
* perform a particular action is in this state.
* </li>
* <li>{@link #TIMED_WAITING}<br>
* A thread that is waiting for another thread to perform an action
* for up to a specified waiting time is in this state.
* </li>
* <li>{@link #TERMINATED}<br>
* A thread that has exited is in this state.
* </li>
* </ul>
*
* <p>
* A thread can be in only one state at a given point in time.
* These states are virtual machine states which do not reflect
* any operating system thread states.
*
* @since 1.5
* @see #getState
*/
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,

/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,

/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,

/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,

/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,

/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}

(看这么长的注释就可见其不简单)

NEW

一切的起点,要从把一个 Thread 类的对象创建出来,开始说起

1
Thread t = new Thread();

也可以使用含参构造

1
Thread t = new Thread(runnable, threadName);

还可以 new 一个继承了 Thread 的子类

1
Thread t = new SubThread();

线程池中的线程也是 new 出来的

1
2
3
4
5
6
7
8
9
public class Executors {
static class DefaultThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread t = new Thread(...);
...
return t;
}
}
}

一切的一切就到了 Thread 的构造函数中,而 Thread 的构造函数又是调用的 Thread 中的 init 方法

1
2
3
4
5
6
7
8
9
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
this.grout = g;
this.name = name;
...
tid = nextThreadID();
}

这个 init 方法,仅仅是给该 Thread 类的对象中的属性,附上值,除此之外啥也没干

它没有给 theadStatus 再次赋值,所以它的值仍然是其默认值——STATE.NEW

State.New

RUNNABLE

在调用 Thread 的 start 方法后才算是启动了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

start 方法一开始就会对线程的状态进行判断,如果线程的状态不是 NEW,就会抛出异常

其核心的启动方法为 start0() 这是一个本地方法

1
private native void start0();

进入 jvm 源码,可以查到这个方法

hotspot/src/os/linux/vm/os_linux.cpp

1
pthread_create(...);

unix 创建线程的方法,pthread_create

此时,在操作系统内核中,才有了一个真正的线程,被创建出来

而 linux 操作系统,是没有所谓的刚创建但没启动的线程这种说法的,创建即开始运行

虽然无法从源码发现线程状态的变化,但通过 debug 的方式,我们看到调用了 Thread.start() 方法后,线程的状态变成了 RUNNABLE,运行态

Runnable

通过这部分,我们知道如下几点:

  • 在 Java 调用 start() 后,操作系统中才真正出现了一个线程,并且立刻运行
  • Java 线程与操作系统内核中的线程是一对一的关系
  • 调用 start 后,线程状态变为 RUNNABLE,这是由 native 方法里的某部分代码造成的

RUNNING 和 READY

CPU 一个核心,同一时刻,只能运行一个线程

具体执行哪个线程,要看操作系统的调度机制

所以,上面的 RUNNABLE 状态,准确说是,可以随时运行的就绪状态

处于这个状态中的线程,又可以细分为两种状态

  • 正在 CPU 中运行的 RUNNING 状态
  • 等待 CPU 分配时间片来运行的 READY 状态

这里的 RUNNING 和 READY 状态,是为了方便理解而创造出来的

无论是 Java 语言,还是操作系统,都不区分这两种状态,在 Java 中统统叫 RUNNABLE

Running 和 Ready

TERMINATED

当一个线程执行完毕(或者因异常中断,或调用已经不建议的 stop 方法),线程的状态就变为 TERMINATED

Terminated

BLOCKED

上面是最常见,最简单的线程生命周期,NEW -> RUNNABLE -> TERMINATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static final Object lock = new Object();

private static volatile int i = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (i == 0) {}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
while (true) {}
}
});
t1.start();
TimeUnit.SECONDS.sleep(3L);
System.out.println("t1 state: " + t1.getState());
t2.start();
System.out.println("t2 state: " + t2.getState());
TimeUnit.MILLISECONDS.sleep(500L);
System.out.println("t2 state: " + t2.getState());
i = 1;
TimeUnit.MILLISECONDS.sleep(500L);
System.out.println("t2 state: " + t2.getState());
}

题外话:上面代码要特别注意给全局变量 i 声明 volatile

输出:

1
2
3
4
t1 state: RUNNABLE
t2 state: RUNNABLE
t2 state: BLOCKED
t2 state: RUNNABLE

由此得出以下转换关系

Blocked

需要注意的是,从 BLOCKED 到 RUNNABLE 后,具体是 RUNNING 还是 READY 这个并不是由 java 决定,取决于 CPU 的调度

如果不考虑虚拟机对 synchronized 的极致优化

当进入 synchronized 块或方法,获取不到锁时,线程会进入一个 该锁对象的同步队列

当持有锁的这个线程,释放了锁之后,会唤醒该锁对象同步队列中的所有线程,这些线程会继续尝试抢锁。如此往复

比如,现在有一个对象锁 🔒A,有 ①,②,③,④ 个线程去抢这个锁

抢到锁的则进入 RUNNABLE 状态,否则进入同步队列为 BLOCKED 状态

线程 ① 抢到锁

线程 ① 抢到锁,线程 ②,③,④ 进入 🔒A 的同步队列

线程 ① 释放锁

线程 ① 释放锁,线程 ②,③,④ 重新变为 RUNNABLE,继续抢锁,此时线程 ③ 抢到了锁,线程 ②,④ 再次进入 🔒A 的同步队列

WAITING

这部分是最复杂的,同时也是面试中考点最多的,将分成三部分讲解。这三部分有很多相同但地方,并不是孤立的知识点

wait/notify/notifyAll

1
2
3
4
5
6
7
new Thread(() -> {
    synchronized (lock) {
       ...
       lock.wait();
       ...
    }
}).start();

调用 lock.wait() 方法,会发生三件事:

  • 释放 🔒
  • 线程变为 WAITING 状态
  • 线程进入 🔒 对象的 等待队列

wait

如果想要将线程 ① 从 🔒 对象的等待队列中取出,需要另一个线程进行唤醒

1
2
3
4
5
6
7
new Thread(() -> {
    synchronized (lock) {
       ...
       lock.notify(); 
       ...
    }
}).start();

这时线程 ① 的状态变为 RUNNABLE,需要注意的是,线程 ① 仍然要抢锁,如果抢锁失败,就和上面 BLOCKED 流程一样了

notify

notify 与 notifyAll 的区别在于,notify 唤醒 🔒 对象的等待队列中 任意一个 线程,而 notifyAll 是清空等待队列,唤醒所有的线程,它们都会重新去抢锁

现在,整个流程图变成这样

thread state

join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
Thread t2 = new Thread(() -> {
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 如果注释掉 t1.start() 会发生什么?
t1.start();
t2.start();
System.out.println("t1 state:" + t1.getState());
System.out.println("t2 state:" + t2.getState());
}

out:

1
2
t1 state:RUNNABLE
t2 state:WAITING

join 也能使线程进入 WAITING 状态

状态更新

thread state

join 是如何将线程放入等待队列的呢?

查看 Thread.join() 方法可以看到底层逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public final synchronized void join(long millis) throws InterruptedException {
...

if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
...
wait(delay);
...
}
}
}

join 的底层就是 wait

与之前自己调用 wait 不同的是

  • join 中调用的 wait 是线程对象的,而前面自己调用的是锁对象的
  • join 调用的 wait 不需要用户执行 notify/notifyAll,而前面自己调用是需要另一个线程调用 notify/notifyAll

那 join 中调用 wait 后,是如何唤醒的呢?

查看 JVM 源码 hotspot/src/share/vm/runtime/thread.cpp

1
2
3
4
5
6
7
8
9
10
void JavaThread::exit(...) { 
...
ensure_join(this);
...
}
static void ensure_join(JavaThread* thread) {
...
lock.notify_all(thread);
...
}

所以还是通过 notifyAll 来唤醒的,只不过是 jvm 等线程执行结束后来唤醒的

所以 join 可以说是等同于 wait,这样状态的变化更清晰了

thread state

既然 Thread.join 会对线程对象进行上锁,那么如果有两个线程 t1,t2,t1 中调用 t2.join(),t2 中调用 t1.join(),如此一来,是否就导致死锁?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Thread[] t = new Thread[2];

public static void main(String[] args) {
t[0] = new Thread(() -> {
try {
t[1].join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t[1] = new Thread(() -> {
try {
t[0].join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
t[0].start();
t[1].start();
}

park/unpark

park 与 unpark 是 java.util.concurrent.locks.LockSupport 下的两个方法

LockSupport 是 JUC 包中的一个实用工具类,提供基本的线程阻塞(parking)和解锁(unparking)机制。它通常用于实现更高级别的同步实用程序,例如 semaphore, mutexes 和 condition variables

用法一看便知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("t1: Parking...");
LockSupport.park();
System.out.println("t1: Unparked and continues execution...");
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("t2: Unparking t1...");
LockSupport.unpark(t1);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
t2.start();
}

Out:

1
2
3
t1: Parking...
t2: Unparking t1...
t1: Unparked and continues execution...

park 可以使线程进入 WAITING 状态

unpark 可以使线程回到 RUNNABLE 状态

不过,与 wait/notify 不同的是

  • park/unpark 与锁无关
  • unpark 可以精准唤醒指定线程
  • park/unpark 无顺序要求,可以先 unpark

整个状态就又发生更新

thread state

TIMED_WAITING

这部分就再简单不过了,将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 TIMED_WAITING 状态的方法了

timed_waiting

需要注意的是,从 TIMED_WAITING 到 RUNNABLE 不仅可以通过主动调用方法来改变,还能 通过超时时间来被动实现

与此同时,还有一个常见的方法也能使线程从 RUNNABLE 到 TIMED_WAITING

就是 Thread.sleep(long)

如此一来,整个线程的状态转换就很清晰明了了

complete thread state

Yield

yield 即 「谦让」,是 Thread 中的方法

yield 用于让出当前线程的时间片,给其他线程来执行,但只是对线程调度器的 建议,是否切换完全取决于 CPU

yield 很卑微,并不能改变线程的执行状态,不如 wait 可以释放锁,不如 sleep 可以暂停线程,一句话总结就是没啥鸟用

ReentrantLock

ReentrantLock 是除 Synchronized 外,最常用的锁,但它们的底层实现并不相同,ReentrantLock 是大佬「Doug Lea」基于 AQS 实现的,而 Synchronized 是基于 JVM 实现的,ReentrantLock 有 Synchronized 之外的更「高级」的功能

这里只分析 ReentrantLock 对线程状态的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static final ReentrantLock LOCK = new ReentrantLock();

private static volatile int i = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
LOCK.lock();
System.out.println("t1 running...");
while (i == 0) {}
LOCK.unlock();
});
Thread t2 = new Thread(() -> {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
LOCK.lock();
try {
System.out.println("t2 running...");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
LOCK.unlock();
}
});
t1.start();
t2.start();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("t1 state:" + t1.getState());
System.out.println("t2 state:" + t2.getState());
i = 1;
TimeUnit.MILLISECONDS.sleep(100);
System.out.println("t1 state:" + t1.getState());
System.out.println("t2 state:" + t2.getState());
}

Out:

1
2
3
4
5
6
t1 running...
t1 state:RUNNABLE
t2 state:WAITING
t2 running...
t1 state:TERMINATED
t2 state:TIMED_WAITING

当 t1 获取到锁,而 t2 没获取到锁在等待时,t2 的状态并 不是 BLOCKED 而是 WAITING

这与 Synchronized 有很大的不同

原因在于 ReentrantLock 是基于 AQS 的,而 AQS 的底层,是用 park 和 unpark 来挂起和唤醒线程,所以状态变为 WAITING

await/signal/signalAll

ReentrantLock(AQS) 还提供了 Condition 类来实现线程间的协调,可以通过 Condition 中的 await 方法实现线程等待,signal 或 signalAll 方法实现线程的唤醒,十分类似 wait/notify/notifyAll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private static final ReentrantLock lock = new ReentrantLock();

private static final Condition condition = lock.newCondition();

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println("t1 running...");
System.out.println("t1 wait condition...");
condition.await();
System.out.println("t1 ending...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println("t2 running...");
System.out.println("t2 wait condition...");
condition.await(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread signalThread = new Thread(() -> {
lock.lock();
condition.signal();
lock.unlock();
});
t1.start();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("t1 state:" + t1.getState());
System.out.println("lock is Locked:" + lock.isLocked());
signalThread.start();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("t1 state:" + t1.getState());
t2.start();
TimeUnit.MILLISECONDS.sleep(500);
System.out.println("t2 state:" + t2.getState());
}

Out:

1
2
3
4
5
6
7
8
9
t1 running...
t1 wait condition...
t1 state:WAITING
lock is Locked:false
t1 ending...
t1 state:TERMINATED
t2 running...
t2 wait condition...
t2 state:TIMED_WAITING

Thanks