Java并发实践:构建自定义同步器的艺术

Java并发实践:构建自定义同步器的艺术

booknotes A collection of my book notes on various computer science books booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

引言

在多线程编程中,同步器是协调线程间通信和同步的重要工具。Java标准库提供了丰富的同步器如ReentrantLockSemaphore等,但在某些特殊场景下,我们需要构建自己的同步器。本章将深入探讨如何构建高效、可靠的自定义同步器。

状态依赖类的基础概念

状态依赖类是指那些操作执行前需要满足特定前置条件的类。例如:

  • 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的原因:

  1. 虚假唤醒wait()可能意外返回
  2. 过早唤醒:其他线程可能在你被唤醒后改变了状态

通知机制

  • notifyAll():唤醒所有等待线程(安全但可能低效)
  • notify():仅唤醒一个线程(高效但容易出错)

最佳实践:除非满足以下条件,否则总是使用notifyAll()

  1. 所有等待线程等待的是同一个条件谓词
  2. 每次通知最多只需要一个线程继续执行

显式条件对象(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核心概念

  1. 状态管理:提供一个int类型的状态变量
  2. 获取/释放操作
    • 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实现

  1. ReentrantLock:使用状态表示重入次数
  2. Semaphore:使用状态表示可用许可数
  3. CountDownLatch:使用状态表示剩余计数
  4. FutureTask:使用状态表示任务状态
  5. ReentrantReadWriteLock:使用状态的高16位表示读锁,低16位表示写锁

构建自定义同步器的建议

  1. 优先使用现有同步器:除非有特殊需求,否则不要重复造轮子
  2. 文档化同步策略:特别是允许子类化时
  3. 封装条件队列:避免暴露给客户端代码
  4. 考虑性能:在正确性的基础上优化通知策略
  5. 测试多线程场景:同步器容易隐藏并发问题

结语

构建自定义同步器是Java并发编程中的高级主题,需要深入理解线程交互、内存可见性和性能考量。通过合理使用条件队列和AQS框架,我们可以创建出既安全又高效的同步组件。记住:在并发世界中,正确性永远比性能更重要!

booknotes A collection of my book notes on various computer science books booknotes 项目地址: https://gitcode.com/gh_mirrors/bo/booknotes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马安柯Lorelei

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值