线程生命周期与基于Object的阻塞队列实现
·
一、线程的完整生命周期(6大状态)
基础简单版本仅包含新建、运行、销毁,开发/面试核心为完整6状态,重点关注阻塞相关状态
线程的状态由JVM调度和代码逻辑共同控制,完整生命周期包含新建(NEW)、就绪、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)、终止(TERMINATED),核心状态转换及说明如下:
- 新建(NEW):创建Thread对象但未调用
start()方法,此时线程仅为普通Java对象,未进入JVM线程调度体系。 - 就绪:调用
start()方法后,线程等待CPU分配执行时间,处于“待运行”状态。 - 可运行(RUNNABLE):CPU调度成功后,线程执行
run()方法内代码,是实际的运行状态;CPU执行权被切走后,线程会回到就绪状态。 - 阻塞相关细分状态:均为线程暂停执行的状态,触发条件不同,是实现线程阻塞的核心:
- 阻塞(BLOCKED):线程因竞争
synchronized对象锁失败进入的阻塞(如锁被其他线程持有)。 - 等待(WAITING):无时间限制的阻塞,由调用
wait()、join()等方法触发,需其他线程手动唤醒。 - 超时等待(TIMED_WAITING):有时间限制的阻塞,由调用
sleep(long)、wait(long)等方法触发,超时后自动唤醒。
- 阻塞(BLOCKED):线程因竞争
- 终止(TERMINATED):线程唯一正确的终止方式为
run()方法内代码执行完毕;严禁使用stop()方法手动终止,会导致资源泄漏、数据不一致等问题。
二、线程阻塞与唤醒的核心原理(Object类wait() + notifyAll())
Object类提供的wait()和notifyAll()是Java原生实现线程协作阻塞的核心方法,也是生产者-消费者模型的基础,必须在synchronized同步方法/代码块中调用,核心原理及特性如下:
wait():当前调用该方法的线程会立即释放持有的对象锁,进入该对象的等待集,处于WAITING状态,直到被其他线程唤醒。notifyAll():唤醒所有因调用该对象wait()方法而阻塞的线程;被唤醒的线程会重新参与对象锁的竞争,竞争成功后继续执行后续逻辑。- 防虚假唤醒:线程可能在未被
notify/notifyAll()唤醒的情况下从wait()恢复(JVM底层偶发),因此必须将wait()的判断条件写在while循环中,唤醒后重新校验条件。 - 锁的释放:
notifyAll()调用后不会立即释放对象锁,需等待当前同步方法/代码块执行完毕,锁才会被释放。
三、基于环形数组实现简单阻塞队列(核心实战)
阻塞队列是线程阻塞的经典应用,核心特性为生产者插入元素时队列满则阻塞,消费者取出元素时队列空则阻塞,以下基于环形数组实现线程安全的简单阻塞队列,结合wait()/notifyAll()实现线程协作,同时保留原代码中t1、t2线程命名规范。
设计核心
- 采用环形数组存储元素,通过
putIndex(插入位置)、takeIndex(取出位置)实现数组空间复用,提升效率。 - 用
count记录队列当前元素个数,作为判断队列满/空的核心依据。 - 所有队列操作均为
synchronized同步方法,保证多线程下操作的原子性,避免数据不一致。 - 生产/消费操作触发阻塞时调用
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线程设计)
- t1生产者线程设计:循环创建10个
t1线程,每个线程以500ms为间隔持续生产任务,调用put()方法将任务放入队列;若队列满,put()方法内的wait()会让t1线程阻塞,直到消费者消费后被唤醒。 - t2消费者线程设计:单个
t2线程以3000ms为间隔持续消费任务,调用take()方法从队列取任务;若队列空,take()方法内的wait()会让t2线程阻塞,直到生产者生产后被唤醒。 - 环形数组的核心作用:通过
putIndex和takeIndex的循环后移,避免普通数组“取元素后需整体移动”的开销,实现数组空间的高效复用,适合固定容量的队列场景。 - 同步方法的必要性:
put()、take()、size()均为synchronized方法,保证多线程下对putIndex、takeIndex、count的操作不会出现“脏读”“脏写”,确保队列状态的一致性。 - GC优化:取出元素后将数组对应位置置为
null,解除JVM对该对象的强引用,让垃圾回收器能及时回收无用对象,避免内存泄漏。 - 异常处理:
wait()和sleep()均会抛出InterruptedException,需捕获并处理;此处直接抛出运行时异常,保证线程在被中断时能及时终止,避免无效阻塞。
五、wait()与sleep()的核心区别(线程阻塞重点)
两者均能让线程暂停执行,但核心差异体现在锁释放、所属类、使用场景,是线程使用的高频考点,具体区别如下:
- 所属类不同:
wait()是Object类的成员方法,sleep(long)是Thread类的静态方法。 - 锁释放不同:
wait()调用后立即释放当前持有的对象锁,这是实现线程协作的关键;sleep()调用后不释放任何锁,仅让线程休眠。 - 使用前提不同:
wait()必须在synchronized同步方法/代码块中调用,否则抛出IllegalMonitorStateException;sleep()无使用前提,可在任意代码位置调用。 - 唤醒方式不同:
wait()需通过notify()/notifyAll()手动唤醒,或线程被中断;sleep()超时后自动唤醒,或线程被中断。 - 使用场景不同:
wait()用于线程间协作阻塞(如生产者-消费者模型);sleep()仅用于模拟耗时操作、线程延时执行,无线程协作能力。
六、线程使用的关键注意事项
- 线程启动必须调用
start()方法,而非直接调用run()方法;直接调用run()方法仅为普通方法调用,不会创建新线程。 - 多线程操作共享资源时,必须保证操作的原子性(如使用
synchronized),否则会出现数据不一致问题。 - 避免使用
while(true)无限循环时的线程空耗:可通过wait()/notifyAll()实现“按需执行”,减少CPU资源占用。 - 线程阻塞后必须保证有唤醒逻辑,否则会出现“线程饿死”,导致线程一直处于阻塞状态无法执行。
- 泛型数组在Java中无法直接创建,因此采用
Object[]数组配合泛型强转实现,通过@SuppressWarnings("unchecked")抑制无关警告。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)