AQS源码【01】

222 阅读5分钟

本文正在参加「Java主题月 - Java开发实战」,详情查看:juejin.cn/post/696719…

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

AbstractQueuedSynchronizer

cookBook - ifeve.com/doug-lea/

@todo 564

子类

  • ReentrantLock 中的 async
  • CountdownLatch 中的 async
  • LimitLatch 中的 async
  • Semaphore 中的 async
  • ReentrantReadWriteLock 中的 async
  • ThreadPoolExecutor 中的 worker

实现原理

先回顾一下,在不显式依赖任何Lock包相关实现的情况下,我们如何做到线程安全?

  1. Synchronized

    • 用一个底层实现的monitor对象,作为竞争对象的锁实例。
    • 将monitor对象的地址(指针)信息记录在synchronized的对象头中
    • 一旦需要获取这把锁,就做竞争。轻量级锁就是CAS变更+自旋,重量级锁就涉及到内核态到用户态的切换
  2. volatile实现可见性与顺序性

  3. Atmoic包实现无锁编程

synchronized实现了线程安全性上的”全能“,通过实现原理,做出一个猜想:

​ 通过外加状态值,对状态值进行竞争,是否可以做到在Java代码层面实现在JVM中Synchronized的效果?AQS是一个值得参考的答案。

注释

  • 提供了依赖于FIFO等待队列的,阻塞锁以及相关同步器(信号/事件)的实现。这个类,是大部分依赖于单一原子整型值标识状态的同步器的基础。
  • 本类支持默认的互斥模式和共享模式。尝试获取的线程需要判断是否获取成功。不同模式下的等待线程,共享同一个FIFO队列。 实现类通常只实现一个模式,但也有两个模式都支持的。

代码

类属性

配置值

  • 自旋阈值
static final long spinForTimeoutThreshold = 1000L;

结构相关

  • 头节点
    • 懒加载,除初始化外仅在setHead中被修改。
    • 如果head存在,它的等待状态必须确保不能为cancelled。
private transient volatile Node head;
  • 尾指针

    • 懒加载
    • 仅在入列方法添加新的等待节点时会被修改。
    private transient volatile Node tail;
    
  • 状态值

    private volatile int state;
    

类方法

通用方法

内部提供了一些基于CAS的方法,用于原子性质的修改。

  • 都是对unsafe类提供的native实现进行的封装。
清单
protected final boolean compareAndSetState(int expect, int update)
    

内部类

node

​ 根据实现的注释,AQS内部是维护了一个FIFO队列的,这个就是FIFO队列的节点实现类,用来存储队列中线程相关信息的。

  • 等待队列是CLH锁队列的一个实现,CLH锁常用在资源锁的实现上,这里的是用来实现阻塞同步器了。

​ 需要注意的一点是,node是一个标注为final的类,因此在继承AQS的实现类中,等待队列的内部实现都是相同的。

  • 属性清单

    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    
类属性
节点属性
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
节点状态值
volatile int waitStatus;

这个状态值有几种:

  • signal(-1)
    • 当前节点的前节点正在(或马上要)阻塞(通过park),因此当前节点必须unpark前节点,当前节点释放或取消。
    • 为避免竞争,获取的方法必须先判断是否需要信号,然后重试获取,失败则阻塞。
  • cancelled(1)
    • 当前节点因为超时或被中断而取消。
    • 节点并不会保持在这个状态。
    • 该状态的线程永远不会再阻塞了。
  • condition(-2)
    • 当前节点此时正在条件队列中(Condition queue)。
    • 不会被用作同步队列节点,直到当状态再转化后变成0为止。
      • 0并不代表任何此属性的其他用途,只是单纯简化机制。
  • propagate(-3)
    • 一个releaseShared 应当被传播到其他节点。
    • 这个值只会被设定在头节点上。
    • doReleaseShared中被设置,用于确保传播行为持续进行,即使因为中断,其他线程已经做完这个工作了。
  • 0
    • 非以上的所有情况。

使用时的额外说明:

  • 非负值代表节点不需要signal

  • 初始值为0,条件节点则是-2(Condition)。

  • 通过CAS进行修改(当条件允许时亦会用非条件的volatile写。)

相关节点
  • 位置相关

    volatile Node prev;
    volatile Node next;
    
    • 前节点

      • 在进入队列时赋值,在出列时变为null(因为GC)。

      • 当前驱节点取消了,@todo

        we short-circuit while
        finding a non-cancelled one
        

        会一直存在因为头节点时不会被取消的:一个节点仅当获取成功的时候变成头节点。

      • 取消状态的线程在acquire时是不可能成功的,而且线程只能取消它自身,不能取消其他线程的。

    • 后节点

      • 代表正在等待释放的待唤醒节点。在入列中初始化,在bypassing cancelled predecessors(@todo)时调整,在出列时为null。

      • 入列操作不会赋值,而是下一个attach操作执行时方才赋值。因此下一个节点为null不必然代表节点在队列末尾,我们会从tail开始双向扫描来确保末尾与否。

      • 取消状态的节点的该引用会指向节点自身而不是空。

  • 功能相关

    volatile Thread thread;
    Node nextWaiter;
    
    • 下个等待节点
      • 等待Condition,或特殊值SHARED态的节点。
      • 因为条件队列仅当互斥模式时被访问,因此此处只用了一个简单的链表来维护等待条件的节点。
      • 这些节点会被转化到重获取的队列中。
      • 因为条件只能是互斥的,因此我们用了一个特殊值来表明share模式。(部分解释了上面的shared的值)