Java线程通信与并发工具深度解析多线程协同工作的核心在于高效的通信机制。本文深入剖析Java线程通信的基石——wait/notify
机制,对比wait
与sleep
差异,详解单例模式的线程安全实现,并通过阻塞队列实现生产者-消费者模型。
一、wait/notify机制:生产者-消费者模型
核心机制:
/**
* 资源池类,用于演示生产者-消费者模式
*/
class ResourcePool {
// 资源队列,用于存储待消费的资源
private final Queue<Object> queue = new LinkedList<>();
// 资源池的最大容量
private final int CAPACITY = 5;
/**
* 生产者方法,用于向资源池中添加资源
*
* @param item 要添加的资源项
* @throws InterruptedException 如果当前线程被中断,则抛出此异常
*/
public synchronized void produce(Object item) throws InterruptedException {
// 当队列已满时,生产者线程进入等待状态
while (queue.size() == CAPACITY) { // 必须使用 while 循环进行条件检查,防止虚假唤醒
// wait() 会释放锁,并使线程进入 WAITING 状态
wait();
}
// 将资源添加到队列中
queue.offer(item);
// 唤醒所有等待的消费者线程
notifyAll();
}
/**
* 消费者方法,用于从资源池中获取资源
*
* @return 获取到的资源项
* @throws InterruptedException 如果当前线程被中断,则抛出此异常
*/
public synchronized Object consume() throws InterruptedException {
// 当队列为空时,消费者线程进入等待状态
while (queue.isEmpty()) {
// wait() 会释放锁,并使线程进入 WAITING 状态
wait();
}
// 从队列中移除并返回第一个资源项
Object item = queue.poll();
// 唤醒所有等待的生产者线程
notifyAll();
return item;
}
}
执行流程图:
关键点:
- 循环检查条件:
while
防止虚假唤醒(Spurious Wakeup) - 锁绑定:
wait/notify
必须在synchronized
块中 - 唤醒策略:
notify()
:随机唤醒一个等待线程notifyAll()
:唤醒所有等待线程(推荐)
二、wait vs sleep对比
特性 | wait() |
sleep() |
---|---|---|
归属类 | Object 方法 |
Thread 静态方法 |
锁释放 | 释放锁 | 不释放锁 |
唤醒条件 | 需notify() 或超时 |
仅超时唤醒 |
使用场景 | 线程间协调 | 单纯延时操作 |
调用位置 | 必须在同步块中 | 任意位置 |
中断响应 | 抛出InterruptedException |
抛出InterruptedException |
三、单例模式线程安全实现
1. 饿汉式(线程安全)
/**
* 饿汉模式单例类
*/
public class EagerSingleton {
// 在类加载时就初始化单例实例
private static final EagerSingleton INSTANCE = new EagerSingleton();
// 私有构造方法,防止外部通过构造方法创建对象
private EagerSingleton() {
}
/**
* 获取单例对象的方法
* @return 单例对象
*/
public static EagerSingleton getInstance() {
return INSTANCE; // 直接返回预先创建好的单例实例
}
}
特点:
- 类加载时创建对象(可能造成资源浪费)
- 无锁机制,性能高
2. 懒汉式(双重检查锁DCL)
/**
* 懒汉模式单例类(线程安全版)
*/
public class LazySingleton {
// 使用 volatile 关键字禁止指令重排序,保证实例化过程的线程安全性
private static volatile LazySingleton INSTANCE;
// 私有构造方法,防止外部通过构造方法创建对象
private LazySingleton() {
}
/**
* 获取单例对象的方法
*
* @return 单例对象
*/
public static LazySingleton getInstance() {
// 第一次检查:若实例未被创建,则进入同步代码块
if (INSTANCE == null) {
synchronized (LazySingleton.class) { // 使用类对象作为锁,保证线程安全
// 第二次检查:再次确认实例未被创建,防止多线程环境下重复创建
if (INSTANCE == null) {
// 实例化对象:1. 分配内存 2. 初始化对象 3. 将引用指向分配的内存地址
INSTANCE = new LazySingleton();
}
}
}
// 返回单例对象
return INSTANCE;
}
}
volatile的必要性:
防止指令重排序导致返回未初始化对象(JDK5+生效)
3. 静态内部类实现
/**
* 静态内部类单例模式(懒汉模式变体)
*/
public class HolderSingleton {
// 私有构造方法,防止外部通过构造方法创建对象
private HolderSingleton() {
}
/**
* 静态内部类,用于持有单例实例
*/
private static class Holder {
// 在静态内部类加载时,创建单例实例
static final HolderSingleton INSTANCE = new HolderSingleton();
}
/**
* 获取单例对象的方法
*
* @return 单例对象
*/
public static HolderSingleton getInstance() {
// 返回静态内部类中持有的单例实例
return Holder.INSTANCE;
}
}
优点:
- 延迟加载(首次调用
getInstance
时加载Holder
类) - 无锁线程安全
- 避免DCL复杂性
四、阻塞队列实现
1. 手工实现阻塞队列
/**
* 手动实现的阻塞队列类
*
* @param <T> 队列中元素的类型
*/
public class ManualBlockingQueue<T> {
// 存储队列元素的数组
private final Object[] items;
// 当前队列中的元素数量
private int count = 0;
// 元素放入的索引位置
private int putIndex = 0;
// 元素取出的索引位置
private int takeIndex = 0;
// 用于同步操作的锁对象
private final Object lock = new Object();
/**
* 构造方法,用于创建指定容量的阻塞队列
*
* @param capacity 队列的容量
*/
public ManualBlockingQueue(int capacity) {
items = new Object[capacity];
}
/**
* 将元素放入队列中,如果队列已满,则阻塞等待
*
* @param item 要放入队列的元素
* @throws InterruptedException 如果当前线程被中断,则抛出此异常
*/
public void put(T item) throws InterruptedException {
synchronized (lock) {
// 队列满时,生产者线程进入等待状态
while (count == items.length) {
lock.wait(); // 释放锁,进入等待状态
}
// 将元素放入队列
items[putIndex] = item;
// 更新放入索引,采用循环队列的方式
putIndex = (putIndex + 1) % items.length;
// 元素数量加 1
count++;
// 唤醒所有消费者线程
lock.notifyAll();
}
}
/**
* 从队列中取出元素,如果队列为空,则阻塞等待
*
* @return 队列中的元素
* @throws InterruptedException 如果当前线程被中断,则抛出此异常
*/
public T take() throws InterruptedException {
synchronized (lock) {
// 队列空时,消费者线程进入等待状态
while (count == 0) {
lock.wait(); // 释放锁,进入等待状态
}
// 获取队列中的元素
T item = (T) items[takeIndex];
// 更新取出索引,采用循环队列的方式
takeIndex = (takeIndex + 1) % items.length;
// 元素数量减 1
count--;
// 唤醒所有生产者线程
lock.notifyAll();
return item;
}
}
}
2. 标准库BlockingQueue使用
// 创建容量为10的阻塞队列
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 启动生产者线程
new Thread(() -> {
while (true) {
String item = produceItem(); // 生产一个项目
try {
queue.put(item); // 阻塞式入队
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("生产者线程被中断");
}
}
}).start();
// 启动消费者线程
new Thread(() -> {
while (true) {
try {
String item = queue.take(); // 阻塞式出队
processItem(item); // 处理项目
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.out.println("消费者线程被中断");
}
}
}).start();
// 模拟生产项目的操作
private static String produceItem() {
// 模拟生产逻辑,返回生产的项目
return "项目_" + System.currentTimeMillis();
}
// 模拟处理项目的操作
private static void processItem(String item) {
// 模拟处理逻辑
System.out.println("处理项目: " + item);
}
阻塞队列实现对比:
实现方式 | 锁类型 | 数据结构 | 适用场景 |
---|---|---|---|
ArrayBlockingQueue |
单锁ReentrantLock | 数组 | 固定大小队列 |
LinkedBlockingQueue |
双锁分离 | 链表 | 高吞吐量场景 |
PriorityBlockingQueue |
ReentrantLock | 二叉堆 | 优先级队列 |
SynchronousQueue |
CAS算法 | 无缓存 | 直接传递任务 |
关键总结
线程通信三原则:
wait
前用while
检查条件(非if
)notifyAll
优于notify
(防止信号丢失)- 同步块外部的状态检查无效
单例模式选择建议:
- 追求简单:静态内部类实现
- 显式控制延迟加载:DCL+
volatile
- 无特殊要求:枚举单例(保证序列化安全)
public enum EnumSingleton { INSTANCE; // JVM保障单例 }
阻塞队列最佳实践:
- 手工实现:理解底层同步机制
- 生产环境:优先使用
LinkedBlockingQueue
- 高吞吐量:考虑
Disruptor
无锁队列
掌握线程通信机制是构建复杂并发系统的基石。通过
wait/notify
解耦生产消费、保障单例初始化安全、合理选择阻塞队列实现,可显著提升系统的稳定性和可维护性。