硬核:5W字17张高清图理解同步器框架AbstractQueuedSynchronizer

本文详细解析了Java并发编程中的AbstractQueuedSynchronizer(AQS)框架,包括其核心功能、同步状态管理、CLH队列及其变体、线程的阻塞与唤醒等原理。AQS是构建锁和其他同步组件的基础,通过AQS可以实现独占和共享模式的同步器。文章通过实例分析了AQS的实现细节,帮助读者深入理解并发编程中的同步机制。
摘要由CSDN通过智能技术生成

前提#

并发编程大师Doug Lea在编写JUCjava.util.concurrent)包的时候引入了java.util.concurrent.locks.AbstractQueuedSynchronizer,其实是Abstract Queued Synchronizer,也就是"基于队列实现的抽象同步器",一般我们称之为AQS。其实Doug Lea大神编写AQS是有严谨的理论基础的,他的个人博客上有一篇论文《The java.util.concurrent Synchronizer Framewor》,可以在互联网找到相应的译文《JUC同步器框架》,如果想要深入研究AQS必须要理解一下该论文的内容,然后结合论文内容详细分析一下AQS的源码实现。本文在阅读AQS源码的时候选用的JDK版本是JDK11

出于写作习惯,下文会把AbstractQueuedSynchronizer称为AQS、JUC同步器框或者同步器框架。

AQS的主要功能#

AQSJUC包中用于构建锁或者其他同步组件(信号量、事件等)的基础框架类。AQS从它的实现上看主要提供了下面的功能:

  • 同步状态的原子性管理。
  • 线程的阻塞和解除阻塞。
  • 提供阻塞线程的存储队列。

基于这三大功能,衍生出下面的附加功能:

  • 通过中断实现的任务取消,此功能基于线程中断实现。
  • 可选的超时设置,也就是调用者可以选择放弃等待任务执行完毕直接返回。
  • 定义了Condition接口,用于支持管程形式的await/signal/signalAll操作,代替了Object类基于JNI提供的wait/notify/notifyAll

AQS还根据同步状态的不同管理方式区分为两种不同的实现:独占状态的同步器共享状态的同步器

同步器框架基本原理#

《The java.util.concurrent Synchronizer Framework》一文中其实有提及到同步器框架的伪代码:

// acquire操作如下:
while (synchronization state does not allow acquire) {
    enqueue current thread if not already queued;
    possibly block current thread;
}
dequeue current thread if it was queued;

//release操作如下:
update synchronization state;
if (state may permit a blocked thread to acquire){
    unblock one or more queued threads;
}

撇脚翻译一下:

// acquire操作如下:
while(同步状态申请获取失败){
    if(当前线程未进入等待队列){
        当前线程放入等待队列;
    }
    尝试阻塞当前线程;
}
当前线程移出等待队列

//release操作如下:
更新同步状态
if(同步状态足够允许一个阻塞的线程申请获取){
    解除一个或者多个等待队列中的线程的阻塞状态;
}

为了实现上述操作,需要下面三个基本环节的相互协作:

  • 同步状态的原子性管理。
  • 等待队列的管理。
  • 线程的阻塞与解除阻塞。

其实基本原理很简单,但是为了应对复杂的并发场景和并发场景下程序执行的正确性,同步器框架在上面的acquire操作和release操作中使用了大量的死循环和CAS等操作,再加上Doug Lea喜欢使用单行复杂的条件判断代码,如一个if条件语句会包含大量操作AQS很多时候会让人感觉实现逻辑过于复杂。

同步状态管理#

AQS内部内部定义了一个32位整型的state变量用于保存同步状态:

/**
 * The synchronization state.(同步状态值)
 */
private volatile int state;

// 获取state
protected final int getState() {
    return state;
}

// 直接覆盖设置state
protected final void setState(int newState) {
    state = newState;
}

// CAS设置state
protected final boolean compareAndSetState(int expect, int update) {
    return STATE.compareAndSet(this, expect, update);
}

同步状态state在不同的实现中可以有不同的作用或者表示意义,这里其实不能单纯把它理解为中文意义上的"状态",它可以代表资源数、锁状态等等,下文遇到具体的场景我们再分析它表示的意义。

CLH队列与变体#

CLH锁即Craig, Landin, and Hagersten (CLH) locks,因为它底层是基于队列实现,一般也称为CLH队列锁。CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束自旋。从实现上看,CLH锁是一种自旋锁,能确保无饥饿性,提供先来先服务的公平性。先看简单的CLH锁的一个简单实现:

public class CLHLock implements Lock {

    AtomicReference<QueueNode> tail = new AtomicReference<>(new QueueNode());

    ThreadLocal<QueueNode> pred;
    ThreadLocal<QueueNode> current;

    public CLHLock() {
        current = ThreadLocal.withInitial(QueueNode::new);
        pred = ThreadLocal.withInitial(() -> null);
    }

    @Override
    public void lock() {
        QueueNode node = current.get();
        node.locked = true;
        QueueNode pred = tail.getAndSet(node);
        this.pred.set(pred);
        while (pred.locked) {
        }
    }

    @Override
    public void unlock() {
        QueueNode node = current.get();
        node.locked = false;
        current.set(this.pred.get());
    }

    static class QueueNode {

        boolean locked;
    }

    // 忽略其他接口方法的实现
}	

上面是一个简单的CLH队列锁的实现,内部类QueueNode只使用了一个简单的布尔值locked属性记录了每个线程的状态,如果该属性为true,则相应的线程要么已经获取到锁,要么正在等待锁,如果该属性为false,则相应的线程已经释放了锁。新来的想要获取锁的线程必须对tail属性调用getAndSet()方法,使得自身成为队列的尾部,同时得到一个指向前驱节点的引用pred,最后线程所在节点在其前驱节点的locked属性上自旋,直到前驱节点释放锁。上面的实现是无法运行的,因为一旦自旋就会进入死循环导致CPU飙升,可以尝试使用下文将要提到的LockSupport进行改造。

CLH队列锁本质是使用队列(实际上是单向链表)存放等待获取锁的线程,等待的线程总是在其所在节点的前驱节点的状态上自旋,直到前驱节点释放资源。从实际来看,过度自旋带来的CPU性能损耗比较大,并不是理想的线程等待队列的实现

基于原始的CLH队列锁中提供的等待队列的基本原理,AQS实现一种了CLH锁队列的变体(Variant)AQS类的protected修饰的构造函数里面有一大段注释用于说明AQS实现的等待队列的细节事项,这里列举几点重要的:

  • AQS实现的等待队列没有直接使用CLH锁队列,但是参考了其设计思路,等待节点会保存前驱节点中线程的信息,内部也会维护一个控制线程阻塞的状态值。
  • 每个节点都设计为一个持有单独的等待线程并且"带有具体的通知方式"的监视器,这里所谓通知方式就是自定义唤醒阻塞线程的方式而已。
  • 一个线程是等待队列中的第一个等待节点的持有线程会尝试获取锁,但是并不意味着它一定能够获取锁成功(这里的意思是存在公平和非公平的实现),获取失败就要重新等待。
  • 等待队列中的节点通过prev属性连接前驱节点,通过next属性连接后继节点,简单来说,就是双向链表的设计
  • CLH队列本应该需要一个虚拟的头节点,但是在AQS中没有直接提供虚拟的头节点,而是延迟到第一次竞争出现的时候懒创建虚拟的头节点(其实也会创建尾节点,初始化时头尾节点是同一个节点)。
  • Condition(条件)等待队列中的阻塞线程使用的是相同的Node结构,但是提供了另一个链表用来存放,Condition等待队列的实现比非Condition等待队列复杂。

线程阻塞与唤醒#

线程的阻塞和唤醒在JDK1.5之前,一般只能依赖于Object类提供的wait()notify()notifyAll()方法,它们都是JNI方法,由JVM提供实现,并且它们必须运行在获取监视器锁的代码块内(synchronized代码块中),这个局限性先不谈性能上的问题,代码的简洁性和灵活性是比较低的。JDK1.5引入了LockSupport类,底层是基于Unsafe类的park()unpark()方法,提供了线程阻塞和唤醒的功能,它的机制有点像只有一个允许使用资源的信号量java.util.concurrent.Semaphore,也就是一个线程只能通过park()方法阻塞一次,只能调用unpark()方法解除调用阻塞一次,线程就会唤醒(多次调用unpark()方法也只会唤醒一次),可以想象是内部维护了一个0-1的计数器。

LockSupport类如果使用得好,可以提供更灵活的编码方式,这里举个简单的使用例子:

public class LockSupportMain implements Runnable {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

    private Thread thread;

    private void setThread(Thread thread) {
        this.thread = thread;
    }

    public static void main(String[] args) throws Exception {
        LockSupportMain main = new LockSupportMain();
        Thread thread = new Thread(main, "LockSupportMain");
        main.setThread(thread);
        thread.start();
        Thread.sleep(2000);
        main.unpark();
        Thread.sleep(2000);
    }

    @Override
    public void run() {
        System.out.println(String.format("%s-步入run方法,线程名称:%s", FORMATTER.format(LocalDateTime.now()),
                Thread.currentThread().getName()));
        LockSupport.park();
        System.out.println(String.format("%s-解除阻塞,线程继续执行,线程名称:%s", FORMATTER.format(LocalDateTime.now()),
                Thread.currentThread().getName()));
    }

    private void unpark() {
        LockSupport.unpark(thread);
    }
}
// 某个时刻的执行结果如下:
2019-02-25 00:39:57.780-步入run方法,线程名称:LockSupportMain
2019-02-25 00:39:59.767-解除阻塞,线程继续执行,线程名称:LockSupportMain

LockSupportpark()方法也有带超时的变体版本方法,遇到带超时期限阻塞等待场景下不妨可以使用LockSupport#parkNanos()

独占线程的保存#

AbstractOwnableSynchronizerAQS的父类,一个同步器框架有可能在一个时刻被某一个线程独占,AbstractOwnableSynchronizer就是为所有的同步器实现和锁相关实现提供了基础的保存、获取和设置独占线程的功能,这个类的源码很简单:

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {

    private static final long serialVersionUID = 3737899427754241961L;

    protected AbstractOwnableSynchronizer() { }
    
    // 当前独占线程的瞬时实例 - 提供Getter和Setter方法
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

它就提供了一个保存独占线程的变量对应的SetterGetter方法,方法都是final修饰的,子类只能使用不能覆盖。

CLH队列变体的实现#

这里先重点分析一下AQS中等待队列的节点AQS的静态内部类Node的源码:

static final class Node {
   // 标记一个节点处于共享模式下的等待
   static final Node SHARED = new Node();
   // 标记一个节点处于独占模式下的等待
   static final Node EXCLUSIVE = null;
   // 取消状态
   static final int CANCELLED =  1;
   // 唤醒状态
   static final int SIGNAL    = -1;
   // 条件等待状态
   static final int CONDITION = -2;
   // 传播状态
   static final int PROPAGATE = -3;
   // 等待状态,初始值为0,其他可选值是上面的4个值
   volatile int waitStatus;
   // 当前节点前驱节点的引用
   volatile Node prev;
   // 当前节点后继节点的引用
   volatile Node next;
   // 当前节点持有的线程,可能是阻塞中等待唤醒的线程
   volatile Thread thread;
   // 下一个等待节点
   Node nextWaiter;
   // 当前操作的节点是否处于共享模式
   final boolean isShared() {
      return nextWaiter == SHARED;
   }
   // 获取当前节点的前驱节点,确保前驱节点必须存在,否则抛出NPE  
   final Node predecessor() {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }
    
    // 空节点,主要是首次创建队列的时候创建的头和尾节点使用
    Node() {}

    // 设置下一个等待节点,设置持有线程为当前线程
    Node(Node nextWaiter) {
        this.nextWaiter = nextWaiter;
        THREAD.set(this, Thread.currentThread());
    }

    // 设置waitStatus,设置持有线程为当前线程
    Node(int waitStatus) {
        WAITSTATUS.set(this, waitStatus);
        THREAD.set(this, Thread.currentThread());
    }

    // CAS更新waitStatus  
    final boolean compareAndSetWaitStatus(int expect, int update) {
        return WAITSTATUS.compareAndSet(this, expect, update);
    }
    // CAS设置后继节点
    final boolean compareAndSetNext(Node expect, Node update) {
       
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倾听铃的声

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值