AQS原理详解

什么是AQS?

AQS 即 AbstractQueuedSynchronizer,是 java.util.concurrent.locks 包的一个重要概念。Java 中锁实现/同步的几种方式:synchronized,ReentrantLock,CAS。其中,可重入锁 ReentrantLock 就是基于 AbstractQueuedSynchronizer(AQS)的。因此,理解 AQS 的实现原理,对 Java 锁理解非常重要。

AQS 具备以下的几个特性:

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断。

AQS原理

AQS 依赖于先进先出(FIFO)阻塞队列,提供了一个实现阻塞锁和同步器(semaphores-信号量,events-事件等)的框架。AbstractQueuedSynchronizer 这个类的设计是作为大多数类型依赖一个单原子值来表示状态的同步器的一个有用的基础。

AQS 的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量state和一个FIFO线程等待队列CLH(多线程竞争state被阻塞时会进入此队列)。

​ CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。AQS 是将每一条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node),来实现锁的分配。

同时AQS 也是自旋锁,在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功。

AQS 定义了两种资源的共享方式:

  • 独占(Exclusive)如 ReentrantLock
  • 共享(Share)如 Semaphore 或 CountDownLatch

AQS源码解析

AQS类定义

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable 

队列节点定义

队列中节点的结构,在 AQS 的 Node 中定义,这是一个 static final 类型。除了双向链表的相关定义:前驱节点、后继节点、节点值(线程 Thread):

volatile Node prev;
volatile Node next;
volatile Thread thread;
节点等待模式

标记节点等待模式的标记,类型也是 Node,SHARED 值不为 null 时,表示是共享模式;

EXCLUSIVE 不为 null 时,表示是独占模式

/** 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;
等待状态值

waitStatus,用于表示线程已取消:

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
 * waitStatus value to indicate the next acquireShared should
 * unconditionally propagate
 */
static final int PROPAGATE = -3;

volatile int waitStatus;
nextWaiter

​ 这个属性比较特殊,因为前面已经有指向后继节点的 next,又增加了一个指向下一个条件等待节点,或特殊 SHARED 值的指针。从下面的官方注释中来看,因为条件队列只有在独占模式下保持时才被访问,所以我们只需要一个简单的链接队列来保持等待条件的节点。

/**
 * Link to next node waiting on condition, or the special
 * value SHARED.  Because condition queues are accessed only
 * when holding in exclusive mode, we just need a simple
 * linked queue to hold nodes while they are waiting on
 * conditions. They are then transferred to the queue to
 * re-acquire. And because conditions can only be exclusive,
 * we save a field by using special value to indicate shared
 * mode.
 */
Node nextWaiter;

​ AQS 的阻塞队列是以双向的链表的形式保存的,是通过 prev 和 next 建立起关系的,但是 AQS 中的条件队列是以单向链表的形式保存的,是通过 nextWaiter 建立起关系的,也就是 AQS 的阻塞队列和 AQS 中的条件队列并非同一个队列。

Node 定义- CLH

CLH 锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁(基于单向链表(隐式创建)的高性能、公平的自旋锁),由于是 Craig、Landin 和 Hagersten 三位大佬的发明,因此命名为 CLH 锁。AQS 内部的 FIFO 线程等待队列,通过内部类 Node 来实现

Node除了默认的无参构造方法外,还有两个有参数的构造方法:

Node(Thread thread, Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

Node(Thread thread, int waitStatus) { // Used by Condition
    this.waitStatus = waitStatus;
    this.thread = thread;
}

AQS 的队列

队尾添加节点
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

可见操作是通过 compareAndSetTail()方法来操作的,依然是 CAS 保障同步。但在 return 之前,还有 enq(node)方法调用,我们再看这段代码:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

操作是在一个死循环中,根据 tail 节点是否为空分为两块大的逻辑:tail == null,cas 设置 head;否则,节点的前驱指向 tail 节点,然后 cas 设置 tail。

操作队列头结点
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

这里只是一个简单的设置方法,node 的线程值和前驱设置为 null。

AQS 的实现

包括:Reentrant、Semaphore,CountDownLatch、ReentrantReadWriteLock、SynchronousQueue 和 FutureTask。

关于资源获取与释放

获取是一种依赖状态的操作,并且通常会阻塞,直到达到了理想状态为止。以前面提到的几种 AQS 的实现为例:

  • ReentrantLock,“获取”操作意味着“等待直到锁可被获取”。
  • Semaphore,“获取”操作意味着“等待直到许可可被获取”。
  • CountDownLatch,“获取”操作意味着“等待直到闭锁达到结束状态”。
  • FutureTask,“获取”操作意味着“等待直到任务完成”。

释放并不是一个可阻塞的操作,当执行“释放”操作时,所有在请求时被阻塞的线程都会开始执行。

状态机

AQS 使用 state(int)以表示状态,提供了 getState、setState 及 compareAndSetState 等 protected 类型方法进行状态转换。通过巧妙地使用 state,表示各种关键状态:

  • ReentrantLock 用 state 表示所有者线程已经重复获取该锁的次数。
  • Semaphore 用 state 表示剩余的许可数量。
  • CountDownLatch 用 state 表示闭锁的状态,如关闭、打开。
  • FutureTask 用 state 表示任务的状态,如尚未开始、正在运行、已完成、已取消。

除了 state,在同步器类中还可以自行管理一些额外的状态变量。如:

  • ReentrantLock 保存了锁的当前所有者的信息,这样就能区分某个获取操作是重入的还是竞争的。
  • FutureTask 用 result 表示任务的结果,该结果可能是计算得到的答案,也可能是抛出的异常。

总结

Java 并发编程的核心在于 java.concurrent.util 包,juc 中大多数同步器的实现都围绕了一个公共的行为,比如等待队列、条件队列、独占获取、共享获取等,这个行为的抽象就是基于AbstractQueuedSynchronized(AQS)。AQS 是定义了多线程访问共享资源的同步器框架。

简单来讲,AQS 就好比一个行为准则,而并发包中的大多数同步器(比如:Lock、Latch、Barrier 等)都是在这个准则下实现。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值