JUC中的AQS底层详细超详解

摘要:当你使用java实现一个线程同步的对象时,一定会包含一个问题:你该如何保证多个线程访问该对象时 , 正确地进行阻塞等待,正确地被唤醒?
本文分享自华为云社区《JUC中的AQS底层详细超详解,剖析AQS设计中所需要考虑的各种问题!》,作者: breakDawn。
java中AQS究竟是做什么的?当你使用java实现一个线程同步的对象时,一定会包含一个问题:
你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒?
关于这个问题,java的设计者认为应该是一套通用的机制
因此将一套线程阻塞等待以及被唤醒时锁分配的机制称之为AQS
全称 AbstractQuenedSynchronizer
中文名即抽象的队列式同步器。
基于AQS,实现了例如ReentenLock之类的经典JUC类 。
AQS简要步骤
  1. 线程访问资源 , 如果资源足够,则把线程封装成一个Node,设置为活跃线程进入CLH队列,并扣去资源
  2. 资源不足,则变成等待线程Node,也进入CLH队列
  3. CLH是一个双向链式队列, head节点是实际占用锁的线程 , 后面的节点则都是等待线程所对应对应的节点
AQS的资源statestate定义AQS中的资源是一个int值,而且是volatile的 , 并提供了3个方法给子类使用:
private volatile int state;protected final int getState() { return state;}protected final void setState(int newState) {state = newState;}// cas方法compareAndSetState(int oldState, int newState);如果state上限只有1,那么就是独占模式Exclusive,例如 ReentrantLock
如果state上限大于1,那就是共享模式Share,例如 Semaphore、CountDownLatch、ReadWriteLock , CyclicBarrier
已经有CAS方法了,为什么资源state还要定义成volatile的?对外暴露的getter/setter方法,是走不了CAS的 。而且setter/getter没有被synchronized修饰 。所以必须要volatile,保证可见性
这样基于AQS的实现可以直接通过getter/setter操作state变量,并且保证可见性,也避免重排序带来的影响 。比如CountDownLatch,ReentrantReadWriteLock,Semaphore都有体现(各种getState、setState)
对资源的操作什么时候用CAS,什么使用setState?volatile的state成员有一个问题 , 就是如果是复合操作的话不能保证复合操作的原子性
因此涉及 state增减的情况,采用CAS
如果是state设置成某个固定值 , 则使用setState
AQS的CLH队列为什么需要一个CLH队列这个队列的目的是为了公平锁的实现
即为了保证先到先得,要求每个线程封装后的Node按顺序拼接起来 。
CLH本质?是一个Queue容器吗不是的 , 本质上是一个链表式的队列
因此核心在于链表节点Node的定义
JUC中的AQS底层详细超详解

文章插图
除了比较容易想到的prev和next指针外
【JUC中的AQS底层详细超详解】还包含了该节点内的线程
以及 waitStatus 等待状态
4种等待状态如下:
  • CANCELLED(1): 因为超时或者中断,节点会被设置为取消状态 , 被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态;
  • SIGNAL(-1):后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点 , 使后继节点的线程得以运行
  • CONDITION(-2) : 点在等待队列中,节点线程等待在Condition上 , 当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
  • PROPAGATE(-3) : 表示下一次共享式同步状态获取将会无条件地传播下去
  • INIT( 0):
入队是怎么保证安全的?入队过程可能引发冲突
因此会用CAS保障入队安全 。
private Node enq(final Node node) { //多次尝试,直到成功为止 for (;;) { Node t = tail; //tail不存在 , 设置为首节点 if (t == null) { if (compareAndSetHead(new Node()))tail = head; } else { //设置为尾节点 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }出队过程会发生什么?一旦有节点出队,说明有线程释放资源了 , 队头的等待线程可以开始尝试获取了 。
于是首节点的线程释放同步状态后,将会唤醒它的后继节点(next)
而后继节点将会在获取同步状态成功时将自己设置为首节点
注意在这个过程是不需要使用CAS来保证的,因为只有一个线程能够成功获取到同步状态

推荐阅读