AQS 队列原理

Lock 使用范式

synchronized 有标准用法,这样的优良传统咱 Lock 也得有,相信很多人都知道使用 Lock 的一个范式

Lock lock = new ReentrantLock();

lock.lock();

try{

 ...

}finally{

 lock.unlock();

}


标准1—finally 中释放锁既然是范式(没事不要挑战更改写法的那种),肯定有其理由,我们来看一下

这个大家应该都会明白,在 finally 中释放锁,目的是保证在获取到锁之后,最终能被释放

标准2—在 try{} 外面获取锁

不知道你有没有想过,为什么会有标准 2 的存在,我们通常是“喜欢” try 住所有内容,生怕发生异常不能捕获的

在 try{} 外获取锁主要考虑两个方面:

  1. 如果没有获取到锁就抛出异常,最终释放锁肯定是有问题的,因为还未曾拥有锁谈何释放锁呢
  2. 如果在获取锁时抛出了异常,也就是当前线程并未获取到锁,但执行到 finally 代码时,如果恰巧别的线程获取到了锁,则会被释放掉(无故释放)

3、线程状态:

 

线程总共有5大状态,通过上面第二个知识点的介绍,理解起来就简单了。

新建状态:新建线程对象,并没有调用start()方法之前

就绪状态:调用start()方法之后线程就进入就绪状态,但是并不是说只要调用start()方法线程就马上变为当前线程,在变为当前线程之前都是为就绪状态。值得一提的是,线程在睡眠和挂起中恢复的时候也会进入就绪状态哦。

运行状态:线程被设置为当前线程,开始执行run()方法。就是线程进入运行状态

阻塞状态:线程被暂停,比如说调用sleep()方法后线程就进入阻塞状态

死亡状态:线程执行结束
 

1、为什么有了synchronized,还要设计新的锁?(背景)

这是由于synchronized的使用场景的局限性导致的。

虽然synchronized能够很好的控制并发访问共享资源,但是其在锁获取与释放的可操作性、可中断的获取锁以及超时获取锁上显得捉襟见肘。同时,其无法处理这样的动态锁获取的场景:如,先获取A锁,再获取B锁,当获取到B锁之后,释放A锁同时获取C锁。

鉴于synchronized以上局限性,设计者考虑Lock接口的实现,以更加灵活的进行锁的获取与释放。

什么是AQS?

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。 AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

AQS定义了两种资源获取方式:独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock) 和共享(多个线程可同时访问执行,如Semaphore/CountDownLatch,Semaphore、CountDownLatCh、 CyclicBarrier )。

AQS 的类名称和修饰上来看,这是一个抽象类,所以从设计模式的角度来看同步器一定是基于【模版模式】来设计的,使用者需要继承同步器,实现自定义同步器,并重写指定方法,随后将同步器组合在自定义的同步组件中,并调用同步器的模版方法,而这些模版方法又回调用使用者重写的方法

我不想将上面的解释说的这么抽象,其实想理解上面这句话,我们只需要知道下面两个问题就好了

  1. 哪些是自定义同步器可重写的方法?
  2. 哪些是抽象同步器提供的模版方法?

同步器可重写的方法

同步器提供的可重写方法只有5个,这大大方便了锁的使用者:

按理说,需要重写的方法也应该有 abstract 来修饰的,为什么这里没有?原因其实很简单,上面的方法我已经用颜色区分成了两类:

  • 独占式
  • 共享式

自定义的同步组件或者锁不可能既是独占式又是共享式,为了避免强制重写不相干方法,所以就没有 abstract 来修饰了,但要抛出异常告知不能直接使用该方法:

protected boolean tryAcquire(int arg) {

 throw new UnsupportedOperationException();

}


表格方法描述中所说的同步状态就是上文提到的有 volatile 修饰的 state,所以我们在重写上面几个方法时,还要通过同步器提供的下面三个方法(AQS 提供的)来获取或修改同步状态:暖暖的很贴心(如果你有类似的需求也可以仿照这样的设计)

表格方法描述中所说的同步状态就是上文提到的有 volatile 修饰的 state,所以我们在重写上面几个方法时,还要通过同步器提供的下面三个方法(AQS 提供的)来获取或修改同步状态:

 

再看看获取锁失败如何加入队列。
如果队列不为空,直接加入队列。
如果队列为空,则生成新的head 和 tail 节点信息。让后在加入队列。

再看看如何阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
    parkAndCheckInterrupt())

final boolean acquireQueued(final Node node, int arg) {

    boolean failed = true;

    try {

        boolean interrupted = false;

        // "死循环",尝试获取锁,或者挂起

        for (;;) {

            // 获取当前节点的前驱节点

            final Node p = node.predecessor();

            // 只有当前节点的前驱节点是头节点,才会尝试获取锁

            // 看到这你应该理解添加哨兵节点的含义了吧

            if (p == head && tryAcquire(arg)) {

                // 获取同步状态成功,将自己设置为头

                setHead(node);

                // 将哨兵节点的后继节点置为空,方便GC

                p.next = null// help GC

                failed = false;

                // 返回中断标识

                return interrupted;

            }

            // 当前节点的前驱节点不是头节点

            //【或者】当前节点的前驱节点是头节点但获取同步状态失败

            if (shouldParkAfterFailedAcquire(p, node) &&

                parkAndCheckInterrupt())

                interrupted = true;

        }

    finally {

        if (failed)

            cancelAcquire(node);

    }

}

获取同步状态成功会返回可以理解了,但是如果失败就会一直陷入到“死循环”中浪费资源吗?很显然不是,shouldParkAfterFailedAcquire(p, node) 和 parkAndCheckInterrupt() 就会将线程获取同步状态失败的线程挂起,我们继续向下看

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

    // 获取前驱节点的状态

    int ws = pred.waitStatus;

    // 如果是 SIGNAL 状态,即等待被占用的资源释放,直接返回 true

    // 准备继续调用 parkAndCheckInterrupt 方法

    if (ws == Node.SIGNAL)

        return true;

    // ws 大于0说明是CANCELLED状态,

    if (ws > 0) {

        // 循环判断前驱节点的前驱节点是否也为CANCELLED状态,忽略该状态的节点,重新连接队列

        do {

            node.prev = pred = pred.prev;

        while (pred.waitStatus > 0);

        pred.next = node;

    else {

        // 将当前节点的前驱节点设置为设置为 SIGNAL 状态,用于后续唤醒操作

        // 程序第一次执行到这返回为false,还会进行外层第二次循环,最终从代码ws == Node.SIGNAL返回

        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

    }

    return false;

}

这个地方设置前驱节点为 SIGNAL 状态到底有什么作用?

 

 如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire 方法会返回 true ,程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起

private final boolean parkAndCheckInterrupt() {

    // 线程挂起,程序不会继续向下执行

    LockSupport.park(this);

    // 根据 park 方法 API描述,程序在下述三种情况会继续向下执行

    //  1. 被 unpark

    //  2. 被中断(interrupt)

    //  3. 其他不合逻辑的返回才会继续向下执行

     

    // 因上述三种情况程序执行至此,返回当前线程的中断状态,并清空中断状态

    // 如果由于被中断,该方法会返回 true

    return Thread.interrupted();

}

被唤醒的程序会继续执行 acquireQueued 方法里的循环,如果获取同步状态成功,则会返回 interrupted = true 的结果

CountDownLanch

前面我们讲到过,等待队列中的线程可能有多个,而调用countDown()方法的线程只唤醒了一个处于等待状态的线程,这里剩下的等待线程是如何被唤醒的呢?其实这些线程是被当前唤醒的线程唤醒的。

具体的我们可以看看await()方法的具体执行过程。离head节点最近的线程阻塞以后, 被unparkSuccessor唤醒. 满足了计数为0, 前置节点为head的条件. 得以继续往下执行-->setHeadAndPropagate

private void setHeadAndPropagate(Node node, int propagate) {undefined

    Node h = head;

    setHead(node);// 我把自己的节点设置为head

   

//然后检查唤醒过程是不是要往下传递

    if (propagate > 0 || h == null || h.waitStatus < 0 ||

        (h = head) == null || h.waitStatus < 0) {undefined

        Node s = node.next;

        if (s == null || s.isShared())

            doReleaseShared();

    }

}

因为我被唤醒了, 我执行完setHeadAndPropagate之后, 就可以跳出await, 进行下面自己的逻辑了. 所以我这个节点也就用没用了, 我把自己的节点设置为head. 然后检查唤醒过程是不是要往下传递. 然后进行唤醒操作. 唤醒下一个线程. 下一个线程被唤醒后, 又可以执行

因为我被唤醒了, 我执行完setHeadAndPropagate之后, 就可以跳出await, 进行下面自己的逻辑了. 所以我这个节点也就用没用了, 我把自己的节点设置为head. 然后检查唤醒过程是不是要往下传递. 然后进行唤醒操作

.....重复下去就完成了所有线程await的等待激活.

5.总过程:

await内部实现流程:

(1)判断state计数是否为0,是0,那么可以执行执行await后面的逻辑

(2)state大于0,则表示需要阻塞等待计数为0

(3)当前线程封装Node对象,进入阻塞队列

(4)不满足激活条件(前置节点是head, 计数为0), 会被操作系统给挂起, 等待激活

(5)被唤醒后, 执行后面的继续唤醒操作,重置头节点状态, 检查唤醒传递等待

(6)跳出await的循环, 开始自己的业务逻辑

countDown内部实现流程:

(1)尝试释放锁tryReleaseShared,实现计数-1

若计数已经小于0,则直接返回false

否则执行计数(AQS的state)减一

(2)若减完之后,state==0,然后就需要唤醒被阻塞的线程了doReleaseShared

如果队列为空,即表示没有线程被阻塞(也就是说没有线程调用了 CountDownLatch#wait()方法),直接退出

(3)头结点如果为SIGNAL, 则依次唤醒头结点下个节点上关联的线程,并出队

(4)唤醒的线程会从await的第5步后开始执行

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值