并发编程学习(15)-----自定义同步工具

思维导图:

引言:

    当原生的同步工具类,比如CountDownLauch,Semaphore,ReentrantLock等不能提供我们所需要的功能时,我们就需要自定义一个新的同步工具类了。

    本文将会先介绍如何对状态依赖性进行管理,然后介绍自定义同步类的工具体系。

  • 原理部分:如何对具有状态依赖性的同步工具进行管理
  • 体系部分:介绍java提供的底层的同步体系

一.状态依赖性的管理

    什么是状态依赖性呢?比如阻塞队列获取元素时,队列中必须要有元素,队列要有元素就是一个状态依赖。再比如,我们不能从一个为空的阻塞队列中删除元素。这就是状态依赖性。

    那么,当我们构建自定义的同步工具是,如何处理这种状态依赖性呢?

   现在,我们构建一个实现有界缓存的基类,可以put或者take,我们在以下几个小节中逐渐完善它。

@ThreadSafe
public abstract class BaseBoundedBuffer <V> {
    @GuardedBy("this") private final V[] buf;
    @GuardedBy("this") private int tail;
    @GuardedBy("this") private int head;
    @GuardedBy("this") private int count;

    protected BaseBoundedBuffer(int capacity) {
        this.buf = (V[]) new Object[capacity];
    }

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

    protected synchronized final V doTake() {
        V v = buf[head];
        buf[head] = null;
        if (++head == buf.length) {
            head = 0;
        }
        --count;
        return v;
    }

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

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

1.1 传递失败调用

    当对象的状态不符合我们当前方法的期待时,我们可以将失败的调用传递给上层调用者。例如,我们可以抛出一个异常。  

    但是,如果这样处理的话,上层调用者就必须捕获异常或者进行其他的什么判断。

@ThreadSafe
public class GrumpyBoundedBuffer <V> extends BaseBoundedBuffer<V> {
    public GrumpyBoundedBuffer() {
        this(100);
    }

    public GrumpyBoundedBuffer(int size) {
        super(size);
    }

    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();
    }
}

1.2 利用轮询和睡眠实现阻塞

    我们不能总是让调用方来处理状态依赖性的异常,最好的办法是,当队列中没有元素又需要take时,保持阻塞,直到有元素才处理,或者当队列已满但又需要put时,保持阻塞,直到队列有空闲位置时才处理。

    下面,我们将利用轮询和睡眠来实现简单的阻塞。

@ThreadSafe
public class SleepyBoundedBuffer <V> extends BaseBoundedBuffer<V> {
    int SLEEP_GRANULARITY = 60;

    public SleepyBoundedBuffer() {
        this(100);
    }

    public SleepyBoundedBuffer(int size) {
        super(size);
    }

    public void put(V v) throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isFull()) {
                    doPut(v);
                    return;
                }
            }
            Thread.sleep(SLEEP_GRANULARITY);
        }
    }

    public V take() throws InterruptedException {
        while (true) {
            synchronized (this) {
                if (!isEmpty()) {
                    return doTake();
                }
            }
            Thread.sleep(SLEEP_GRANULARITY);
        }
    }
}

1.3 使用条件队列阻塞

    使用轮询和休眠固然可以实现简单的休眠,但是这样做有一个较大的缺点,那就是,如果在休眠期内,状态依赖发生了改变,那将不会做出任何响应。如何是得挂起的线程在状态依赖改变后立即醒来呢?

    那么如何解决上述问题呢?答案就是使用java的条件队里。所有的队列都是条件队列,队列中储存的是希望关注此对象的线程们。如果要使用某个对象的条件队列,必须先获取此对象的独占锁。然后调用条件队列的API,

  • wait:自动释放锁,并请求操作系统挂起当前线程
  • notify:随机通知并环境条件队列中的一个线程
  • notifyAll :唤醒所有条件队列中的线程

    下面,我么使用条件队列实现这个有界队列。

@ThreadSafe
public class BoundedBuffer <V> extends BaseBoundedBuffer<V> {
    // 条件谓词: not-full (!isFull())
    // 条件谓词: not-empty (!isEmpty())
    public BoundedBuffer() {
        this(100);
    }

    public BoundedBuffer(int size) {
        super(size);
    }

    // 阻塞并直到: not-full
    public synchronized void put(V v) throws InterruptedException {
        while (isFull()) {
            wait();
        }
        doPut(v);
        notifyAll();
    }

    // 阻塞并直到: not-empty
    public synchronized V take() throws InterruptedException {
        while (isEmpty()) {
            wait();
        }
        V v = doTake();
        notifyAll();
        return v;
    }
}

二.底层工具

    这个小节我们将介绍几种实现同步工具类所依赖的几种比较底层的工具。

2.1 条件队列

    在上文中,我们使用条件队列实现了阻塞功能。但是,使用条件队列还有一些其他的问题值得我们关注

  • 条件谓词:条件谓词就是使某个操作称为状态依赖的前提条件。想要正确的使用条件队列,关键是找出对象在哪个条件谓词上等待
  • 过早唤醒:当某个线程被notifyAll或者notify唤醒时,并不代表条件谓词已经成真了,所以,我们必须将wait方在一个while循环中,保证只有条件谓词为真时,才往下执行代码
  • 丢失的信号:指的是线程必须等待一个已经为真的信号,但是在开始等待之前没有检查条件谓词。所以,我们最好在方法执行开始时就对条件谓词进行检查。
  • 通知:notify只会随机通知一个线程,很有可能通知的不是需要此条件谓词的线程。而真正需要判断此条件谓词是否变化的线程有没有通知到,就会发生类似信号丢失的情况,所以,最好使用notifyAll,尽管唤醒所有线程会有一定的性能开销。
  • 子类的安全问题:对于具有状态依赖性处理的类,要么完全关闭方法的继承,有么将条件谓词和通知协议完全公开。
  • 入口协和和出口协议:对于每个依赖状态的操,以及每个修改其他操作所依赖的状态的操作,都应该定义一个入口协议和出口协议,入口协议就是当前操作的条件谓词,出口协议则包括检查被该操作修改的状态变量,并确认是否使某个条件谓词为真,若是,则通知相关条件队列。

2.2 显式的Condition对象

    条件队列具有显式的对象Condition,与对象内置的条件队列不同只和对象的内置锁对应不同,一个显式锁Lock可以和多个Condition对象对应,也会提供比内置条件队列更多的功能。

    请注意,Condition使用signal()替代notify(),使用signalAll()替代notifyAll,但是由于所有的对象都是从Object继承的,所以本身也有notify和notifyAll的方法可供调用,所以使用时务必小心。

   下面,我们使用显式的条件队列实现有界队列:

@ThreadSafe
public class ConditionBoundedBuffer <T> {
    protected final Lock lock = new ReentrantLock();
    // 条件谓词: notFull (count < items.length)
    private final Condition notFull = lock.newCondition();
    // 条件谓词: notEmpty (count > 0)
    private final Condition notEmpty = lock.newCondition();
    private static final int BUFFER_SIZE = 100;
    @GuardedBy("lock") private final T[] items = (T[]) new Object[BUFFER_SIZE];
    @GuardedBy("lock") private int tail, head, count;

    // 阻塞并直到: notFull
    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();
        }
    }

    // 阻塞并直到: notEmpty
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();
            }
            T x = items[head];
            items[head] = null;
            if (++head == items.length) {
                head = 0;
            }
            --count;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

2.3 AbstractQueueSynchronizer

    我们简称AbstractQueueSynchronizer为AQS。如果一个类想成为状态依赖的类,就必须拥有一些状态。AQS就负责管理同步器类中的状态,它管理了一个整数状态信息,可以通过getState,setState,compareAndSetState等protected类型的方法进行操作。

    许多的同步器类都是基于AQS实现的,比如ReetrantReadWriteLock,TeentrantLock,CountDownLuach,Semaphore,FutrueTask。他们都利用AQS所管理的整数状态信息充当自己的状态。

  • TeentrantLock中用于保存获取锁的次数。
  • Semaphore中用于保存当前可用许可的数量。
  • CountDownLatch中用于保存当前的计数值。
  • FutrueTask中用于保存任务的状态
  • ReetrantReadWriteLock中使用qian16位和后16位分别保存写入锁和读取锁的计数

    下面,我们基于AQS实现一个简单的二元锁:

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

    public void signal() {
        sync.releaseShared(0);
    }

    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(0);
    }

    private class Sync extends AbstractQueuedSynchronizer {
        @Override
        protected int tryAcquireShared(int ignored) {
            // 如果闭锁是开的 (state == 1), 那么操作成功,否则失败
            return (getState() == 1) ? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int ignored) {
            // 现在打开闭锁
            setState(1);
            // 现在其他的线程可以获取该闭锁
            return true;

        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章