一、线程的完整生命周期(6大状态)

基础简单版本仅包含新建、运行、销毁,开发/面试核心为完整6状态,重点关注阻塞相关状态

线程的状态由JVM调度和代码逻辑共同控制,完整生命周期包含新建(NEW)、就绪、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED),核心状态转换及说明如下:

  1. 新建(NEW):创建Thread对象但未调用start()方法,此时线程仅为普通Java对象,未进入JVM线程调度体系。
  2. 就绪:调用start()方法后,线程等待CPU分配执行时间,处于“待运行”状态。
  3. 可运行(RUNNABLE):CPU调度成功后,线程执行run()方法内代码,是实际的运行状态;CPU执行权被切走后,线程会回到就绪状态。
  4. 阻塞相关细分状态:均为线程暂停执行的状态,触发条件不同,是实现线程阻塞的核心:
    • 阻塞(BLOCKED):线程因竞争synchronized对象锁失败进入的阻塞(如锁被其他线程持有)。
    • 等待(WAITING):无时间限制的阻塞,由调用wait()join()等方法触发,需其他线程手动唤醒。
    • 超时等待(TIMED_WAITING):有时间限制的阻塞,由调用sleep(long)wait(long)等方法触发,超时后自动唤醒。
  5. 终止(TERMINATED):线程唯一正确的终止方式run()方法内代码执行完毕;严禁使用stop()方法手动终止,会导致资源泄漏、数据不一致等问题。

二、线程阻塞与唤醒的核心原理(Object类wait() + notifyAll())

Object类提供的wait()notifyAll()是Java原生实现线程协作阻塞的核心方法,也是生产者-消费者模型的基础,必须在synchronized同步方法/代码块中调用,核心原理及特性如下:

  1. wait():当前调用该方法的线程会立即释放持有的对象锁,进入该对象的等待集,处于WAITING状态,直到被其他线程唤醒。
  2. notifyAll():唤醒所有因调用该对象wait()方法而阻塞的线程;被唤醒的线程会重新参与对象锁的竞争,竞争成功后继续执行后续逻辑。
  3. 防虚假唤醒:线程可能在未被notify/notifyAll()唤醒的情况下从wait()恢复(JVM底层偶发),因此必须将wait()的判断条件写在while循环中,唤醒后重新校验条件。
  4. 锁的释放:notifyAll()调用后不会立即释放对象锁,需等待当前同步方法/代码块执行完毕,锁才会被释放。

三、基于环形数组实现简单阻塞队列(核心实战)

阻塞队列是线程阻塞的经典应用,核心特性为生产者插入元素时队列满则阻塞,消费者取出元素时队列空则阻塞,以下基于环形数组实现线程安全的简单阻塞队列,结合wait()/notifyAll()实现线程协作,同时保留原代码中t1、t2线程命名规范。

设计核心

  1. 采用环形数组存储元素,通过putIndex(插入位置)、takeIndex(取出位置)实现数组空间复用,提升效率。
  2. count记录队列当前元素个数,作为判断队列满/空的核心依据。
  3. 所有队列操作均为synchronized同步方法,保证多线程下操作的原子性,避免数据不一致。
  4. 生产/消费操作触发阻塞时调用wait(),操作完成后调用notifyAll()唤醒对应阻塞线程。

完整代码实现

package com.th0314;

/**
 * 基于环形数组实现的简单阻塞队列
 * 核心:队列满则生产者阻塞,队列空则消费者阻塞
 * 基于Object的wait()/notifyAll()实现线程阻塞与唤醒
 */
public class SimpleBlockingQueue<T> {
    private final Object[] items; // 存储元素的环形数组,final保证容量不可变
    private int putIndex; // 下一个元素的插入位置,初始默认0
    private int takeIndex; // 下一个元素的取出位置,初始默认0
    private int count; // 队列当前元素个数,核心判断条件

    // 构造方法:初始化队列容量,容量必须大于0
    public SimpleBlockingQueue(int capacity) {
        if (capacity <= 0) {
            throw new IllegalArgumentException("队列容量必须大于0");
        }
        this.items = new Object[capacity];
    }

    // 插入元素:队列满则阻塞,同步方法保证线程安全
    public synchronized void put(T t) throws InterruptedException {
        // while循环判断,防止虚假唤醒:队列满则持续阻塞
        while (count == items.length) {
            System.out.println("队列已满,生产者线程进入等待");
            wait(); // 释放对象锁,当前线程进入阻塞状态
        }
        // 环形数组:将元素插入到当前putIndex位置
        items[putIndex] = t;
        // 插入位置后移,若到数组末尾则重置为0,实现环形复用
        if (++putIndex == items.length) {
            putIndex = 0;
        }
        count++; // 元素个数+1
        notifyAll(); // 唤醒所有阻塞的消费者线程,告知队列有新元素
    }

    // 取出元素:队列空则阻塞,同步方法保证线程安全,抑制泛型强转警告
    @SuppressWarnings("unchecked")
    public synchronized T take() throws InterruptedException {
        // while循环判断,防止虚假唤醒:队列空则持续阻塞
        while (count == 0) {
            wait(); // 释放对象锁,当前线程进入阻塞状态
        }
        // 从takeIndex位置取出元素并强转为泛型类型
        T t = (T) items[takeIndex];
        items[takeIndex] = null; // 置空元素,解除引用,帮助GC回收,避免内存泄漏
        // 取出位置后移,若到数组末尾则重置为0,实现环形复用
        if (++takeIndex == items.length) {
            takeIndex = 0;
        }
        count--; // 元素个数-1
        notifyAll(); // 唤醒所有阻塞的生产者线程,告知队列有空闲位置
        return t;
    }

    // 获取队列当前元素个数,同步方法保证多线程下计数准确
    public synchronized int size() {
        return count;
    }

    // 测试主类:生产者(t1)、消费者(t2)、监听线程实现多线程协作
    class Main {
        public static void main(String[] args) {
            // 创建容量为50的阻塞队列
            SimpleBlockingQueue<String> blockingQueue = new SimpleBlockingQueue<>(50);

            // 生产者线程:创建10个t1线程,循环生产任务,每500ms生产一个
            for (int i = 0; i < 10; i++) {
                // 保留原代码t1线程命名,实现生产者逻辑
                Thread t1 = new Thread() {
                    @Override
                    public void run() {
                        int count = 0; // 记录当前线程生产的任务数
                        while (true) { // 无限循环生产
                            try {
                                Thread.sleep(500); // 模拟生产任务的耗时操作
                                blockingQueue.put("task" + count); // 将任务放入阻塞队列
                                System.out.println("生产任务中---- 已经生产总数:" + count);
                                count++; // 任务数自增
                            } catch (InterruptedException e) {
                                // 捕获中断异常,抛出运行时异常终止线程
                                throw new RuntimeException(e);
                            }
                        }
                    }
                };
                t1.start(); // 启动生产者t1线程
            }

            // 消费者线程:创建t2线程,循环消费任务,每3000ms消费一个
            // 保留原代码t2线程命名,实现消费者逻辑
            Thread t2 = new Thread() {
                @Override
                public void run() {
                    while (true) { // 无限循环消费
                        try {
                            Thread.sleep(3000); // 模拟消费任务的耗时操作
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        String str = null;
                        try {
                            str = blockingQueue.take(); // 从阻塞队列取出任务,空则阻塞
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        System.out.println(str + "end"); // 打印消费完成的任务
                    }
                }
            };
            t2.start(); // 启动消费者t2线程

            // 监听线程:循环监听,每1000ms打印一次队列当前任务量
            Thread t3 = new Thread() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(1000); // 每1000ms监听一次
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 打印队列当前的任务数量
                        System.out.println("------------------------- 现有的任务量:" + blockingQueue.size());
                    }
                }
            };
            t3.start(); // 启动监听线程
        }
    }
}

四、代码核心要点解析(保留t1/t2线程设计)

  1. t1生产者线程设计:循环创建10个t1线程,每个线程以500ms为间隔持续生产任务,调用put()方法将任务放入队列;若队列满,put()方法内的wait()会让t1线程阻塞,直到消费者消费后被唤醒。
  2. t2消费者线程设计:单个t2线程以3000ms为间隔持续消费任务,调用take()方法从队列取任务;若队列空,take()方法内的wait()会让t2线程阻塞,直到生产者生产后被唤醒。
  3. 环形数组的核心作用:通过putIndextakeIndex的循环后移,避免普通数组“取元素后需整体移动”的开销,实现数组空间的高效复用,适合固定容量的队列场景。
  4. 同步方法的必要性put()take()size()均为synchronized方法,保证多线程下对putIndextakeIndexcount的操作不会出现“脏读”“脏写”,确保队列状态的一致性。
  5. GC优化:取出元素后将数组对应位置置为null,解除JVM对该对象的强引用,让垃圾回收器能及时回收无用对象,避免内存泄漏。
  6. 异常处理wait()sleep()均会抛出InterruptedException,需捕获并处理;此处直接抛出运行时异常,保证线程在被中断时能及时终止,避免无效阻塞。

五、wait()与sleep()的核心区别(线程阻塞重点)

两者均能让线程暂停执行,但核心差异体现在锁释放、所属类、使用场景,是线程使用的高频考点,具体区别如下:

  1. 所属类不同:wait()Object类的成员方法,sleep(long)Thread类的静态方法。
  2. 锁释放不同:wait()调用后立即释放当前持有的对象锁,这是实现线程协作的关键;sleep()调用后不释放任何锁,仅让线程休眠。
  3. 使用前提不同:wait()必须在synchronized同步方法/代码块中调用,否则抛出IllegalMonitorStateExceptionsleep()无使用前提,可在任意代码位置调用。
  4. 唤醒方式不同:wait()需通过notify()/notifyAll()手动唤醒,或线程被中断;sleep()超时后自动唤醒,或线程被中断。
  5. 使用场景不同:wait()用于线程间协作阻塞(如生产者-消费者模型);sleep()仅用于模拟耗时操作、线程延时执行,无线程协作能力。

六、线程使用的关键注意事项

  1. 线程启动必须调用start()方法,而非直接调用run()方法;直接调用run()方法仅为普通方法调用,不会创建新线程。
  2. 多线程操作共享资源时,必须保证操作的原子性(如使用synchronized),否则会出现数据不一致问题。
  3. 避免使用while(true)无限循环时的线程空耗:可通过wait()/notifyAll()实现“按需执行”,减少CPU资源占用。
  4. 线程阻塞后必须保证有唤醒逻辑,否则会出现“线程饿死”,导致线程一直处于阻塞状态无法执行。
  5. 泛型数组在Java中无法直接创建,因此采用Object[]数组配合泛型强转实现,通过@SuppressWarnings("unchecked")抑制无关警告。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐