7.手把手教你用AQS来实现锁

在使用AQS之前,我们首先需要了解它。

AQS是一个抽象类,不可以被实例化,它的设计之初就是为了让子类通过继承来实现多样的功能的。它内部提供了一个FIFO的等待队列,用于多个线程等待一个事件(锁)。它有一个重要的状态标志——state,该属性是一个int值,表示对象的当前状态(如0表示lock,1表示unlock)。AQS提供了三个protected final的方法来改变state的值,分别是:getState、setState(int)、compareAndSetState(int, int)。根据修饰符,它们是不可以被子类重写的,但可以在子类中进行调用,这也就意味着子类可以根据自己的逻辑来决定如何使用state值。

综上,我们知道AQS有2个宝贝,一个等待队列,还有一个STATE标志,并且通过CAS来改变它的值。

AQS的子类应当被定义为内部类,作为内部的helper对象。事实上,这也是juc种锁的做法,如ReentrantLock,便是通过内部的Sync对象来继承AQS的。AQS中定义了一些未实现的方法(抛出UnsupportedOperationException异常)

tryAcquire(int) 尝试获取state
tryRelease(int) 尝试释放state
tryAcquireShared(int) 共享的方式尝试获取
tryReleaseShared(int) 共享的方式尝试释放
isHeldExclusively() 判断当前是否为独占锁
这些方法是子类需要实现的,可以选择实现其中的一部分。根据实现方式的不同,可以分为两种:独占锁和共享锁。其中JUC中锁的分类为:

独占锁:ReentrantLock、ReentrantReadWriteLock.WriteLock
共享锁:ReentrantReadWriteLock.ReadLock、CountDownLatch、CyclicBarrier、Semaphore
其实现方式为:

独占锁实现的是tryAcquire(int)、tryRelease(int)
共享锁实现的是tryAcquireShared(int)、tryReleaseShared(int)

AQS中还提供了一个内部类ConditionObject,它实现了Condition接口,可以用于await/signal。采用CLH队列的算法,唤醒当前线程的下一个节点对应的线程,而signalAll唤醒所有线程。

总的来说,AQS提供了三个功能:

实现独占锁
实现共享锁
实现Condition模型

综上对AQS的了解,我们大概已经可以把模板搭一下了。

public class MyLock implements Lock {

    Helper helper = new Helper();
    @Override
    public void lock() {
        helper.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        helper.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return helper.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return helper.tryAcquireNanos(1,unit.toNanos(time));
    }

    @Override
    public void unlock() {
        helper.release(1);
    }

    @Override
    public Condition newCondition() {
        return helper.newCondition();
    }

    private class Helper extends AbstractQueuedSynchronizer {
        @Override
        protected boolean tryAcquire(int arg){
           
        }

        @Override
        protected boolean tryRelease(int arg) {
            
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }
}

上面MYLOCK的大多数方法,其实我们根本不用操心,统一交给AQS的方法来帮我们完成就好。而我们真正要实现的就是tryAcquire和tryRelease 这2个操作。

首先我们还是结合前一章的思路来实现tryAcquire,本质就是如果第一个线程进来就拿到锁,后面进来的RETURN FALSE。然而之前我们声明了一个BOOL变量。这里我们可以直接AQS 的STATE变量。我们可以用初始值为0来表示没有线程拿到锁,一旦第一个线程进来了,就把它设置为非0.

protected boolean tryAcquire(int arg){
            //如果第一个线程进来,拿到锁,返回TRUE

            //如果第二个线程进来,返回FALSE,拿不到锁

            int state = getState();
            if(state == 0){
                if(compareAndSetState(0,arg)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }

            return false;
        }

下面我们再来思考释放,因为锁的获取和释放是一一对应的,我们首要判断锁住的线程是不是同一个,如果是,我们就把STATE - ARG。如果STATE回到0,则代表锁释放成功。

protected boolean tryRelease(int arg) {
            //锁的获取和释放需要11对应,那么调用这个方法的线程,一定是当前线程让。
            if(Thread.currentThread() != getExclusiveOwnerThread()){
                throw new RuntimeException();
            }
            int state = getState() - arg;
            setState(state);
            if(state == 0){
                setExclusiveOwnerThread(null);
                return true;
            }
            return false;
        }

下面我们就可以用之前的那个测试类Sequence来测试下我们写的锁了。
可以发现输出的是50,锁是成功的。

然后去测试重入锁的DEMO,发现会卡主。我们要再加一些逻辑来实现重入锁。

protected boolean tryAcquire(int arg){
            //如果第一个线程进来,拿到锁,返回TRUE

            //如果第二个线程进来,返回FALSE,拿不到锁

            Thread t = Thread.currentThread();
            int state = getState();
            if(state == 0){
                if(compareAndSetState(0,arg)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
            }else if(getExclusiveOwnerThread() == t){
                //因为只有一个线程可以进这个IF,所以没有线程安全性问题
                setState(state+arg);
                return true;
            }

            return false;
        }

然后就实现好了,现在DEMO也能过了。

你可能感兴趣的:(7.手把手教你用AQS来实现锁)