多线程的状态

·Java, 多线程

在 Java 中,线程的生命周期被明确地定义在 java.lang.Thread.State 这个枚举类中。一个线程在任何给定时间点,都有且仅有处于这六种状态中的一种。


线程的六种状态 (The 6 States)

这六种状态是:

  1. NEW (新建)
  2. RUNNABLE (可运行)
  3. BLOCKED (阻塞)
  4. WAITING (无限等待)
  5. TIMED_WAITING (限时等待)
  6. 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() 方法中的代码。
  • 类比: 你已经过了安检,在候机厅(就绪),随时等待登机(获取 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++ 看起来是一行代码,但它在底层不是原子的,至少包含三步:

  1. 读取 count 的值 (0)
  2. 增加 值 (0 + 1 = 1)
  3. 写回 count (1)

可能发生的错误:

  1. 线程 A 读取 count (0)。
  2. (此时 CPU 切换) 线程 B 读取 count (0)。
  3. 线程 B 增加 (1) 并写回 (1)。
  4. (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 实例(obj1obj2…),任何线程调用 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 (监视器) 的概念。

  1. Monitor: 在 Java 中,任何对象new Object()thisMyClass.class)都可以是一个 Monitor。
  2. 锁的获取: 当一个线程 A 遇到 synchronized (obj) 时,它会尝试获取 obj 这个 Monitor 的所有权(即锁)
  3. 获取成功: 如果 obj 的 Monitor 没有被其他线程持有,线程 A 成功获取锁,进入代码块执行。
  4. 获取失败 (关键): 如果 obj 的 Monitor 已经被线程 B 持有:
    • 线程 A 会被挂起
    • 线程 A 进入该 Monitor 的等待队列(Entry Set)。
    • 线程 A 的状态从 RUNNABLE 变为 BLOCKED。(这就是我们上一节课讲的 BLOCKED 状态的来源!)
  5. 锁的释放: 当线程 B 执行完毕,退出 synchronized 代码块时,它会释放 obj 的 Monitor 的锁。
  6. 唤醒: JVM 会从该 Monitor 的等待队列(Entry Set)中唤醒一个(或多个)BLOCKED 的线程,让它们重新变为 RUNNABLE 状态,再次尝试获取锁。

4. synchronized 的关键特性

  1. 可重入性 (Reentrancy)

    • 含义: 一个已经持有锁的线程,可以再次获取同一个锁,而不会被自己阻塞。
    • 示例:
    public synchronized void methodA() {
        methodB(); // 线程进入 B
    }
    public synchronized void methodB() {
        // ...
    }
    
    • 如果一个线程调用了 methodA(),它获取了 this 锁。当它在内部调用 methodB() 时,它尝试再次获取 this 锁。因为 synchronized 是可重入的,它会成功,否则它会在这里死锁。
    • 原理: Monitor 会记录持有锁的线程,并维护一个计数器。每重入一次,计数器+1;每退出一层,计数器-1。当计数器为 0 时,锁才真正被释放。
  2. 互斥性 (Mutual Exclusion)

    • (已讲过) 确保同一时间只有一个线程在临界区(被锁住的代码)内。
  3. 可见性 (Visibility)

    • 含义: synchronized 不仅保证了互斥,还保证了内存可见性
    • 规则: 当一个线程退出 synchronized 块时,它在块内修改的所有变量,都会被强制刷新到主内存。当一个线程进入 synchronized 块时,它会清空本地工作内存,强制从主内存重新加载变量的最新值。
    • 重要性: 这解决了多线程中“一个线程修改了值,另一个线程却看不到”的问题。

我们已经知道了 synchronized 是如何让线程进入 BLOCKED 状态的。您想继续了解它是如何与 Object.wait() 和 Object.notify() 配合,让线程进入 WAITING 状态的吗?