JUC包中锁的实现原理——AQS

锁是java同步的重要机制,在之前的博客中写了一些关于锁的内容,但比较浅显,这里从源码的角度来分析一下java中方的锁。java中的锁主要有两种:Synchronized和ReenTrarantLock,前者是从jvm的层面实现的,后者是从代码的层面实现的,因此这篇文章的主要对象是针对后者。谈到ReenTrarantLock的实现,就不得不提到AQS。

AbstractQueuedSynchronizer抽象同步队列简称AQS,他是实现同步器的基础组件,并发包中的许多组件CountDownLatch、CycleBarrier、Semaphore都是基于AQS实现的,虽然在实际的项目中很少用到,但是其架构设计还是值得学习的,下面是一张AQS的主要类图结构,只包含了主要内容:

AQS的主要思想就是维护一个单一的状态信息state,通过getState(),setState(),compareAndSetState()方法来修改其值。对于AQS来说,线程同步的关键就是对状态值state进行操作,根据操作state的方式,又分为独享锁和共享锁;在独占方式下的获取和释放资源的方法为:acquire()和release(),在共享方式下的获取和释放资源的方法为:acquireShared()和releaseShared()。

使用独占方式获取的资源是与具体线程绑定的,就是如果一个线程获取了资源,就会标记是这个线程获取到了,其他线程再尝试操作state时会发现当前资源不是自己持有的,就会在获取失败后阻塞,然后该线程会被包装为一个Node节点,加入到AQS阻塞队列当中,直到持有state的线程释放资源,该线程会被唤醒继续争取资源。比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock后,在AQS内部会使用CAS操作把state状态值从0改为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁的时候发现它就是锁的持有者,则会把状态值从1改为2,也就是设置了重入次数为2,当另一个线程获取锁时发现自己并不是锁的持有者就会被放入AQS阻塞队列后挂起。

在独占方式下,获取与释放资源的流程如下:

(1)当一个线程调用acquire(int arg)方法获取资源时,会先尝试使用tryAcquire方法获取资源,如成果则直接返回,失败则将当前线程封装为Node.EXCLUSIVE的Node节点插入到AQS阻塞队列的尾部。

    public final void acquire(int arg) {
        //尝试使用tryAcquire方法获取资源,具体就是设置state状态的值
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

(2)当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,设置状态变量state的值,然后激活AQS阻塞队列中一个被阻塞的线程,被激活的线程调用tryAcquire方法进行尝试,看当前状态变量state的值是否为0,若为0,则该线程被激活,继续向下运行,否则会被放入AQS队列被挂起。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            如果头节点不为null且状态值不为0,则唤醒一个阻塞线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

     protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

在这里强调一下,AQS没有提供可用的tryAcquire和tryRelease方法,因为它是一个基础框架,这两个方法需要由具体的子类来实现;比如实现AQS的ReentrentLock,定义status为0时表示锁空闲,为1时表示锁被占用,则在实现tryAcquire方法时,需要使用CAS算法查看当前state是否为0,如果为0则修改为1,并将当前锁的持有线程设置为当前线程,关于ReentrantLock我会在以后的博文中进行详细讲解。

在共享方式下,获取与释放锁的流程如下:

(1)当线程调用acquireShared(int arg)获取共享资源时,会先使用tryAcquiredShared方法尝试获取资源,具体是设置state变量的值,若成功则直接返回,失败则将当前线程封装为Node.SHARED的Node节点插入到AQS阻塞队列末尾。

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

(2)当一个线程调用releaseShared(int arg)方法时会尝试使用tryReleaseShared操作释放资源,设置状态变量state的值,然后激活AQS阻塞队列中一个被阻塞的线程,被激活的线程调用tryAcquireShared方法进行尝试,看当前状态变量state的值是否为0,若为0,则该线程被激活,继续向下运行,否则会被放入AQS队列被挂起。

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }

AQS的阻塞队列

在之前的操作中,我们反复提及节点和阻塞队列,下面我们来看看这些数据的结构,Node是AQS的一个内部类,封装竞争资源失败被阻塞的线程,当线程竞争失败时,会调用enq()方法将该线程放入阻塞队列末尾,AQS提供一个双向队列存储Node节点。

static final class Node {

        //表示线程已被取消
        static final int CANCELLED =  1;
        //表示线程需要被唤醒
        static final int SIGNAL    = -1;
        //线程在条件队列中等待
        static final int CONDITION = -2;
        //线程释放资源时需要通知其它节点
        static final int PROPAGATE = -3;
        //封装线程的状态,有如上几种
        volatile int waitStatus;
        //线程对应节点的前节点
        volatile Node prev;

        //线程对应节点的后节点
        volatile Node next;

        //下一个在条件队列中等待的节点
        Node nextWaiter;

        //AQS提供的入队操作
        private Node enq(final Node node) {
        //经典的无限循环
        for (;;) {
            Node t = tail;
            //若头尾节点都为null,设置一个标记节点(不含内容),并让首尾节点都指向该节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                CAS设置当前节点为最后一个节点,并返回
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
}

AQS的条件队列

大家都知道,notify和wait是配和Synchronized关键字实现线程同步的基础设施,同样的,条件变量的signal和await方法也是配额和AQS实现的锁来实现线程同步的基础设施,先通过一个例子来看看条件变量是什么:

    ReentrantLock lock=new ReentrantLock();
    //创建了一个ConditionObject对象,即所谓的条件变量
    Condition condition=lock.newCondition();

    lock.lock();
    try{
        System.out.println("begin");
        //调用条件变量awai()方法阻塞当前线程,直到其它线程调用signal()方法唤醒
        condition.await();
        System.out.println("end");
    }catch(InterruptedException e){
        e.printStackTrace();
    }finally{
        lock.unlock();
    }
    
    lock.lock();
    try{
        System.out.println("begin");
        //调用条件变量的signa()方法唤醒一个线程
        condition.signal();
        System.out.println("end");
    }catch(InterruptedException e){
        e.printStackTrace();
    }finally{
        lock.unlock();
    }

通过这个例子,大家应该已经明白了什么是条件变量,我们来看看条件变量的数据结构,注释已经解释的比较清楚。ConditionObject是AQS的内部类,能够访问AQS的变量state和方法,在每个条件变量内部对维护了一个条件队列,用来存放调用条件变量的await()方法而被阻塞的线程,注意条件队列是针对条件变量的,AQS阻塞队列是针对整个AQS的。

public class ConditionObject implements Condition, java.io.Serializable {

        private static final long serialVersionUID = 1173984872572414699L;
        
        //条件等待队列的首节点
        private transient Node firstWaiter;

        //条件等待队列的尾节点
        private transient Node lastWaiter;

        //调用该方法的线程会被放入条件变量的阻塞队列
        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //创建节点,并插入到条件队列末尾
            Node node = addConditionWaiter();
            //释放当前线程获取的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                //调用park方法阻塞该线程
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

        //唤醒条件队列中被阻塞的线程
        public final void signal() {
            //如果不是独占,抛出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                //将条件队列首个节点移动到AQS阻塞队列,准备争取锁资源
                doSignal(first);
        }

        //将一个被封装的线程加入条件等待队列末尾
        private Node addConditionWaiter() {
            Node t = lastWaiter;
            // 如果最后一个节点被取消,则删除
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //将当前线程封装为一个节点加入到条件队列末尾,并返回
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }
}

总结一下工作流程:当多个线程调用lock()方法获取锁时,只有一个线程获得了锁,其它节点会被封装为Node节点插入到AQS阻塞队列末尾,并做CAS尝试获取锁;这时如果获得了锁的线程调用了await()方法,则会释放获得的锁,并被转换为Node节点插入到该条件变量的条件队列里面,这时因为调用lock()方法被阻塞到AQS阻塞队列中的一个线程会得到锁,如果该线程调用了调用变量的signal()方法,则会将条件队列里面的一个Node节点移入到AQS阻塞队列,继续争夺锁资源。

最后我们基于AQS实现一个自定义锁,完成生产者-消费者模型:


//基于AQS实现一个自定义锁
public class NonReentrantLock implements Lock,java.io.Serializable {

    private static class Sync extends AbstractQueuedSynchronizer{
        //判断锁是否被其它线程持有
        protected boolean isHeldExclusively(){
            return getState()==1;
        }

        public boolean tryAcquire(int acquires){
            assert acquires==1;
            //尝试使用CAS修改state,若成功则获得锁,并设置该线程为锁的持有者
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int releases){
            assert releases==1;
            if(getState()==0){
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

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

    private final Sync sync=new Sync();

    @Override
    public void lock() {
        sync.acquire(1);
    }

    public boolean isLocked(){
        return sync.isHeldExclusively();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

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

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

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

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


//基于自定义锁实现生产者-消费者模型
public class ReetrantLockList {
    private ArrayList<String> arrayList=new ArrayList<>();

    private volatile ReentrantLock reentrantLock=new ReentrantLock();

    public void add(String e){
        reentrantLock.lock();
        try {
            arrayList.add(e);
        }finally {
            reentrantLock.unlock();
        }
    }

    public void remove(String e){
        reentrantLock.lock();
        try {
            arrayList.remove(e);
        }finally {
            reentrantLock.unlock();
        }
    }

    public String get(int index){
        reentrantLock.lock();
        try {
            return arrayList.get(index);
        }finally {
            reentrantLock.unlock();
        }
    }
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值