Lock锁之AQS

AQS是一种设计的思想,通过一个锁变量(一般是int的变量),修饰为volatile保证各线程可见,获取锁相当于修改这个锁变量,一般修改通过CAS来实现,修改成功即持有锁,否则进入等待队列(等待队列是一个双向链表)的队尾

java里的Lock锁实现对象,其中控制锁资源的操作大部分都是基于AQS的思想,下面以Sync类来例子来介绍AQS的设计思想

进入ReentrantLock(可重入锁)的代码,可以看到他维护了一个Sync变量

ReentrantLock实现的阻塞/唤醒操作都是基于这个类的,下面我们从lock方法入手,一步步解析他是如何实现“锁操作”的(下述代码基于公平锁的实现)

Lock()

    // ReentrantLock
    public void lock() {
        sync.lock();
    }
    
    // Sync
    final void lock() {
        acquire(1);
    }

我们平常调用的lock方法实际都是调用acquire方法,传入的参数为1

acquire()

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire方法涉及到三个操作

  • tryAcquire:尝试获取锁
  • addWaiter:加入等待队列
  • acquireQueued:后续操作

上面if条件逻辑为:如果获取锁失败,那么加入等待队列,并进行后续操作(阻塞并等待被唤醒),selfInterrupt是设置当前线程的一个中断操作(调用Thread.currentThread().interrupt())

tryAcquire()

tryAcquire方法是一个基于CAS的操作,通过尝试修改一个资源变量,来判断当前线程是否可以持有锁

首先需要判断当前锁资源状态是否为0(getState获取锁资源状态),如果为0说明锁处于释放状态,然后会尝试取获取锁(compareAndSetState是一个CAS操作,尝试把0修改为1)

注:这里可以看到有个hasQueuedPredecessors方法,这个是公平锁用来保证公平性的方法(非公平锁只有CAS操作)

修改锁资源变量成功后,会将当前线程设置到exclusiveOwnerThread变量中,后续用于判断是否可重入

如果资源变量不是0,那么会判断当前线程是否是占有锁资源的线程,如果是的话就将state+1(重入)

只要返回true就说明锁占有成功,否则失败

addWaiter()

如果获取锁失败就会进入addWaiter方法将当前线程包装为Node节点,这里传入的mode是Node.EXCLUSIVE独占模式,意思是只允许一个线程操作(另一种是共享模式,如CountDownLatch)

addWaiter方法会将线程包装为node节点并插入到队列的末端,这里的队列是一个虚拟队列,即节点通过pre和next指针来形成队列(CLH队列),并没有实际的队列实现类

如果队列已经存在节点,那么直接尝试插入队尾,如果插入失败,通过enq方法循环插入

enq也是一个初始化方法,当队列不存在节点会初始化一个Node(这个node没有任何含义,通过new出来的)

acquireQueued() - 阻塞

acquireQueued是对队列的一些操作,也可以理解为是从队列中取节点的操作,阻塞和唤醒都是在这个方法里完成的,所以里面是一个死循环。

这个方法会通过两个部分去介绍,分别对应阻塞和唤醒

这里我们先不去关注里面定义的局部变量,直接看循环体内,node.predecessor()是获取节点前继节点的方法,得到前继节点p后,获取判断该节点是否为头节点,如果是头节点那么会尝试去获取锁。

这里有个问题,**为什么要判断前继节点是不是头节点,而不是判断自己本身是不是头节点呢?**还记得enq方法里的初始化嘛,初始化的时候头节点是直接new出来的,并没有实际意义(也没有跟线程绑定),所以我们应该是从头节点的下一个节点开始操作。

这里我们假设获取锁失败,那么最后就会进入shouldParkAfterFailedAcquire方法,这个方法是用来确保当前的节点在阻塞后,能够被唤醒(通过设置前继节点p的状态waitStatus,这里不作赘述),确保该节点能够被唤醒后,就会调用我们的parkAndCheckInterrupt进行阻塞操作

LockSupport.park是将线程阻塞的操作,此时我们的线程就停在了这个方法的位置,让出cpu时间,进入了漫长的等待

unLock()

现在来看下释放锁的过程

    // ReentrantLock
    public void unlock() {
        sync.release(1);
    }
    // sync
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

上述代码中,tryRelease是尝试释放锁,unparkSuccessor是唤醒我们阻塞的线程

tryRelease

这里会判断持有锁的线程是不是当前线程,如果是的话,就会将state减1,直到减为0,才释放锁,这就是可重入锁的特性(同时可以看到,如果不是持有该锁的线程调用该方法,那么就会抛异常)

unparkSuccessor

释放锁成功后,就会调用unparkSuccessor来唤醒我们的线程

    private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 可以看到操作的是头节点的下一个节点
        Node s = node.next;
        // 如果该节点waitStatus > 0 表示注销状态 则从尾部往前取第一个不是注销状态的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            // 唤醒!
            LockSupport.unpark(s.thread);
    }

这里涉及比较多状态量的判断,这里不作过多赘述

acquireQueued() - 唤醒

重新回到这个方法,这里我们被唤醒后,又会尝试取获取锁,假设这次我们获取到了锁

setHead方法会将当前节点设置为头节点,并将线程信息置空,所以每次操作都是取头节点的下一个节点(因为头节点相当于一个傀儡节点)

退到最外层后,如果acquireQueued返回是true,那么会调用selfInterrupt,这是设置当前线程的中断状态,只有当线程是因为中断被唤醒,acquireQueued才是返回true(涉及中断的知识读者自行查阅)

至此我们获取到lock锁,继续执行后续代码~

  • 38
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值