在 Java 中,线程的生命周期被明确地定义在 java.lang.Thread.State 这个枚举类中。一个线程在任何给定时间点,都有且仅有处于这六种状态中的一种。
线程的六种状态 (The 6 States)
这六种状态是:
NEW(新建)RUNNABLE(可运行)BLOCKED(阻塞)WAITING(无限等待)TIMED_WAITING(限时等待)TERMINATED(终止)
1. NEW (新建)
- 状态描述: 当你使用
new Thread(...)创建了一个Thread对象,但还未调用其start()方法时,这个线程就处于NEW状态。 - 类比: 你已经买好了机票(
Thread对象),但你还没去机场(没调用start())。 - 如何进入:
new Thread(); - 如何离开: 调用
thread.start()方法。- ➡️ 转向
RUNNABLE状态。
- ➡️ 转向
2. RUNNABLE (可运行)
这是最容易误解的状态。在 Java 的视角里,RUNNABLE 状态包含了传统操作系统所说的**“就绪态”和“运行中”**两种状态。
- 状态描述:
- 就绪 (Ready): 线程调用
start()后,进入了可运行线程池,等待 JVM 的线程调度器分配 CPU 时间片。 - 运行中 (Running): 线程调度器分配了 CPU 时间片,该线程正在 CPU 上执行它的
run()方法中的代码。
- 就绪 (Ready): 线程调用
- 类比: 你已经过了安检,在候机厅(就绪),随时等待登机(获取 CPU)。或者你已经在飞机上了(运行中)。
- 如何进入:
- 从
NEW状态调用start()。 - 从
BLOCKED状态获得了锁。 - 从
WAITING状态被notify()/notifyAll()唤醒。 - 从
TIMED_WAITING状态等待超时,或被唤醒。
- 从
- 如何离开:
run()方法正常执行完毕。 ➡️ 转向TERMINATED。run()方法因异常退出。 ➡️ 转向TERMINATED。- 尝试进入一个
synchronized同步块,但锁被其他线程持有。 ➡️ 转向BLOCKED。 - 调用
Thread.sleep(long millis)。 ➡️ 转向TIMED_WAITING。 - 调用
Object.wait()。 ➡️ 转向WAITING。 - 调用
Object.wait(long millis)。 ➡️ 转向TIMED_WAITING。 - 调用
t.join()。 ➡️ 转向WAITING(或TIMED_WAITING)。 - 调用
LockSupport.park()。 ➡️ 转向WAITING。
3. 🔒 BLOCKED (阻塞)
这个状态特指一种情况:等待 synchronized 的监视器锁 (Monitor Lock)。
- 状态描述: 线程A 尝试进入一个
synchronized修饰的代码块或方法,但该锁正被线程B持有。此时,线程A 就会进入BLOCKED状态,直到线程B 释放这个锁。 - 类比: 你想进一个卫生间(
synchronized块),但门被锁了(被占用了),你只能在门口(BLOCKED)排队等着。 - 如何进入: 从
RUNNABLE状态尝试获取一个已被其他线程持有的synchronized锁。 - 如何离开: 持有锁的线程释放了该锁,JVM 唤醒此线程,它会重新进入
RUNNABLE状态,再次尝试获取锁(注意:不是直接获得,是回去重新竞争)。
4. WAITING (无限等待)
这个状态是主动的,线程在等待某个特定条件的发生,并且没有时间限制。
- 状态描述: 线程在
RUNNABLE状态下调用了特定方法(如Object.wait()),主动放弃 CPU,进入等待队列,直到被其他线程显式唤醒。 - 类比: 你在餐厅点好了菜(
wait()),然后就开始玩手机,你主动进入等待,直到服务员把菜端上来(notify())。 - 如何进入:
- 调用
Object.wait()(必须在synchronized块中调用)。 - 调用
Thread.join()(等待另一个线程t终止)。 - 调用
LockSupport.park()。
- 调用
- 如何离开:
- 其他线程调用了
Object.notify()或Object.notifyAll()。 join()的那个线程t运行结束 (TERMINATED)。LockSupport.unpark(thread)被调用。- (离开后,通常会进入
RUNNABLE或BLOCKED状态)
- 其他线程调用了
5. ⏰ TIMED_WAITING (限时等待)
和 WAITING 类似,但是这个等待是有时间限制的。
- 状态描述: 线程主动进入等待,但它会在指定时间后自动醒来,或者被中途唤醒。
- 类比: 你在餐厅点菜(
wait(long)),并告诉服务员:“如果 10 分钟上不来(超时),我就不要了,先给我上汤”。 - 如何进入:
- 调用
Thread.sleep(long millis)。 - 调用
Object.wait(long millis)。 - 调用
Thread.join(long millis)。 - 调用
LockSupport.parkNanos(long nanos)或parkUntil(long deadline)。
- 调用
- 如何离开:
- 等待时间结束(超时)。
- 在超时前,被其他线程显式唤醒(同
WAITING的离开方式)。 - (离开后,进入
RUNNABLE或BLOCKED状态)
6. 💀 TERMINATED (终止)
- 状态描述: 线程的
run()方法已经执行完毕(无论是正常返回还是抛出异常),线程的生命周期结束。 - 类比: 你的航班已经抵达目的地。
- 如何进入:
run()方法执行完成。 - 如何离开: 无法离开。这是一个终态。
- 注意: 一个
TERMINATED状态的线程,不能被再次调用start(),否则会抛出IllegalThreadStateException。
📊 总结:BLOCKED vs WAITING vs TIMED_WAITING
这是最容易混淆的三个状态,我帮您做一个对比:
| 状态 | 触发原因 (如何进入) | 唤醒方式 (如何离开) |
|---|---|---|
BLOCKED | 被动:等待 synchronized 监视器锁。 | 被动:持有锁的线程释放了锁。 |
WAITING | 主动:调用 Object.wait(), t.join(), LockSupport.park()。 | 主动:notify(), join的线程结束, unpark()。 |
TIMED_WAITING | 主动:调用 sleep(), wait(t), join(t) 等带时间参数的方法。 | 主动:同 WAITING 的唤醒方式,或者超时。 |
🔔 重点提示:Lock vs synchronized
BLOCKED状态只针对synchronized关键字。- 如果您使用的是 JUC 包中的
Lock(例如ReentrantLock),当调用lock.lock()时,如果锁不可用,线程会进入WAITING状态 (内部使用LockSupport.park()),而不是BLOCKED状态。这是Lock锁和synchronized锁在线程状态上的一个重大区别。
这些变化的核心,特别是 BLOCKED 和 WAITING 状态的管理,依赖于 Java 的同步机制。
synchronized 关键字
synchronized 是 Java 中用于解决线程安全问题的一个核心关键字。
它本质上是一种内置的、排他性的锁(也称为“监视器锁”或 “Monitor Lock”)。它的核心作用是确保在同一时刻,只有一个线程可以执行被它修饰的代码块或方法,从而防止多个线程同时读写共享数据时引发的冲突。
1. 为什么需要 synchronized?(解决的问题)
在多线程环境中,如果多个线程同时访问同一个共享变量(如一个全局计数器),并且至少有一个线程会修改它,就可能发生竞态条件 (Race Condition),导致数据不一致。
一个经典的例子:count++
假设两个线程(A 和 B)同时执行 count++,count 初始值为 0。
count++ 看起来是一行代码,但它在底层不是原子的,至少包含三步:
- 读取
count的值 (0) - 增加 值 (0 + 1 = 1)
- 写回
count(1)
可能发生的错误:
- 线程 A 读取
count(0)。 - (此时 CPU 切换) 线程 B 读取
count(0)。 - 线程 B 增加 (1) 并写回 (1)。
- (CPU 切换回) 线程 A 增加它之前读到的值 (1) 并写回 (1)。
- 期望结果: 两个线程各加了 1,
count应为 2。 - 实际结果:
count最终为 1。数据丢失了!
synchronized 通过加锁来解决这个问题,它强制要求线程排队,一次只让一个线程执行 count++ 操作。
2. synchronized 的三种使用方式
synchronized 可以用在三个地方,但本质上锁住的对象不同:
1. 修饰实例方法 (Instance Method)
当 synchronized 修饰一个普通的(非 static)方法时,它锁住的是当前类的实例 (this)。
- 语法:
public class MyClass {
public synchronized void myMethod() {
// 这里的代码是线程安全的
}
}
- 效果:
- 如果
obj1是MyClass的一个实例,多个线程同时调用obj1.myMethod(),只有一个能执行,其他的会阻塞。 - 如果
obj1和obj2是两个不同的实例,一个线程调用obj1.myMethod(),另一个线程调用obj2.myMethod(),它们不会互相阻塞,因为它们锁的是不同的this对象。
- 如果
2. 修饰静态方法 (Static Method)
当 synchronized 修饰一个 static 方法时,它锁住的是整个类本身 (MyClass.class)。
- 语法:
public class MyClass {
public static synchronized void myStaticMethod() {
// 这里的代码是线程安全的
}
}
- 效果:
MyClass.class对象在 JVM 中是唯一的。- 无论你有多少个
MyClass实例(obj1,obj2…),任何线程调用myStaticMethod()都会竞争同一把锁(MyClass.class的锁)。
3. 修饰代码块 (Code Block)
这是最灵活的方式。你可以显式指定**“锁对象”**。
- 语法:
public class MyClass {
private final Object lock = new Object(); // 推荐:使用一个私有的、final的对象作为锁
public void myMethod() {
// ... 非同步的普通代码 ...
// 只对“关键”代码块加锁
synchronized (lock) {
// 这里的代码是线程安全的
// 比如 count++
}
// ... 其他非同步代码 ...
}
}
- 效果:
synchronized (this) { ... }和修饰实例方法(方式1)的效果完全一样。synchronized (MyClass.class) { ... }和修饰静态方法(方式2)的效果完全一样。- 最佳实践: 如上例所示,使用一个专门的
private final Object lock。这可以减小锁的粒度(只锁住必要的部分),并且不会意外地与类上的其他synchronized方法(它们锁this)发生冲突。
3. synchronized 是如何工作的?(核心原理)
synchronized 的工作依赖于一个叫做 Monitor (监视器) 的概念。
- Monitor: 在 Java 中,任何对象(
new Object(),this,MyClass.class)都可以是一个 Monitor。 - 锁的获取: 当一个线程 A 遇到
synchronized (obj)时,它会尝试获取obj这个 Monitor 的所有权(即锁)。 - 获取成功: 如果
obj的 Monitor 没有被其他线程持有,线程 A 成功获取锁,进入代码块执行。 - 获取失败 (关键): 如果
obj的 Monitor 已经被线程 B 持有:- 线程 A 会被挂起。
- 线程 A 进入该 Monitor 的等待队列(Entry Set)。
- 线程 A 的状态从
RUNNABLE变为BLOCKED。(这就是我们上一节课讲的BLOCKED状态的来源!)
- 锁的释放: 当线程 B 执行完毕,退出
synchronized代码块时,它会释放obj的 Monitor 的锁。 - 唤醒: JVM 会从该 Monitor 的等待队列(Entry Set)中唤醒一个(或多个)
BLOCKED的线程,让它们重新变为RUNNABLE状态,再次尝试获取锁。
4. synchronized 的关键特性
可重入性 (Reentrancy)
- 含义: 一个已经持有锁的线程,可以再次获取同一个锁,而不会被自己阻塞。
- 示例:
public synchronized void methodA() { methodB(); // 线程进入 B } public synchronized void methodB() { // ... }- 如果一个线程调用了
methodA(),它获取了this锁。当它在内部调用methodB()时,它尝试再次获取this锁。因为synchronized是可重入的,它会成功,否则它会在这里死锁。 - 原理: Monitor 会记录持有锁的线程,并维护一个计数器。每重入一次,计数器+1;每退出一层,计数器-1。当计数器为 0 时,锁才真正被释放。
互斥性 (Mutual Exclusion)
- (已讲过) 确保同一时间只有一个线程在临界区(被锁住的代码)内。
可见性 (Visibility)
- 含义:
synchronized不仅保证了互斥,还保证了内存可见性。 - 规则: 当一个线程退出
synchronized块时,它在块内修改的所有变量,都会被强制刷新到主内存。当一个线程进入synchronized块时,它会清空本地工作内存,强制从主内存重新加载变量的最新值。 - 重要性: 这解决了多线程中“一个线程修改了值,另一个线程却看不到”的问题。
- 含义:
我们已经知道了 synchronized 是如何让线程进入 BLOCKED 状态的。您想继续了解它是如何与 Object.wait() 和 Object.notify() 配合,让线程进入 WAITING 状态的吗?