从 LockSupport 到 AQS 的简单学习

学习 AQS 之前, 需要对以下几点内容都有所了解. 本章内容将先从以下几点开始然后逐步到 AQS.

  • CAS 概念 (在前面几篇)
  • LockSupport 概念
  • CLH 队列锁概念
  • AQS 概念
  • 从 ReentrantLock 重入锁来看 AQS

一. CAS

关于 CAS 在前面文章有写过, 传送门 java 基础回顾 - 基于 CAS 实现原子操作的基本理解


 

二. LockSupport

2.1 传统线程等待/唤醒机制

日常我们经常使用的等待与唤醒机制无非就是下面两种方式

  • Object 类中的 waitnotify 方法实现线程等待和唤醒.
  • Condition 接口中的 awaitsignal 方法实现线程的等待和唤醒.

这两种方式如果大家都使用过的话, 就会知道会有以下两个问题. 也就是传统的 synchronizedLock 实现等待唤醒通知的约束

  • 线程先要获得并持有锁, 必须在锁块(synchronized 或 lock)中.
  • 必须要先等待后唤醒, 线程才能够被唤醒

LockSupport 就解决了传统的等待/唤醒机制的痛点, 下面一起来看下 LockSupport 究竟是什么.

2.2 LockSupport 是什么

LockSupport 是一个线程阻塞工具类, 内部所有方法都是静态的, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法. 可以唤醒任意指定线程.
LockSupport 使用了一种名为 permit(许可)的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个 permit(许可).
permit(许可) 只有两个值: 0 与 1. 默认为 0.
 

2.3 LockSupport 的阻塞方法
 public static void park() {
    UNSAFE.park(false, 0L);
 }

permit(许可) 默认是 0, 所以一开始调用阻塞方法 park(), 当前线程就会阻塞, 直到别的线程将当前线程的 permit(许可)设置为 1 时才会被唤醒, 被唤醒后会将 permit(许可) 再次设置为 0 并返回. 别的线程调用了 interrupt 中断该线程, 也会立即返回.
 

2.4 LockSupport 的唤醒方法
 public static void unpark(Thread thread) {
     if (thread != null)
         UNSAFE.unpark(thread);
 }

在调用 unpark(thread) 方法后, 就会将传入 thread 线程的 permit(许可) 设置为 1, 并自动唤醒传入的 thread 线程, 即之前阻塞中的 LockSupport.park() 方法会立即返回. 多次调用 unpark() 方法并不会累加, 值还会是 1.
 

2.5 LockSupport 简单例子

LockSupport 先阻塞后唤醒

这是一个典型的先阻塞后唤醒的例子: thread1 进入后调用 LockSupport.park() 发现 permit(许可) 值为 0, 直接进入阻塞状态. 3 秒后进入到 thread2 调用 LockSupport.unpark(thread1)thread1 中的 permit(许可) 值改为 1, 并唤醒 thread1. 最后在 thread1LockSupport.park() 就会立刻返回, 结束阻塞状态同时将 permit(许可) 值再改为 0.

如果让 thread2 先给 thread1 发放许可, 然后 thread1 再进行阻塞. 又会输出什么呢.

LockSupport 先唤醒后阻塞

输出结果是没问题的, 不会被阻塞. 这也就解决了传统的等待/唤醒机制的痛点.

 

2.6 LockSupport 总结
  • LockSupport 和每个使用它的线程都有一个 (permit)许可 关联. permit 相当于1,0的开关, 默认是0.
  • 如果将 (permit)许可 看成凭证可能会更容易理解一点. 线程阻塞需要消耗凭证 (permit), 这个凭证最多只有1个. 默认是没有凭证的.也就是 0.
  • 调用 park 方法时候, 会先判断有没有凭证, 也就是 permit 值是否为 1.
    • 如果有凭证, 则会直接消耗掉这个凭证然后正常退出. 也就是将 permit的值改为 0, 然后直接返回.不会阻塞. 这就是为什么 LockSupport 可以先唤醒后阻塞的原理.
    • 如果没有凭证, 就必须阻塞等待有凭证可用. 也就是等待 permit 的值由 0 变为 1. 阻塞后, 一旦在外部为此线程发放了凭证(外部调用了 unpark 方法, 将 permit 改为 1), 那么将结束阻塞状态, 并消费掉这个凭证.(将 permit 改为 0).
  • 调用 unpark 方法, 会为指定线程发放一个凭证, 将指定线程的 permit 值改 为 1. 但凭证最多只能有1个, 累加无效.

问题: 如果先唤醒两次再阻塞两次, 最终结果是什么呢? 答案是最终还是会阻塞线程, 因为凭证的数量最多就是 1, 唤醒再多次也没用, 但是只要阻塞一次, 就会消费掉这个凭证, 再进行阻塞, 就没有凭证了, 所以还会阻塞.

如果有兴趣研究 LockSupport 源码的同学可以看下这篇文章, 【细谈Java并发】谈谈LockSupport.


 

三. CLH 队列锁的概念

CLH 名字的由来即 Craig, Landin, Hagersten 国外三个大牛名字的缩写.
CLH 队列是单向队列, 其主要特点是自旋检查前驱节点的 locked 状态
CLH 队列锁一个自旋锁. 能确保无饥饿性. 提供先来先服务的公平性.
CLH 队列锁也是一种基于链表的可扩展, 高性能, 公平的自旋锁. 申请锁线程仅在本地变量上自旋, 它不断轮询前面一个节点的状态, 假设发现上一个节点释放了锁就结束自旋.

3.1 CLH 队列锁的获取

CLH 队列中当一个线程需要获取锁的时候, 会将其封装成为一个 QNode 节点, 将其中的 locked 设置为 true, 表示需要获取锁, myPred 则表示对其前驱节点的引用.

QNode

 
线程 A 要获取锁的时候, 会先使自己成为队列的尾部, 同时获取一个指向其前驱节点的引用 myPred
线程 B 想要获取锁, 同样需要放到队列的尾部, 并将 myPred 指向线程 A 的节点.
线程 A 拿锁

线程 B 拿锁

 
每个线程就在前驱节点的 locked 字段上自旋, 直到前驱节点释放锁. 也就是前驱节点的 locked 字段变为 false
 
当一个线程需要释放锁的时候, 将当前节点的 locked 设置为 false, 同时回收前驱节点. 如下图所示. 前驱节点释放锁, 线程 A 的 myPred 所指向前驱节点的 locked 字段变为 false, 线程 A 就可以获取到锁.
image.png


 

四. AQS 概念

4.1 AQS 简介

AQS 即 AbstractQueuedSynchronizer 抽象的队列式同步器. 是用来构建锁或者其他同步组件的基础框架. 它不能被实例化, 设计之初就是为了让子类通过继承 AQS 并实现它的抽象方法来管理同步状态.

简单理解就是: 如果被请求的共享资源空闲, 则将当前请求资源的线程设置为有效的工作线程, 并将共享资源设置为锁定状态, 如果被请求的共享资源被占用, 那么就需要一套线程阻塞等待以及唤醒时锁分配的机制, 这个机制就是 AQS, 是基于 CLH 队列的变体实现的. 它将每一条请求共享资源的线程封装成队列的一个节点 Node, 将暂时获取不到锁的节点Node加入到队列中. 通过 CAS, 自旋, 以及 LockSupport.park() 的方式维护内部的一个使用 volatile 修饰的 int 类型共享变量 state 的状态. 使并发达到同步的效果.

state 变量可以理解为共享资源, state 的访问方式有以下三种, 根据修饰符protected final, 说明它们是不可以被子类重写的, 但是可以在子类中进行调用, 这也就意味这之类可以根据自己的逻辑来决定如何使用 state 的值.

  • getState() : 获取当前同步状态
  • setState(int newState) : 设置当前同步状态
  • compareAndSetState(int expect, int update) : 使用 CAS 设置当前状态, 该方法可以保证状态设置的原子性.

ReentrantLock | CountDownLatch | ReentrantReadWriteLock | Semaphore 这些都是基于 AQS 实现的. AQS 的子类应被定义为内部类, 作为内部的 helper 对象. 如 ReentrantLock, 它便是通过内部的 Sync 对象来继承 AQS 的.

CLH 是单向队列, 这里说的基于 CLH 的变体是基于链表实现的双向同步队列, 在 CLH 的基础上进行了变种. CLH 的特点是自旋检查前驱节点的 locked 状态. 而 AQS 同步队列是双向队列, 每个节点也有状态 waitStatus, 而其也不是一直对前驱节点的状态自旋, 而是自旋一段时间后阻塞让出 CPU 时间片, 等待前驱节点主动唤醒后继节点.

state = 0,代表没有线程持有锁,state > 0 有线程持有锁,并且 state 的大小是重入的次数

AQS
4.2 AQS 同步器与锁之间的关系

AQS 同步器是实现锁或者任意同步组件的关键, 在锁的实现中聚合同步器. 可以这样理解二者之间的关系.

  • 锁是面向使用者的, 它定义了使用者与锁交互的接口, 隐藏了实现的细节.
  • 同步器是面向锁的实现者, 它简化了锁的实现方式, 平布了同步状态的管理, 线程的排队, 等待与唤醒等底层操作.
     
4.3 资源的获取与释放

在 AQS 中定义了两种资源的共享方式.

  • 独占式 Exclusive : 只有一个线程能执行, 例如 ReentrantLock
  • 共享式 Share : 多个线程可以同时执行, 如 Semaphore, CountDownLatch, ReadWriteLock.

不同的自定义同步器争用共享资源的方式也不同, 自定义同步器在实现时只需要实现共享资源 state 的获取与释放即可, 至于具体等待队列的维护, AQS 已经在顶层实现好了. 自定义同步器时主要实现以下几个方法.

  • isHeldExclusively() : 当前线程是否正在独占资源.
  • tryAcquire(int arg) : 独占式. 尝试获取资源. 成功返回 true, 失败返回 false.
  • tryRelease(int) : 独占式. 尝试释放资源. 成功返回 true, 失败返回 false.
  • tryAcquireShared(int) : 共享式. 尝试获取资源. 返回大于等于 0 的值表示成功, 反之获取失败.
  • tryReleaseShared(int) : 共享式. 尝试释放资源. 如果释放后允许唤醒后续等待节点返回 true, 否则返回 false.

ReentrantLock 为例. state 的初始值为 0, 表示未锁定状态. 当 A 线程 Lock() 时, 会调用 tryAcquire(int arg) 方法独占该锁并将 state + 1. 后面其他线程再调用 tryAcquire(int arg) 时就会失败, 直到 A 线程调用了 unLock() 后将 state = 0 释放锁为止, 其他线程才会有机会获取到锁. A 线程在释放之前, 是可以自己重复获取次锁的, 即将 state 累加. 最后释放的时候, 重入几次锁就需要释放几次锁, 这样才能保证 state 的值变为 0. 这便是锁的可重入概念.

一般来说, 我们自定义同步器时, 要么是独占的方式, 要么是共享的方式. 但是 AQS 也支持同时实现独占和共享两种方式. 例如 ReentrantReadWriteLock

 

4.4 AQS 中的 Node 节点

上面说过了, 在 AQS 中将请求共享资源的线程都封装成为了一个 Node 节点. 现在我们来看一下这个 Node 里面都包含了什么.
AbstractQueuedSynchronizer.java 380 行

   static final class Node {  
           //表示线程以共享的模式等待锁
           static final Node SHARED = new Node();
           //表示线程以独占的模式等待锁
           static final Node EXCLUSIVE = null;

           //节点状态值----表示线程获取锁的请求已取消. 当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的节点将不会再变化
           static final int CANCELLED =  1;
           //节点状态值----表示后继节点在等待当前节点唤醒. 后继节点入队时, 会将前继节点的状态更新为此状态.
           static final int SIGNAL    = -1;
           //节点状态值----表示线程正在等待状态  这个状态只在condition await时设置
           static final int CONDITION = -2;
           //节点状态值----表示共享模式下, 前继节点不仅会唤醒其后继节点, 同事也可能唤醒后继节点的后继节点.
           static final int PROPAGATE = -3;
 
           //节点状态, 节点在获取锁和释放锁的状态. 上面4 个节点状态对应此变量.
           volatile int waitStatus;
           //前驱指针
           volatile Node prev;      
           //后继指针
           volatile Node next;
           //记录阻塞的线程
           volatile Thread thread;
           //condition 中是记录下一个节点, 
           //Lock 中是记录当前的 node 是独占 node 还是共享 node
           Node nextWaiter;
           //如果当前节点是以共享模式等待,则返回 true.
           final boolean isShared() {
               return nextWaiter == SHARED;
           }
           //返回前驱节点
           final Node predecessor() throws NullPointerException {
               Node p = prev;
               if (p == null)
                   throw new NullPointerException();
               else
                   return p;
           }
           //构造函数1 不存放任何线程, 用于生成哨兵节点
           Node() {    // Used to establish initial head or SHARED marker
           }
           //构造函数 2 用于锁
           Node(Thread thread, Node mode) {     // Used by addWaiter
               this.nextWaiter = mode;
               this.thread = thread;
           }
           //构造函数 3 用于 Condition
           Node(Thread thread, int waitStatus) { // Used by Condition
               this.waitStatus = waitStatus;
               this.thread = thread;
           }
       }

这里需要说明一下, AQS 的同步队列是有些特别的, 其 head 节点是一个空节点也可称为哨兵节点, 没有记录线程 node.thread = null, 其后继节点才是实质性的有线程的节点, 这样做的好处是. 当最后一个有线程的节点出队后, 不需要想着清空队列, 同时下次有新节点入队也不需要重新实例化队列. 所以队列为空时, head = tail = null, 当第一个线程节点入队时, 会先初始化, head, tail 先指向一个空节点. 再将新节点作为当前 tail 的下一个节点.通过 CAS 设置成功后, 将新节点设置为新的 tail 节点即可. 入队,出队操作后面分析 ReentrantLock 的时候会分析到. 这里不再进行说明.

弄明白了原理及节点这些后, 下面我们从 ReentrantLock 来解读 AQS.


 

五. 从 ReentrantLock 来看 AQS

5.1 ReentrantLock 结构

ReentrantLock 是可重入锁. 重入锁的概念在上面 4.3 资源的获取与释放中已经解释过. 忘记的可以翻看一下.
上面说过 AQS 是为了让子类通过继承 AQS 并实现它的抽象方法来管理同步状态的. AQS 的子类应当被定义为内部类, 作为内部的 helper 对象. 那我们先看 ReentrantLock 的结构是否是这样的.

ReentrantLock

   public class ReentrantLock implements Lock, java.io.Serializable {
        ...
       abstract static class Sync extends AbstractQueuedSynchronizer {
         abstract void lock();
         ...
       }
       static final class FairSync extends Sync {
         ...
       }

       static final class NonfairSync extends Sync {
          ...
       }
       ...
   }

可以看到 ReentrantLock 内部的 Sync 对象继承了 AQS, 但是 Sync 又是一个静态抽象类, FairSyncNonfairSync 又都继承了 Sync. 这两个方法类分别是 ReentrantLock 内部实现的 公平锁与非公平锁. 他们分别都实现了 Sync 内部的 lock 方法. 那么这两个对象又是什么时候创建的呢. 现在跳转到 ReentrantLock 的构造函数.

ReentrantLock 的有两个构造函数, 分别如下.

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

我们平时使用的基本都是非公平锁. 也就是无参的构造函数. 通过调用构造函数及传入参数的不同, 创建的锁也不相同. 我们来分析非公平锁也就是 NonfairSync 的获取锁与释放锁的流程. 其实他们两个差不多, 无非就是多了一个判断条件, 这点在最后会单独来分析一下.

非公平锁: 不管是否有等待队列, 如果可以获取锁, 则立刻占有锁.

公平锁: 讲究先来先到. 线程在获取锁时, 如果这个锁的等待队列中已经有线程在等待, 那么当前线程就会进入等待队列中.

 

5.2 从 ReentrantLock.lock() 开始

假如现在有 A, B 两个线程来获取锁. 并且我们创建的是非公平锁, 也就是通过无参构造创建的 ReentrantLock, 那么调用的 lock 方法, 其实是调用了 NonfairSynclock. 方法.

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            //通过 CAS 改变 AQS 中 state 的值. 期望是 0, 改为 1, 成功返回 true.
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        ...
    }

线程A 进来执行 if 内语句, 线程 B 进来执行 else 语句.

  • A 线程执行到 lock() 内部的 compareAndSetState(0,1) 的时候, AQS 中 state 值默认是 0, 所以这里成立, 返回了 true, 然后调用 setExclusiveOwnerThread 方法记录下独占模式下锁持有者的线程.
  • 当 B 线程执行 if 内的语句的时候, 返回的就是 false 了, 因为期望值不对. 就调用了 AQS 的 acquire(1).
     
5.3 AbstractQueuedSynchronizer.acquire
      public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  implements java.io.Serializable {
        ...
        public final void acquire(int arg) {
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }
        protected boolean tryAcquire(int arg) {
            throw new UnsupportedOperationException();
        }
        ...
       }

这里又调用了 tryAcquire, 在学习 AQS 中就说过, 自定义同步器时需要实现的几个方法.

  • tryAcquire(int arg) : 独占式. 尝试获取资源. 成功返回 true, 失败返回 false.
  • tryRelease(int) : 独占式. 尝试释放资源. 成功返回 true, 失败返回 false.

所以这里调用的是 ReentrantLock.NonfairSync 中重写的 tryAcquire 方法. 假如返回了 false, 那么取反得 true, 就又会调用 addWaiter(Node.EXCLUSIVE)acquireQueued(), 那么还是先来看 tryAcquire()

 

5.4 ReentrantLock.NonfairSync.tryAcquire()
    static final class NonfairSync extends Sync {
        ...
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

nonfairTryAcquire 方法在 Sync 中. 直接进入.
 

5.5 ReentrantLock.Sync.nonfairTryAcquire()
       //参数值为 1.
       final boolean nonfairTryAcquire(int acquires) {
           //获得当前线程对象
           final Thread current = Thread.currentThread();
           //获得共享资源状态, 
           int c = getState();
           //是 0 表示未被占用. 这里判断的目的是有可能 B 线程刚进入, A 线程就执行完了. 那么 B 就可以直接占用资源
           if (c == 0) {
               //占用资源, 改变状态为 1.
               if (compareAndSetState(0, acquires)) {
                   //记录 B 线程.
                   setExclusiveOwnerThread(current);
                   return true;
               }
           }
           //判断当前要获取锁的线程是不是之前记录的线程. 也就是判断是不是重入.
           else if (current == getExclusiveOwnerThread()) {
               //如果是重入, 那么资源的状态值就 +1.
               int nextc = c + acquires;
               if (nextc < 0) // overflow
                   throw new Error("Maximum lock count exceeded");
               //改变状态值.
               setState(nextc);
               return true;
           }
           return false;
       }

B 线程尝试拿锁失败, 那么要怎么办呢, 下一步应该就是要加入到等待队列中了.
B 线程执行到这里的时候, 假设 A 线程还在执行, 那么 ifelse if 都不成立, 则直接返回 false. 在 5.3 中这个返回值取反为 true 后就会调用 acquireQueued, 参数是 addWaiter 方法的返回值和 1. 先进去看一下 addWaiter

      if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
           selfInterrupt();

 

5.6 AbstractQueuedSynchronizer.addWaiter()
    //调用此方法的时候, 传入的 Node 对象为 Node.EXCLUSIVE, 独占模式.
    //还记得在 AQS 中的内部的 Node 中, 独占模式的声明吗? static final Node EXCLUSIVE = null;
    //所以这里传入的是一个 null.
    private Node addWaiter(Node mode) {
        //将 B 线程封装成一个 Node 节点. 模式为独占模式.
        Node node = new Node(Thread.currentThread(), mode);
        //将尾指针赋值给 pred.
        Node pred = tail;
        //判断 pred 是否为 null
        if (pred != null) {
           //不为 null, 就将新节点的前驱节点指向队列的尾部 tail.
            node.prev = pred;
            //通过 CAS 将新节点变为队列尾部
            if (compareAndSetTail(pred, node)) {
                //将之前尾部节点的后置节点指向新节点
                pred.next = node;
                //返回新节点
                return node;
            }
        }
       // pred = null 说明还没有初始化头尾, 
        enq(node);
        //初始化好头尾并入队成功后,返回 B 线程封装的节点.
        return node;
    }

    //cas 自旋入队到尾部
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //队列为空, 就创建一个空节点(哨兵节点),并将 head 与 tail 指向它
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else { //第二次自旋就进入到 else 中
                //新节点的前驱节点指向第一次自旋时创建的尾部空节点.因为目前队列中除了哨兵节点外没有节点存在
                node.prev = t;
                //将尾指针指向新节点
                if (compareAndSetTail(t, node)) {
                    //将哨兵节点的后置节点指向新节点.
                    t.next = node;
                    return t;
                }
            }
        }
    }

通过前面的 尝试拿锁 tryAcquireaddWaiter 表示 B线程拿锁失败, 已经被加入到等待队列的队尾了, 那么下一步要干什么呢? 那就是再尝试拿一次锁, 因为有可能这个时间,A 线程执行完了. 如果再一次拿锁失败, 那么就需要进入到阻塞状态了, 直到 A 线程释放锁然后唤醒 B 线程. acquireQueued 方法就是完成了这几步的操作, 那么现在接着返回到5.3, 看 acquireQueued 方法

     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
          selfInterrupt();

 

5.7 AbstractQueuedSynchronizer.acquireQueued
      final boolean acquireQueued(final Node node, int arg) {
          //表示是否成功获取资源
          boolean failed = true;
          try {
              //是否中断
              boolean interrupted = false;
              //自旋
              for (;;) {
                  //获取到 B 线程节点的前驱节点. 也就是哨兵节点. 
                  //为啥不是 A 线程节点? 因为 A 线程最先抢占到资源, 都没有入队, 直接就执行了. 所以队列中没有 A 线程节点.
                  final Node p = node.predecessor();
                  //如果前驱节点是哨兵节点, 即 B 线程节点是第二位. 那么就有资格去再次尝试获取锁.
                  // 这时候 A 线程如果还没执行完, 那么执行 tryAcquire 也就是第5步, 返回的肯定也是 false. 不成立, 所以不进入.
                  // 如果 A 线程在这个时刻执行完了, 那么执行第5步的时候就会返回 true. 进入 if 内
                  if (p == head && tryAcquire(arg)) {
                      //拿到锁后, 将 head 指向 B 线程节点, 
                      // 并将 B 线程节点中的 node.thread = null, 
                      //同时断开B 线程节点的前驱节点也就是哨兵节点的引用 node.prev = null.
                      //所以 head 所指的标杆结点,就是当前获取到资源的那个结点或null。
                      setHead(node);
                      //setHead 中 已经将线程 B的节点 node.prev 设置为 null, 这里再将 head.next 也设置为 null
                      // 就是为了方便 GC 回收以前的 head 节点
                      p.next = null; // help GC
                      //成功获取资源
                      failed = false;
                      //返回等待过程中是否被中断过.
                      return interrupted;
                  }
                  //如果B 线程执行到这里的时候, A 线程未释放资源, 那么就通过 lockSupport.park 进入阻塞状态. 直到被 unpark 唤醒
                  // 同时如果不可中断的情况下被中断了, 那么会从 park 中醒来, 发现拿不到锁, 从而继续进入到 park阻塞状态.
                  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                      //如果等待中被中断, 改变 interrupted 标志位.
                      interrupted = true;
              }
          } finally {
              //等待过程中没有成功获取资源, 那么取消节点在队列中的等待.
              if (failed)
                  cancelAcquire(node);
          }
      }

期间调用了 shouldParkAfterFailedAcquire(哨兵节点, B 线程节点) 通过前驱节点判断当前节点是否需要进入阻塞 与 parkAndCheckInterrupt() 阻塞线程方法, 先来看着两个方法具体实现过程.
 

5.8 AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
      //当 B 线程执行到这里时, 传入参数为 pred = 哨兵节点, node = B 线程节点
      //如果还有 C 线程, 那么 pred = B 线程节点, node = C  线程节点.
      private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          //获得前驱节点的状态. 默认为 0.
          int ws = pred.waitStatus;
          // 如果前驱节点的状态是 Node.SIGNAL, 说明已经通知前驱节点释放锁时通知当前节点, 那么当前节点就可以进入阻塞状态了. 返回 true.
          // 在 4.4 Node 节点说明中, 已经写明 
          //  Node.SIGNAL 表示后继节点在等待当前节点唤醒. 后继节点入队时, 会将前继节点的状态更新为此状态.
          if (ws == Node.SIGNAL)
              return true;
          // 前驱节点状态 > 0  说明前驱节点已经放弃获取锁了, 
          // 那么就一直往前找, 直到找到一个正常等待的状态的节点, 并排在它后面, 
          // 由于那些已经放弃获取锁的节点, 由于被加塞到它们前面, 那些节点相当于形成了一个无引用的链,  稍后会被GC 回收.
          if (ws > 0) {
              do {
                  node.prev = pred = pred.prev;
              } while (pred.waitStatus > 0);
              pred.next = node;
          } else {
              //因为当前 B 线程节点的前驱节点是哨兵节点, 状态默认是 0 ,所以会执行这句代码.
              //把前驱节点的状态,也就是哨兵节点的状态设置为 -1, 并且返回了 false. 然后在第7步内,再次进行自旋.
              compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
          }
          return false;
      }

由于在锁的使用场景内, NodewaitStatus 初始值必定是 0 , 因此在这方法首次进入的时候, 前驱节点的状态必定是 0, 所以会先修改哨兵节点的状态值为 -1. 然后会返回 false. 那么接着会回到 5.7 if 条件内的第一个不成立, 再次进行自旋, 如果期间 A 线程也未执行完成, 那么会再次调用 shouldParkAfterFailedAcquire 方法. 当第二次进入到这个方法的时候, if (ws == Node.SIGNAL) 这个判断就会成立, 直接返回 true. (第一次将前驱节点的状态设置为 -1, 第二次进入就返回 true ). 那么按照之前说的, 当前节点已经可以进入阻塞状态了, 接着执行 5.7 中阻塞线程方法 parkAndCheckInterrupt ().

     if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                //如果等待中被中断, 改变 interrupted 标志位.
                interrupted = true;

 

5.9 AbstractQueuedSynchronizer.parkAndCheckInterrupt()
    private final boolean parkAndCheckInterrupt() {
        //走到这里, 线程 B 才算是被阻塞, 然后等待被唤醒.
        LockSupport.park(this);
        //被唤醒后, 才会执行这行代码. 
       // 被唤醒后, 查看自己是不是被中断的.
        return Thread.interrupted();
    }

 

5.10 lock 小结

至此, B 线程成功入队阻塞. 在 5.7 中代码执行到 parkAndCheckInterrupt() 阻塞后, 就暂停了向下执行. 等待被唤醒了.
那么现在简单的总结一下 acquireQueued() 方法内部的流程

  • 调用 lock() 方法, 直接通过 CAS 修改共享资源 state 状态,
    • 修改成功表示获取到锁, 并记录当前线程.
    • 修改失败调用 acquire () 获取锁.
  • acquire () 获取锁方法内会先调用 tryAcquire() --> nonfairTryAcquire(), 在 tryAcquire() 方法返回 false 的情况下才会调用 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    • nonfairTryAcquire() 内先判断共享资源state是否为 0 . 为 0 则通过 CAS 修改共享资源为 1, 并记录当前线程. 返回 true.
    • state 不为 0, 接着判断记录的线程是否是当前线程, 这个判断表示是否重入, 是重入则共享资源 state 值累加 1. 并返回 true.
    • 在共享资源不为 0, 并且不是重入的情况下, 直接返回 false.
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 方法内, 会先调用 addWaiter() 入队
    • addWaiter() --> enq(), 在 addWaiter() 方法内先将当前线程封装为一个节点.模式为独占模式.
    • 接着判断队尾 tail 是否为 null, 也就是判断是否初始化过队列.
      • 如果 tail 不为 null, 通过 CAS 自旋将新节点加入到队尾. 并返回新节点.
      • tailnull, 调用 enq() 方法自旋初始化head,tail后将新节点加入到队尾,
  • 入队成功后执行执行 acquireQueued(新节点, 1) 阻塞线程. acquireQueued() 方法也是自旋方法.
    • 先获取新节点的前驱节点,
    • 如果前驱节点是 head 并且再次尝试拿锁成功, 执行出队和换head操作.
    • 前驱节点不是 head 或者尝试拿锁失败, 进入阻塞判断方法 shouldParkAfterFailedAcquire()
  • shouldParkAfterFailedAcquire() 只会执行 2 次, 最后会返回 true, 表示可以进入阻塞状态.
  • acquireQueued() 方法内又会调用 parkAndCheckInterrupt() 方法使用 LockSupport.park() 阻塞当前线程. 等待被唤醒.

到这里, 获取锁的流程已经分析完了. 接下来就是释放锁与唤醒了.
 

5.11 从 ReentrantLock.unLock() 开始

假如这时候, A 线程已经执行完成, 并且调用了 unLock 那么代码如下.

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

平时我们调用 unLock 后, 调用了 Sync.release(1) 方法, 但是 AQS 中 release 方法与 acquire 方法一样, 都是 final 类型的, 所以执行的还是 AbstractQueuedSynchronizer.release(1)
 

5.12 AbstractQueuedSynchronizer.release()
      public final boolean release(int arg) {
          if (tryRelease(arg)) {
              Node h = head;
              if (h != null && h.waitStatus != 0)
                  unparkSuccessor(h);
              return true;
          }
          return false;
      }

这个方法内部和上面分析的 tryAcquire () 一样, 都是需要自定义的同步器去实现的. 我们进ReentrantLock.Sync.tryRelease(1)去看一下.
值得注意的是, 在成功释放锁之后( tryRelease 返回 true之后), 唤醒后继节点只是一个 "附加操作", 无论该操作结果怎样, 最后 release 操作都会返回 true.
 

5.13 ReentrantLock.Sync.tryRelease()
        //传入的值为 1.
        protected final boolean tryRelease(int releases) {
            //如果这里没有重入, 那么 getState() 值就为 1, 1-1 =0 , 表示释放共享资源
            int c = getState() - releases;
            //释放锁的线程当前必须是持有锁的线程
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                //清除记录的线程
                setExclusiveOwnerThread(null);
            }
            //设置锁的状态为未占用.
            setState(c);
            return free;
        }

正常来说 tryRelease () 都会成功的. 因为这是独占模式. 它来释放锁, 那么肯定是已经拿到锁了. 直接减掉相应量的值即可(state -= arg). 不需要考虑线程安全问题. 那么接着回到 5.12.

      public final boolean release(int arg) {
          //这里返回了 true.
          if (tryRelease(arg)) {
              //找到 head
              Node h = head;
              // h 如果为 null, 说明没有下一个结点了, 因为如果有下一个实际的节点的话, 在入队的时候就会初始化了 `head, tail`. 虽然是哨兵节点.
              //在这里 h != null 成立, 并且, head 的状态为-1. 在第8步 shouldParkAfterFailedAcquire 方法设置的.
              if (h != null && h.waitStatus != 0)
                  //唤醒后置线程节点
                  unparkSuccessor(h);
              return true;
          }
          return false;
      }

唤醒后继的条件是 h != null && h.waitStatus != 0, head 不为 nullhead 的状态不是初始状态, 则唤醒后置. 在独占模式下h.waitStatus可能等于0,-1.

 

5.14 AbstractQueuedSynchronizer.unparkSuccessor()
      //参数值为 head
      private void unparkSuccessor(Node node) {
          // head 状态为 -1.
          int ws = node.waitStatus;
          if (ws < 0)
              //通过 CAS 修改 head 状态为 0.
              compareAndSetWaitStatus(node, ws, 0);
         // 获取到 B 线程节点.
          Node s = node.next;
         //唤醒后继节点的线程, 若为空, 从 tail 往后遍历找一个距离`head`最近的正常的节点
         //通常情况下, 要唤醒的节点就是自己的后置节点. 如果后置节点在等待锁就直接唤醒.
         //但是 上面 5.8 也说过, 也有可能存在后继节点放弃等待锁的情况.
          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;
          }
         //唤醒 B 线程.
          if (s != null)
              LockSupport.unpark(s.thread);
      }

这里有个疑问, 为什么要从队尾逆向向前查找? 而不是直接从 head 开始向后查找呢? 这样只要正向找到第一个, 是不是就可以停止了. 这里这样设计的原因是为了照顾新入队的节点, 这里又会回到上面的 5.6 addWaiter

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        //将尾指针赋值给 pred.
        Node pred = tail;
        //判断 pred 是否为 null
        if (pred != null) {
           //将新节点的前驱节点指向队列的尾部 tail. 
            node.prev = pred;   //-----------------  step1
            //通过 CAS 将新节点变为队列尾部
            if (compareAndSetTail(pred, node)) {  //-----------------  step2
                //将之前尾部节点的后置节点指向新节点
                pred.next = node; //  -----------------  step3
                //返回新节点
                return node;
            }
        }
        enq(node);
        return node;
    }

仔细观察可以发现, 节点入队并不是一个原子操作, 虽然用了 compareAndSetTail 操作保证了将新节点变为尾节点, 但是只能保证step1step2 是执行完成的. 有可能在执行 step3 的时候, 就有别的线程释放了锁调用了 unparkSuccessor() 方法.那么此时 step3 还没执行, 还未将之前尾节点的后置节点指向新节点. 所以如果从前向后遍历的话, 是遍历不到我们新加入的节点的. 还未形成引用关系.
但是因为在 step2 中已经将尾节点设置成功, 同时在 step1 中也将新节点的前驱节点指向了前尾节点. 所以如果从后往前遍历的话, 新的尾结点是可以遍历到, 而且前驱的前尾结点也建立了关系, 可以一直向前查找.

现在 B 线程被唤醒了, 那么接下来的流程是怎么样的呢. 还记得 B 线程是在哪里被阻塞的吗? 是在 5.9, 下面是代码.

   private final boolean parkAndCheckInterrupt() {
       //走到这里, 线程 B 才算是被阻塞, 然后等待被唤醒.
       LockSupport.park(this);
       //被唤醒后, 才会执行这行代码. 
      // 被唤醒后, 查看自己是不是被中断的.
       return Thread.interrupted();
   }

被唤醒后, 执行了 Thread.interrupted(). 我们知道这个方法函数返回的是当前正在执行线程的中断状态,并清除它. 因为 B 线程没有被中断, 所以这里返回的是 false. 接下来又回到了 5.7 acquireQueued 方法中调用了 parkAndCheckInterrupt()if 判断

      final boolean acquireQueued(final Node node, int arg) {
          boolean failed = true;
          try {
              boolean interrupted = false;
              for (;;) {
                  //B 线程被唤醒后, 再次自旋一次, p 还是为 head 节点.
                  final Node p = node.predecessor();
                  //第一个条件成立, 再次调用 tryAcquire, 尝试拿锁. 这时候已经可以拿到锁了.state = 0.在上面 5.5 直接就返回了 true.
                  if (p == head && tryAcquire(arg)) {
                      //拿到锁后, 将 head 指向 B 线程节点, 并将 B 线程节点中的 node.thread = null, 
                      //同时断开B 线程节点的前驱节点也就是哨兵节点的引用 node.prev = null.
                      //所以 head 所指的标杆结点,就是当前获取到资源的那个结点或null。
                      setHead(node);
                      //setHead 中 已经将线程 B的节点 node.prev 设置为 null, 这里再将 head.next 也设置为 null
                      // 就是为了方便 GC 回收以前的 head 节点
                      p.next = null; // help GC
                      //成功获取资源
                      failed = false;
                      //返回等待过程中是否被中断过. 没有被打断过, 所以返回 false.
                      return interrupted;
                  }/
                  // ------------------------------------直接看这里 -------------------------------------------
                  //B 线程被唤醒后, parkAndCheckInterrupt() 返回的是 false.那么 if 条件不成立
                  //方法将再次自旋.
                  if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                      //如果等待中被中断, 改变 interrupted 标志位.
                      interrupted = true;
              }
          } finally {
              //failed= false, 所以不会取消排队, 整个方法结束.
              if (failed)
                  cancelAcquire(node);
          }
      }

 

5.15 unlock 小结

release 方法是独占模式下线程释放锁的顶层入口, 如果彻底释放了, 也就是 state = 0, 它会唤醒等待队列里的其他线程来获取资源.

  • 调用unlock 方法后内部调用了 AbstractQueuedSynchronizer.release() 方法. 在其方法内部又调用 tryRelease 尝试释放锁.一般都会释放成功.
  • 释放成功后准备唤醒后继节点, 但是有一个唤醒条件, 就是 if (h != null && h.waitStatus != 0) , h 如果为 null, 说明没有下一个节点了, 因为如果有下一个实际的节点的话, 在入队的时候就会初始化了 head, tail. 虽然是哨兵节点. 所以在这里 h != null 成立, 并且, head 的状态为 -1. 在 5.8 shouldParkAfterFailedAcquire 方法设置的. 所以条件成立, 调用 AbstractQueuedSynchronizer.unparkSuccessor() 方法唤醒
  • 在唤醒的后置节点的时候, 先将 head的状态设置为 0, 接着从 head.next 获取到要唤醒的线程节点. 调用 LockSupport.unpark(s.thread) 将其唤醒.
  • 唤醒后代码又回到了第7步中内之前阻塞的地方, 条件不成立, 再次自旋. 尝试拿锁成功, 设置 head为要唤醒的节点, 也就是换头出队. 再将原 head 节点的 next 置为 null. 方便 GC 原 head 节点. 最后整个方法结束.

至此, 从 ReentrantLock 来看 AQS 关于独占锁部分已经分析完了, 有兴趣的朋友可以按照这个思路, 执行分析一下 AQS 的共享锁. 在最后补上之前说的 ReentrantLock 公平锁与非公锁的区别.


 

六. 公平锁与非公平锁的区别

上面说过, 无论有参还是无参的 ReentrantLock 构造函数, 构建出来的对象, 都会调用 lock 方法, 只是根据创建 ReentrantLock 对象时传入参数来决定调用的是公平锁的 lock 还是非公平锁的 lock.
那么就先看两者之间 lock 的区别.

 //非公平锁
 static final class NonfairSync extends Sync {
     ...
     final void lock() {
         if (compareAndSetState(0, 1))
             setExclusiveOwnerThread(Thread.currentThread());
         else
             acquire(1);
     }
    ...
 }
 //公平锁
 static final class FairSync extends Sync {
   ...
   final void lock() {
       acquire(1);
   }

这里就能看出一点区别, 非公平锁在 lock 的时候, 都会先去通过 CAS 改变 state 的值, 看是否能够成功, 也就是说, 非公平锁每次都先会尝试插队. 不管能不能插队成功, 先插了再说. 如果插队失败, 那就是和公平锁一样, 都调用了 AbstractQueuedSynchronizer. acquire() 方法. 在 AbstractQueuedSynchronizer. acquire() 方法内, 又都调用了各自实现的 tryAcquire() 方法. 那么接着看着两种锁各自实现的 tryAcquire().

public class ReentrantLock implements Lock, java.io.Serializable {
    //非公平锁
    static final class NonfairSync extends Sync {
        ..
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    ...
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }           
    ...
    //公平锁
    static final class FairSync extends Sync {
       ...
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //区别在这里
                if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
}

非公平锁与公平锁的 tryAcqure() 方法实现, 区别就在 公平锁的 if 判断内多了一个条件. !hasQueuedPredecessors()

!hasQueuedPredecessors() 是什么呢

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

简单就是说判断等待队列中是否存在有效的节点, 通过这个判断得知公平锁在lock 的时候会先判断等待队列是否有有效的节点存在, 要拿锁的线程是否需要排队.

公平锁与非公平锁的区别

  • 在调用 lock 方法的时候, 非公平锁会先拿一次锁, 拿不到才去执行自己实现的 tryAcquire() 方法. 而公平锁则会直接调用自己实现的 tryAcquire().
  • tryAcquire() 方法内, 公平锁会多一个判断, 判断当前等待队列中是否存在有效的节点, 看是否需要排队.

这就是他们的区别. 后续流程都基本一致. 本章内容到此就结束了, 如果能看到这里, 说明你的毅力还真是强大. 若对你有所帮助, 请点赞关注走一波.

以上内容如有分析错误, 还请留言, 大家一起探讨.

你可能感兴趣的:(从 LockSupport 到 AQS 的简单学习)