AQS-学习

204 阅读7分钟

简介

AQS(AbstractQueuedSynchronizer):抽象的队列式同步器。是JUC下很多并发工具(ReentrantLock、CountDownLatch等)的基类;

AQS运用了模式设计方法,将可复用操作封装成模版(独占、共享),子类只需要关注 加锁、释放锁的逻辑即可。

基本组成

  • state
  • Node 组成的CLH 双向队列,FIFO。 简称:同步队列
  • Node 组成的条件 单向队列。 简称:等待队列

state

描述:

表示资源状态,不同类表示的资源意义也不同;

volatile 修饰,CAS设置状态后,其它线程可以直接感知;

实现类含义说明
ReentrantLock资源状态0:资源没被获取,1:资源已被获取,>1:资源被同线程重入次数
CountDownLatch未被释放的资源数还有多少个资源没被释放
Semaphore可获取的资源数量还可以获取多少个资源
ReentrantReadWriteLock读、写锁的状态前16位表示读锁状态,后16位表示写锁状态
LimitLatch ( Tomcat实现)未使用使用自定义的count与limit进行比较
CountDownLatch2 (RocketMq实现)未被释放的资源数还有多少个资源没被释放; RebalanceImpl,默认是20s对消息队列与消息实例分配一次。先reset设为1,在await 20s,中途可以被mq中断立即进行分配
ThreadPoolExecutor.Worker资源状态0:资源未被获取,1:资源已被获取; 不可重入

Node

描述: Node中包含线程的引用,可以简单的把它想象成装线程的东西

字段:

状态名称状态描述
CANCELLED:1已取消。是个异常状态 如代码执行异常,在catch中会将节点设为CANCELLED
SIGNAL:-1当前节点执行完后会唤醒下个节点中的thread
CONDITION:-2表示Node节点在等待队列中。Conditions时使用
PROPAGATE:-3共享模式下的状态。CountDownLatch时使用
0默认状态

waiteState:节点等待状态 prev、next:上、下 一个Node节点引用

thread:线程引用

nextWaiter:

  • 等待队列中表示下一个节点引用
  • 同步队列中表示独占锁、共享锁标记【待补充】

同步队列

描述:

当线程获取资源失败或不满足获取条件后,将插入到同步队列尾部,并将前节点的waiteState设为SIGNAL,然后通过LockSupport#park挂起自己,等待被唤醒。

等待队列

ArrayBlockingQueue就借此实现的;

描述:

当线程执行AQS.ConditionObject#await之后,将插入到等待队列并从同步队列中删除节点,然后挂起当前线程

。。。阻塞中。。。

当别的线程执行AQS.ConditionObject#signal时,修改节点状态(-2 -> 0)后再次插入到同步队列,如果前一个节点状态不为SIGNAL或设置SIGNAL失败后将直接唤醒,否则会等到AQS#release时再唤醒

从阻塞中苏醒后,会再次尝试获取锁

ConditionObject

描述:

AQS中的内部类,包含 await、signal等方法,一个条件下对应一个等待队列

主要方法分析

独占模式

  • acquire
  • release

共享模式

  • acquireShare【待补充】

  • releaseShare【待补充】 子类为公平锁时使用

  • hasQueuedPredecessors

带计时的获取资源(CountDownLatch#await)

  • tryAcquireNanos【待补充】

acquire

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

以上代码可调整为

public final void acquire(int arg) {
  
  // 调用子类实现类,尝试获取资源,获取成功直接返回
   if(tryAcquire(arg)){
        return;
    }
  
  // 创建Node节点,插入到同步队列尾部
  Node node = addWaiter(Node.EXCLUSIVE);
  
  // 从同步队列中获取锁,失败后会被挂起,线程阻塞
  if(acquireQueued(node,arg)){
        // 如果是被中断唤醒,则继续标记线程中断状态
        selfInterrupt();
    }
}
​
private Node addWaiter(Node mode) {
    // 新建Node节点
    Node node = new Node(Thread.currentThread(), mode);
    // 因为等待队列是FIFO,所以选tail节点作为perv节点
    Node pred = tail;
    // 队列未初始化时,pred为null
    if (pred != null) {
        // 将尾节点赋值给当前节点的上一个节点
        node.prev = pred;
          // 尝试把队尾设为node,执行成功后 node = tail  。并不是把node赋值给pred
        if (compareAndSetTail(pred, node)) {
           // 将前一个尾节点的下一个节点设为当前节点
            pred.next = node;
            return node;
        }
    }
    // 队列未初始化或上面的CAS失败进入
    enq(node);
    return node;
}
​
private final boolean compareAndSetTail(Node expect, Node update) {
  // this: 包含要修改字段的对象
  // tailOffset: 字段在对象内的偏移量,指的就是tail字段
  // expect:字段的期望值
  // update:如果该字段的值等于字段的期望值,用于更新字段的新值
    return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
​
// 如果addWaiter中的compareAndSetTail失败或队列需要初始化,会执行该方法
// 会返回当前节点的上一个节点
private Node enq(final Node node) {
    // 自旋一直到入队成功
  for (;;) {
      Node t = tail;
      if (t == null) { // CAS设置队头
          if (compareAndSetHead(new Node()))
              tail = head;
      } else {
          // 将尾节点设置为当前节点的下一个节点
          node.prev = t;
           // CAS设置队尾
          if (compareAndSetTail(t, node)) {
              // 将前一个尾节点的下一个节点设为当前节点
              t.next = node;
              return t;
          }
      }
  }
}
​
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)) {
                // 将节点设置为头节点,清空Thread与prev
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 不是第一个节点或获取锁失败,判断是否挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 挂起当前线程,并且检测是否中断
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
​
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前一个节点等待状态为SIGNAL,代表当前节点可以被唤醒,可以去阻塞了
    if (ws == Node.SIGNAL)
        return true;
    // 前一个节点状态为CANCELLED,则需要跳过,进行往前找
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // CAS将前一个节点等待状态改为SIGNAL
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}
​
 private final boolean parkAndCheckInterrupt() {
        // 线程挂起
        LockSupport.park(this);
        // 检测是否中断,消除中断标记,因为 除了被前一节点唤醒之外,还有可能被Thread#interrupt方法唤醒
        // 如果不消除中断标记,再次获取锁失败后则无法进行阻塞
        return Thread.interrupted();
    }

release

public final boolean release(int arg) {
   // 调用子类的实现类
    if (tryRelease(arg)) {
        Node h = head;
        // 头节点不为空且状态不是0,进行唤醒操作
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
​
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        // 将头节点的状态更新为0
        compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
     // 若后继结点为空,或状态为CANCEL,则从后尾部往前遍历找到最前的一个处于正常阻塞状态的结点
    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;
    }
    if (s != null)
        // 唤醒下一个节点,下一个节点将从 #acquireQueued中的#parkAndCheckInterrupt方法醒来
        LockSupport.unpark(s.thread);
}

hasQueuedPredecessors

// true:自己前面还有节点 false: 说明自己就是第一个节点
public final boolean hasQueuedPredecessors() {
      
    Node t = tail; 
    Node h = head;
    Node s;
   // 获取头节点的下一个节点,并且 下一个节点中的线程 != 当前线程
    return h != t &&
          ((s = h.next) == null || s.thread != Thread.currentThread());
}

acquireShare

/**
 * 共享式获取资源的模版方法
 */
public final void acquireShared(int arg){
 
    // 调用子类实现,<0代表未获取到资源
    if (tryAcquireShared(arg) < 0)
    // 未获取到资源,入队
    doAcquireSharedInterruptibly(arg);
}

问题

  • #relase#unparkSuccessor,如果头节点的下个节点为空或等待状态>0,为什么会从尾节点开始找,不是破坏同步队列了吗?公平锁不会变的不公平吗?

并发情况下,enq方法可能会导致,某节点下的next节点为null,但该节点确是后续节点的prev节点。

详情可以参照: www.tqwba.com/x_d/jishu/2…

  • 在AQS#parkAndCheckInterrupt中,LockSupport#park之后要Thread#interrupted方法?

Thread#interrupt方法也会唤醒LockSupport#park,如果不消除中断状态,CAS再次获取锁失败后将无法阻塞。

详情可参照:LockSupport

  • AQS的等待队列跟CLH比有哪些区别

    • CLH是单向队列,AQS等待队列是双向的
    • CLH是自旋查看上一个节点的lock状态,AQS等待队列也有状态是waitStatus,但不会一直自旋查看上一个节点,而是自旋一段时间符合条件后阻塞,让出cpu的时间片,等待上一个节点唤醒
  • ConditionObject#await、#signal与Object#wait、#notify区别

【待补充】

  • 独占模式、共享模式区别
    • 独占模式,资源只能被一个线程占有(ReentrantLock)。共享模式,资源可以被多个线程同时占有(CountDownLatch、Semaphore)。

    • AQS子类#tryAcquire返回值不同,独占返回boolean,true代表获取成功,共享返回int,>=0代表成功

    • 独占模式在tryAcquire、tryRelease修改AQS.state状态时不需要考虑多线程情况,而共享模式则需要考虑,像CountDownLatch、Semaphore都使用自旋+CAS解决