Java并发实践:构建自定义同步器的艺术
引言
在多线程编程中,同步器是协调线程间通信和同步的重要工具。Java标准库提供了丰富的同步器如ReentrantLock
、Semaphore
等,但在某些特殊场景下,我们需要构建自己的同步器。本章将深入探讨如何构建高效、可靠的自定义同步器。
状态依赖类的基础概念
状态依赖类是指那些操作执行前需要满足特定前置条件的类。例如:
FutureTask
必须在完成后才能调用get()
方法- 阻塞队列在满时不能插入元素,在空时不能移除元素
在多线程环境中,处理状态依赖比单线程复杂得多,因为前置条件可能因其他线程的操作而改变。
状态依赖的三种实现方式
1. 异常传递模式
@ThreadSafe
public class GrumpyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
// 当条件不满足时抛出异常
public synchronized void put(V v) throws BufferFullException {
if (isFull()) throw new BufferFullException();
doPut(v);
}
}
缺点:调用方需要处理异常并实现重试逻辑,代码冗长且效率低。
2. 轮询休眠模式
@ThreadSafe
public class SleepyBoundedBuffer<V> extends BaseBoundedBuffer<V> {
public void put(V v) throws InterruptedException {
while (true) {
synchronized (this) {
if (!isFull()) {
doPut(v);
return;
}
}
Thread.sleep(SLEEP_GRANULARITY);
}
}
}
改进点:将阻塞逻辑封装在类内部,简化调用方代码。
缺点:休眠时间难以确定,响应性差。
3. 条件队列模式(推荐)
@ThreadSafe
public class BoundedBuffer<V> extends BaseBoundedBuffer<V> {
public synchronized void put(V v) throws InterruptedException {
while (isFull()) wait(); // 等待非满条件
doPut(v);
notifyAll(); // 通知可能等待的消费者
}
}
优势:
- 高效:线程在条件不满足时挂起,不消耗CPU
- 响应快:条件满足时立即唤醒
条件队列的正确使用姿势
条件谓词(Condition Predicate)
条件谓词是决定线程是否应该等待的关键布尔表达式。例如:
- 对于
take()
操作:!isEmpty()
- 对于
put()
操作:!isFull()
黄金法则:必须在持有锁的情况下检查条件谓词,并在调用wait()
时继续持有锁。
等待的标准范式
synchronized(lock) {
while (!conditionPredicate()) {
lock.wait();
}
// 执行条件满足后的操作
}
使用while
而非if
的原因:
- 虚假唤醒:
wait()
可能意外返回 - 过早唤醒:其他线程可能在你被唤醒后改变了状态
通知机制
notifyAll()
:唤醒所有等待线程(安全但可能低效)notify()
:仅唤醒一个线程(高效但容易出错)
最佳实践:除非满足以下条件,否则总是使用notifyAll()
- 所有等待线程等待的是同一个条件谓词
- 每次通知最多只需要一个线程继续执行
显式条件对象(Condition)
Java还提供了更灵活的Condition
接口,它是内置条件队列的增强版:
Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
优势:
- 一个锁可以关联多个条件队列
- 提供更丰富的等待方法(如超时、不可中断等)
- 更好的控制条件队列的可见性
示例:
public void put(T x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待非满条件
items[tail] = x;
if (++tail == items.length) tail = 0;
++count;
notEmpty.signal(); // 通知可能等待的消费者
} finally {
lock.unlock();
}
}
AbstractQueuedSynchronizer(AQS)框架
AQS是构建大多数Java同步器的基础框架,它封装了复杂的同步和排队机制。
AQS核心概念
- 状态管理:提供一个
int
类型的状态变量 - 获取/释放操作:
acquire
:可能阻塞直到状态允许release
:改变状态并唤醒等待线程
实现一次性门闩示例
public class OneShotLatch {
private final Sync sync = new Sync();
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(0);
}
public void signal() {
sync.releaseShared(0);
}
private class Sync extends AbstractQueuedSynchronizer {
protected int tryAcquireShared(int ignored) {
return (getState() == 1) ? 1 : -1; // 门闩打开时成功
}
protected boolean tryReleaseShared(int ignored) {
setState(1); // 打开门闩
return true; // 允许其他线程继续
}
}
}
标准库同步器的AQS实现
- ReentrantLock:使用状态表示重入次数
- Semaphore:使用状态表示可用许可数
- CountDownLatch:使用状态表示剩余计数
- FutureTask:使用状态表示任务状态
- ReentrantReadWriteLock:使用状态的高16位表示读锁,低16位表示写锁
构建自定义同步器的建议
- 优先使用现有同步器:除非有特殊需求,否则不要重复造轮子
- 文档化同步策略:特别是允许子类化时
- 封装条件队列:避免暴露给客户端代码
- 考虑性能:在正确性的基础上优化通知策略
- 测试多线程场景:同步器容易隐藏并发问题
结语
构建自定义同步器是Java并发编程中的高级主题,需要深入理解线程交互、内存可见性和性能考量。通过合理使用条件队列和AQS框架,我们可以创建出既安全又高效的同步组件。记住:在并发世界中,正确性永远比性能更重要!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考