AQS详解

AQS详解

AbstractQueuedSynchronizer(简称AQS,JUC包的核心),Lock系列,Semaphore,FutureTask,CountDownLatch,等等都是基于AQS实现的。

1、 AQS----状态管理

AQS底层使用一个int类型的成员变量来表示同步状态(非结点状态)private volatile int state;(32位)。因此可以有2的32次方个状态,具体如何设置由实现类自己决定。

state的访问方式有三种:

1.getState() 获取当前状态的值,不涉及修改,多线程下,没有影响
2.setState() 设置当前状态的值,非线程安全的。
3.compareAndSetState() 通过 CAS 操作设置状态的值,线程安全的。

AQS—同步队列

同步队列顾名思义,本质是一个队列,符合FIFO(先进先出的原则),里面存放的是将一个个的Node结点,这些Node结点的内部是获取锁失败的线程。也就是说,获取锁失败的线程会包装成一个个结点,放进这个同步队列中。

2、Node结点源码

static final class Node {
    
     // 共享状态
    static final Node SHARED = new Node();  
     //排他状态
    static final Node EXCLUSIVE = null;
     // 当前结点从队列中取消
    static final int CANCELLED =  1;
     // 该节点获取锁释放后,会唤醒后继结点来获取锁
    static final int SIGNAL    = -1;
    //与Condition有关
    static final int CONDITION = -2;
    // 共享式同步状态无条件的传播下去
    static final int PROPAGATE = -3;
    // 结点的等待状态,默认值为0可以设置为以上几种值
    volatile int waitStatus;  
    //前驱
    volatile Node prev;     
    //后继
    volatile Node next;      
    // 当前线程
    volatile Thread thread;  
    //
    Node nextWaiter;

结点的状态是理解AQS的关键,大于0的只有一个就是取消状态,牢记即可。AQS同步队列中的结点线程获取锁的过程采用唤醒的机制,当前持有锁的线程释放锁之后,唤醒紧跟在其后的节点,依次类推。
在这里插入图片描述
采取面向对象的特性,将线程封装为Node,其中包括线程本身,以及当前线程的状态、共享或者排他,前驱指针,后继指针。

何为唤醒机制

线程进入同步队列之后,只要不是第二个结点,就直接睡吧,等你前面的结点执行完了,它自然会唤醒你,然后你再去竞争锁。

3、 独占式锁的获取流程(典型的模板设计模式)

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

首先判断tryAcquire(arg)的返回值结果。

  • true则表示获取锁成功,表达式整体为false,就结束了。因为涉及多线程并发操作,因此方法内部使用cas操作讲state的值由0改为1,并且设置持有锁的线程为当前线程,另外考虑锁重入的情况,state会继续增加,这两种都会返回true,代表获取锁成功,否则的话返回false。
  • false就会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法。
addWaiter(Node.EXCLUSIVE)源码

在这里插入图片描述
这个方法是将获取锁失败的线程包装成一个Node,然后放进CLH队列中。

- 问题: CAS什么情况下会失败?

CAS的操作过程就是判断实际值和预期值相不相等,失败必然是不相等,也就是说,当前结点预期的tail和实际的tail不相等。
什么情况下不相等,自然是别的线程结点尾插进了队列,使得tail指向了这个结点,CAS失败的这个结点,会重新获取tail,然后再重新入队。

enq(final Node node)源码

在这里插入图片描述
这个方法是将包装好的Node链进队列中。因为当第一个获取锁失败的线程执行到这个代码的时候,整个队列还没有进行初始化,也就是没有Head和Tail,因此首先要进行队列的初始化。所以先创建了一个空的Node,并且把设置为让Head和Tail暂时都指向这个空Node。if执行完毕,因为外层是个While循环,因此再次进入走else分支,标准的双向链表尾插操作。

acquireQueued(final Node node, int arg)源码

顾名思义在队列中获取锁。线程被放入队列中后不会立即park,而是首先判断自己是不是第二个节点,如果是的话还会尝试获取锁。

在这里插入图片描述
可以看出只有第二个结点才有资格获取锁,且获取锁成功会将自己设置为头节点,并且把Node中的Thread属性置为null,之前的头节点删除。其它结点压根就没有资格获取锁,根据队列的FIFO原则。

问题:因为队列的FIFO原则,线程是按照排队的顺序依次获取锁的,为什么不特殊处理还是非公平锁?

非公平是因为虽然队列中的结点不会跟第二个结点中的线程抢,但队列外的线程会跟它竞争。竞争成功则会访问共享资源,失败则乖乖排队。

shouldParkAfterFailedAcquire(Node pred, Node node) 源码

pred 前驱结点,node是当前结点。

在这里插入图片描述
这个方法应该算是队列中结点获取锁的关键方法,之前就说过AQS队列基于一种唤醒的方式获取锁,而不是CAS。因为CAS会消耗资源,即使自适应自旋也有很大的随机性。因此AQS采取唤醒的策略来执行。当前结点只要与前驱结点达成协议,即把前驱结点的状态值设置为-1,则前驱结点获取锁成功后,就会唤醒你,说兄弟马上轮到你了快醒醒。

就假如你去理发店理发,前面好几个人在等着,你坐那干等也不是个办法,于是你给排在你前面的第一个人说,大哥,你理完了叫叫我,我先睡一觉。给他说的这个过程就相当于将前驱结点的状态置为-1.就相当于一种协议。但有一种特殊情况,就是你前面排的人可能有急事人家不理了,他待会就要走了,你还让他怎么叫醒你,因此你需要一直向前找,直至找到离你最近的且没有急事的人,让他待会叫你,这个过程就相当于剔除队列中处于取消状态结点的过程。最后,因为多线程的缘故,需要使用CAS将前驱结点的线程状态设置为-1.

parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

将线程阻塞,就是上述情景的睡一觉。

独占式锁的释放流程(典型的模板设计模式)
release(int arg)源码解析

在这里插入图片描述
该过程就相当于前面的人理完发了,然后准备走了,此时叫醒排在他后面的人。

unparkSuccessor(Node node) 唤醒结点
 private void unparkSuccessor(Node node) {  //head
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        // 后继结点为null,或者后继结点的状态为取消状态
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 从尾部向前找,一直找到头,最终的结果就是找到第一个满足t.waitStatus <= 0的
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);  // 唤醒线程
    }

唤醒线程,不一定都可以唤醒,假如后继结点被取消了,或者压根就没有后继结点,就不用唤醒。但如果有只是被取消了,就从后向前找,直到找到最前的那个没有被取消的接地,将它唤醒。
就如理发一样,你现在理完了,之前有人该你说,当你理完发了叫叫他,但你发现他都不见了,因此你只能选择后面排队中的最先排队的叫醒,或者你是最后一个,后面根本就没有人排队了,你自然也不需要唤醒了。直接走人就是了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值