Java 实例 – 线程挂起(超详细)

Java 实例 – 线程挂起:从概念到实战

在多线程编程的世界里,线程就像一群正在奔跑的快递员。每个快递员(线程)都负责一个任务,比如送货、打包、登记。但有时候,某个快递员在送货途中发现目的地暂时无法送达,比如客户不在家、仓库没开门。这时候,他不能一直傻等,也不能强行闯入,而是选择“暂停工作”,等待条件满足后再继续。这种“暂停”的行为,就是我们常说的“线程挂起”。

在 Java 中,线程挂起并不是一个直接的 API 操作,而是通过一系列机制来实现的。理解这些机制,是掌握并发编程的关键一步。本文将通过真实代码示例,带你一步步搞懂 Java 实例 – 线程挂起的原理与用法。


什么是线程挂起?

线程挂起(Thread Suspension)是指让一个正在运行的线程暂时停止执行,进入等待状态,直到某个条件满足后再恢复运行。这在多任务处理中非常常见,比如:

  • 等待某个资源释放
  • 等待用户输入
  • 等待数据库查询结果返回

虽然 Java 早期版本曾提供 suspend()resume() 方法,但它们因容易导致死锁,已被标记为 过时(deprecated)。现在的 Java 通过 wait()notify()notifyAll() 以及 LockCondition 等机制来安全地实现线程挂起。

💡 小贴士:线程挂起 ≠ 线程终止。挂起是暂停,终止是彻底结束。就像你暂时关掉手机,但手机还在,只是不响了。


使用 wait() 和 notify() 实现线程挂起

wait()notify() 是最经典、最基础的线程挂起机制,它们必须配合 synchronized 使用,属于对象级别的操作。

原理说明

  • wait():让当前线程释放锁并进入等待状态,直到其他线程调用 notify()notifyAll()
  • notify():唤醒一个正在等待该对象锁的线程。
  • notifyAll():唤醒所有等待该对象锁的线程。

这就像一个“排队等餐”的餐厅:你坐在位置上,服务员告诉你“稍等一下”,你就放下筷子(释放锁),坐下来等。当菜做好了,服务员喊“上菜了”,你才继续吃饭。

实际代码示例

public class WaitNotifyExample {
    private final Object lock = new Object(); // 用于同步的锁对象
    private boolean isReady = false; // 标记数据是否准备就绪

    // 生产者线程:负责准备数据
    public void producer() {
        synchronized (lock) {
            System.out.println("生产者开始准备数据...");
            // 模拟耗时操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.out.println("生产者被中断");
                return;
            }

            isReady = true; // 数据准备完成
            System.out.println("生产者:数据已准备就绪,通知等待线程");
            lock.notify(); // 唤醒一个等待的线程
        }
    }

    // 消费者线程:等待数据并消费
    public void consumer() {
        synchronized (lock) {
            // 如果数据未准备就绪,线程将进入等待状态
            while (!isReady) {
                try {
                    System.out.println("消费者:数据尚未就绪,进入等待...");
                    lock.wait(); // 释放锁并等待,直到被唤醒
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("消费者被中断");
                    return;
                }
            }

            System.out.println("消费者:数据已就绪,开始消费");
            // 消费数据的逻辑
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    public static void main(String[] args) {
        WaitNotifyExample example = new WaitNotifyExample();

        // 启动生产者线程
        Thread producerThread = new Thread(example::producer);
        // 启动消费者线程
        Thread consumerThread = new Thread(example::consumer);

        // 先启动消费者,让它先等待
        consumerThread.start();
        try {
            Thread.sleep(1000); // 稍等一下,确保消费者先启动
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 再启动生产者,触发通知
        producerThread.start();

        try {
            producerThread.join(); // 等待生产者结束
            consumerThread.join(); // 等待消费者结束
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

📌 代码说明

  • synchronized(lock) 确保同一时间只有一个线程能访问共享资源。
  • while (!isReady) 而不是 if,是为了防止“虚假唤醒”(spurious wakeup)——即使没有被 notify(),线程也可能醒来,所以必须重新检查条件。
  • lock.wait() 会自动释放锁,并让线程进入等待队列。
  • lock.notify() 唤醒一个等待线程,但不会立即恢复执行,需重新获取锁。

使用 Lock 和 Condition 实现更精细的挂起控制

wait()notify() 虽然经典,但使用起来不够灵活,比如不能指定唤醒哪个线程。Java 5 引入了 java.util.concurrent.locks 包,提供了更强大的锁机制。

Lock 与 Condition 的优势

  • 可以创建多个 Condition,实现“按条件唤醒”。
  • 支持公平锁、可中断锁、超时锁等高级特性。
  • 更清晰的代码结构,避免死锁风险。

实际代码示例

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockConditionExample {
    private final Lock lock = new ReentrantLock(); // 可重入锁
    private final Condition condition = lock.newCondition(); // 创建条件对象
    private boolean isReady = false;

    // 生产者方法
    public void producer() {
        lock.lock(); // 获取锁
        try {
            System.out.println("生产者:开始准备数据...");
            Thread.sleep(2000);

            isReady = true;
            System.out.println("生产者:数据准备完成,通知等待线程");
            condition.signal(); // 唤醒一个等待的线程
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("生产者被中断");
        } finally {
            lock.unlock(); // 一定要释放锁
        }
    }

    // 消费者方法
    public void consumer() {
        lock.lock(); // 获取锁
        try {
            while (!isReady) {
                System.out.println("消费者:数据未就绪,等待中...");
                condition.await(); // 等待,释放锁,进入等待状态
            }

            System.out.println("消费者:数据已就绪,开始消费");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("消费者被中断");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        LockConditionExample example = new LockConditionExample();

        Thread producer = new Thread(example::producer);
        Thread consumer = new Thread(example::consumer);

        consumer.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        producer.start();

        try {
            producer.join();
            consumer.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

📌 关键点

  • lock.lock()lock.unlock() 必须成对出现,通常放在 try-finally 中,防止锁未释放。
  • condition.await() 相当于 wait(),但更灵活。
  • condition.signal() 相当于 notify()
  • 可以创建多个 Condition 实现多条件等待,比如“库存不足”、“订单超时”等。

线程挂起的常见误区与最佳实践

在实际开发中,很多线程挂起的问题源于对机制的理解不深。以下是几个常见误区:

误区 正确做法
Thread.suspend()resume() 已废弃,极易造成死锁,应避免使用
if 中使用 wait() 应使用 while 循环判断条件,防止虚假唤醒
忘记在 finally 中释放锁 使用 try-finally 确保锁一定释放
多个线程竞争同一条件但未使用 notifyAll() notifyAll() 更安全,避免唤醒遗漏

最佳实践建议

  • 优先使用 LockCondition,比 synchronized 更灵活。
  • 所有等待逻辑都用 while 包裹,而非 if
  • 线程挂起期间不要持有大锁,避免阻塞其他线程。

为什么线程挂起是并发编程的核心?

想象一个电商系统:用户下单时,系统需要检查库存。如果库存不足,就不能立即扣减。这时,系统可以挂起当前线程,等待库存补货。等到补货完成,再唤醒线程继续处理订单。这正是线程挂起的典型应用场景。

没有线程挂起机制,系统只能不断轮询(busy-wait),浪费 CPU 资源。而通过挂起,系统能高效地利用资源,实现高并发、低延迟。

Java 实例 – 线程挂起,正是构建高性能并发程序的基石之一。掌握它,你就迈出了成为高级 Java 开发者的坚实一步。


总结

本文从线程挂起的概念出发,通过 wait()/notify()Lock/Condition 两种主流方式,详细演示了如何实现线程安全的挂起与唤醒。我们不仅讲解了原理,还提供了可运行的代码示例,并指出了常见陷阱。

无论你是初学者还是中级开发者,理解并掌握线程挂起机制,都对提升你的并发编程能力至关重要。记住:线程挂起不是“暂停”,而是“智慧地等待”。

在未来的并发编程中,你会频繁遇到“等待资源”“等待条件满足”的场景。而 Java 实例 – 线程挂起,正是解决这些问题的钥匙。