JAVA并发编程实战-构建自定义的同步工具

思维导图

在这里插入图片描述

1 管理状态依赖性

在顺序结构中,如果类的先验条件无法满足,就会标为失败。但是在并发程序中,会被其它线程活动所改变。

1.1 示例:将先验条件失败传给调用者

如demo-1是一个有限缓存base类实现。

public abstract class BaseBoundedBuffer<V> {
    private final V[] buffer;
    private int head;
    private int tail;
    private int count;

    protected BaseBoundedBuffer(int capacity) {
        buffer = (V[]) new Object[capacity];
    }

    protected synchronized void doPut(V v) {
        buffer[tail] = v;
        if (++tail == buffer.length) {
            tail = 0;
        }
        count++;
    }

    protected synchronized V doTake() {
        V result = buffer[head];
        if (++head == buffer.length) {
            head = 0;
        }
        count--;
        return result;
    }

    public synchronized boolean isEmpty() {
        return count == 0;
    }

    public synchronized boolean isFull() {
        return count == buffer.length;
    }
}

检查再运行,故方法均需要同步。

demo-2是一个base类的实现:

/**
 * 检查再运行
 * @param <V> 类型
 */
public class GrumpyBoundBuffer<V> extends BaseBoundedBuffer<V>{
    protected GrumpyBoundBuffer(int capacity) {
        super(capacity);
    }

    public synchronized void put(V v) throws BufferFullException {
        if (isFull()) {
            throw new BufferFullException();
        }
        doPut(v);
    }

    public synchronized V take() throws BufferEmptyException {
        if (isEmpty()) {
            throw new BufferEmptyException();
        }
        return doTake();
    }

    //调用GurmpyBoundBuffer的客户端逻辑
    public void client() throws InterruptedException {
        GrumpyBoundBuffer<String> stringGrumpyBoundBuffer = new GrumpyBoundBuffer<>(100);
        while (true) {
            try {
                String take = stringGrumpyBoundBuffer.take();
                //处理take
            } catch (BufferEmptyException e) {
                //空队列,休眠在尝试
                Thread.sleep(200);
                //或者直接重试,不用忙等待
                //或者取两者折中使用yield,放弃cpu重新进入就绪队列
                Thread.yield();
            }
        }
    }
}

这里先验条件失败:缓存满或者缓存空,都将会视为失败,抛给调用者异常。我们有两种方式处理异常:

  • 忙等待:调用者直接进行重试,不停自旋,该方法消耗cpu,适用短期可以成功。
  • 采取休眠措施,防止消耗cpu,但是可能错过依赖条件改变。

1.2 轮询+休眠实现阻塞

如demo-3实现了封装轮询+休眠重试机制:

public class SleepyBound<V> extends BaseBoundedBuffer<V> {

    protected SleepyBound(int capacity) {
        super(capacity);
    }

    //轮旋+休眠
    public void put(V v) throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isFull()) {
                    doPut(v);
                    return;
                }
            }
            Thread.sleep(500);
        }
    }

    //轮旋+休眠
    public V take() throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isEmpty()) {
                    return doTake();
                }
            }
            Thread.sleep(500);
        }
    }
}

该方法相比demo-2有所改善,调用者不必处理失败和重试,这些已经有缓存实现了。

这里我们需要再响应性和cpu使用率做出权衡:休眠间隔越小,响应性越好,单cpu消耗越高。

1.3 让条件队列来解决这一切

条件队列可以让一组线程(等待集)——以某种方式等待相关条件变为真。

java每个对象都可以作为锁,也可以作为条件队列,Object的wait、notify和notifyAll就是内部队列的API。

  • wait:释放锁,并请求OS挂起。
  • notify:通知调用notify的对象的其中一个挂起线程进行唤醒。
  • notifyAll:通知所有等待线程进行唤醒。

下列demo-4利用了wait和notify实现有限队列:

/**
 * 通过wait notifyAll阻塞和唤醒对象上的线程
 * @param <V>
 */
public class BoundBuffer<V> extends BaseBoundedBuffer<V> {
    protected BoundBuffer(int capacity) {
        super(capacity);
    }

    public synchronized void put(V v) throws InterruptedException {
        //阻塞唤醒后必须再次测试条件谓词
        while (isFull()) {
            wait();
        }
        doPut(v);
        notifyAll();
    }

    public synchronized V take() throws InterruptedException {
        //阻塞唤醒后必须再次测试条件谓词
        while (isEmpty()) {
            wait();
        }
        V result = doTake();
        notifyAll();
        return result;

    }
}

这里使用对象的条件队列,可以更高效的唤醒,只有依赖条件变化才会唤醒等待线程。

2 使用条件队列

2.1 条件谓词

条件谓词是先验条件的第一站,它在一个操作和状态之间建立了依赖关系。

将条件谓词和与之关联的条件队列,以及在条件队列种等待的操作,都写入文档。

一个重要的三元关系:条件谓词涉及状态变量,状态变量由锁保护,所以在测试条件谓词之前需要持有锁。

锁对象和条件队列对象(这里是wait和notify对象)需要是同一个对象。

2.2 过早的唤醒

一个单独的条件队列可以和多个条件谓词共同使用。

这也就意味着,当notifyAll唤醒你的线程时,不一定你需要的条件谓词已经满足。

因此当wait唤醒时,都必须再次测试谓词条件。

一个等待条件谓词满足的规范式如下:

private final Object obj = new Object();
    //条件等待的规范式
    public void stateDependentMethod() throws InterruptedException {
        //条件谓词必须由锁保护
        synchronized (obj) {
            while (!conditionPredicate()) {
                obj.wait();
            }
            //处理对象
        }
    }

当使用条件等待时:

  • 永远设置一个条件谓词——线程执行前必须满足它。
  • 永远在调用wait前测试条件谓词,在唤醒后再次判断条件谓词。
  • 永远在循环中调用wait。
  • 确保条件谓词的状态变量由锁保护,而这个锁与条件队列相关联。
  • 当调用wait、notify和notifyAll要持有与条件队列关联的锁,并且在检查条件谓词之后,执行被保护的逻辑之前,不要释放锁。

2.3 丢失的信号

当一个线程等待的信号变为真,但是在等待前检查条件谓词却变为了假,也就是丢失信号。

2.4 通知

无论合适,当你在等待一个条件时,一定要确保有人会在条件谓词变为真时通知你。

demo-4说明了为何使用nofityAll而不是notify通知对象,因为这里涉及到两个条件谓词非空和非满的判断,如果单一唤醒一个线程,可能出现信号丢失。

只有满足下列条件,才能用单一的notify代替notifyAll:

  • 相同的等待者,只有一个条件谓词与条件队列关联,每个线程从wait唤醒后执行相同的等待逻辑。
  • 一个对条件队列的通知,至多只激活一个线程。

2.5 示例:阀门类

使用条件等待的阀门类,如demo-5:

public class ThreadGate {
    private boolean isOpen;
    private int generation;

    public synchronized void close() {
        isOpen = false;
    }

    public synchronized void open() {
        isOpen = true;
        generation++;
        notifyAll();
    }

    public synchronized void await() throws InterruptedException {
        int awaitGeneration = this.generation;
        //防止快速打开,快速关闭,导致所有线程无法被释放。
        while (!isOpen && awaitGeneration == this.generation) {
            wait();
        }
        //做action
    }
}

每次开启使用自增的generation主要是防止快速的打开关闭,导致线程无法被唤醒。

2.6 子类的安全问题

一个依赖于状态的类,要么将它的等待和通知暴露给子类,要么完全阻止子类参与。

3 显式的Condition对象

Lock是广义的内部锁,Condition是广义的内部条件队列。
Condition条件队列的接口如下:

`

demo-6是使用Condition来构建缓存队列的方式:

public class ConditionBoundedBuffer<V> {
    protected final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final V[] buffer;
    private int head, tail, count;

    public ConditionBoundedBuffer(int capacity) {
        buffer = (V[]) new Object[capacity];
    }

    public void put(V v) throws InterruptedException {
        lock.lock();
        try {
            while (count == buffer.length) {
                notFull.await();
            }
            buffer[tail] = v;
            ++count;
            if (++tail == buffer.length) {
                tail = 0;
            }
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public V take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            V result = buffer[head];
            --count;
            if (++head  == buffer.length) {
                head = 0;
            }
            notFull.signal();
            return result;
        } finally {
            lock.unlock();
        }
    }
}

这里可以看出相比wait的好处,一个锁可以由多个Condition条件队列,可以更精确的唤醒等待线程,减少相当数量的上下文转化。

4 剖析Synchronizer

我们经常使用的同步器基本都是基于AbstractQueuedSynchronizer,我们也可以根据这些基于类,进行扩展自己的同步器,比如demo-7的一个计数计数器:

/**
 * 基于锁和条件实现一个信号量
 */
public class SemaphoreOnLock {
    private final Lock lock = new ReentrantLock();
    private final Condition permitCondition = lock.newCondition();
    private int permits;

    /**加锁主要是保证permits可见性。
     *
     * @param permitCount 信号数
     */
    public SemaphoreOnLock(int permitCount) {
        lock.lock();
        try {
            permits = permitCount;
        } finally {
            lock.unlock();
        }
    }

    public void acquire() throws InterruptedException {
        lock.lock();
        try {
            while (permits == 0) {
                permitCondition.await();
            }
            --permits;
        } finally {
            lock.unlock();
        }
    }

    public void release() {
        lock.lock();
        try {
            ++permits;
            permitCondition.signal();
        } finally {
            lock.unlock();
        }
    }
}

通过利用lock和permit确保信号量的获取和释放。

5 AbstractQueuedSynchronizer

一个基于AQS的同步器基本操作,都是一些不同形式的获取和释放。AQS维护了对状态的管理,和条件队列的维护。

AQS的获取和释放基本操作如下所示:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

其中tryAcquire和tryRelease需要子类进行实现。而阻塞队列和唤醒线程不需要我们关心。

一个获取操作通常包括两步:

  • 判断当前状态是否允许获取,允许就让线程执行。否则就阻塞线程。
  • 可能进行的状态更新。

5.1 一个简单的闭锁

demo-8是一个依赖AQS实现的闭锁:

public class OneShotLatch {
    private final Sync sync = new Sync();

    /**
     * tryReleaseShared设置状态为1,返回true,会唤醒所有等待的线程
     */
    public void signal() {
        sync.releaseShared(0);
    }

    /**
     * 初始时都会阻塞,因为tryAcquireShared<0
     * @throws InterruptedException
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(0);
    }


    private class Sync extends AbstractQueuedSynchronizer{

        @Override
        protected boolean tryReleaseShared(int arg) {
            setState(1);
            return true;
        }

        @Override
        protected int tryAcquireShared(int arg) {
            return getState() == 1 ? 1:-1;
        }
    }
}

通过实现AQS的模板方法来完成对线程的阻塞和唤醒。await和signal相当于获取和释放

6 JUC中实现AQS的同步器

6.1 ReentrantLock

只支持独占的操作。实现的tryAcquire和tryRelease如下:
在这里插入图片描述
这里判断状态变量如果为0试图进行原子交换,如果成功获取锁,否则失败。进入阻塞队列等待唤醒。

在这里插入图片描述
这里需要判断是否当前线程有资格进行释放,之后再进行释放操作修改状态等。

6.2 Semaphore和CountDownLatch

两者实现方式类似,都是通过维护一个许可,来进行判断操作是否成功。

Semaphore的实现acquire:
在这里插入图片描述
通过判断获取后是否小于0进行操作,如果小于0则直接获取失败,进行阻塞,否则,尝试原子交换,成功的话,返回获取成功,失败则再次进行循环。

释放操作:

在这里插入图片描述
这里释放就是简单的循环原子更改状态变量。然后由AQS唤醒阻塞的线程。

参考文献

[1]. 《JAVA并发编程实战》.

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

LamaxiyaFc

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

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

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

打赏作者

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

抵扣说明:

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

余额充值