【工作记录】AQS学习笔记

简介

在Java中,AbstractQueuedSynchronizer(AQS)是Java并发包(java.util.concurrent.locks)中一个用于构建锁和同步器框架的基础类。提供了一种实现阻塞锁和其他同步组件的底层机制。

基本原理概述

它的核心原理包括以下关键点:

  • 状态管理:
    AQS通过一个volatile类型的整型变量state来表示同步状态。比如在独占锁(如ReentrantLock)中,state为0表示锁未被任何线程持有,大于0则表示当前持有锁的线程数量以及重入次数。
  • 等待队列:
    AQS维护了一个FIFO双向链表作为同步队列,即CLH队列,用于存放等待获取锁或同步状态的线程。当线程尝试获取锁但发现状态不可用时,会将自己包装成一个节点(Node)并加入到队列尾部进行自旋或挂起等待。
  • 获取/释放同步状态:
    提供了tryAcquire()和tryRelease()等模板方法给子类去具体实现。这些方法决定了如何基于state值去尝试获取或释放同步状态。例如,在非公平锁中,tryAcquire()可能直接尝试获取锁,而在公平锁中,它会检查是否有其他线程等待更长时间。
  • 线程阻塞与唤醒:
    利用Unsafe类或者其他并发工具对线程进行阻塞和唤醒操作。当线程无法立即获取锁时,会调用acquireQueued()方法将线程放入等待队列,并进入park()方法挂起;而当锁释放时,则会从等待队列中的某个节点开始唤醒等待线程,使其重新尝试获取锁。
  • 可重入性支持:
    AQS可以支持重入,这意味着已经获得锁的线程可以再次成功请求该锁,对应的state会递增以记录重入次数。
  • 共享模式与独占模式:
    AQS同时支持独占模式(只有一个线程能获取到同步状态)和共享模式(多个线程可以同时获取到同步状态),分别对应于ReentrantLock、Semaphore等不同的并发组件。
  • 中断处理:
    当等待线程被中断时,AQS会根据中断策略进行相应处理,这通常由具体的同步组件决定,可以通过覆盖tryAcquireSharedInterruptibly()等方法实现。
    通过继承AQS并实现上述抽象方法,开发者可以创建各种复杂的同步组件,如互斥锁、信号量、读写锁等,无需关注底层的线程调度和阻塞/唤醒逻辑,大大简化了并发编程的复杂度。

核心流程说明

AbstractQueuedSynchronizer(AQS)的流程主要包括线程获取和释放同步状态以及在无法立即获取时如何进入等待队列、唤醒后续线程等步骤。

以下是其核心流程概述:

  1. 获取同步状态:

    • 当线程尝试获取同步状态时,首先调用子类重写的tryAcquire(int arg)方法。
      • 如果该方法返回true,表示线程成功获取到同步状态,并执行相应的业务逻辑。
      • 如果返回false,表示当前不能获取到同步状态,线程需要被放入同步队列中等待。
     public final void acquire(int arg) {
         if (!tryAcquire(arg) &&
             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
             selfInterrupt();
     }
    
    • addWaiter(Node.EXCLUSIVE)将当前线程包装成一个独占模式的Node节点并插入同步队列尾部。
    • acquireQueued(Node node, int arg)让线程在队列中自旋或阻塞等待,直到获取到同步状态或者被中断。
  2. 等待与自旋:

    • 在同步队列中的线程会不断地检查自己的前驱节点是否为头节点,如果是,则再次尝试获取同步状态。
    • 若非头节点或尝试获取失败,则通过循环+CAS的方式更新节点的waitStatus值,并可能进入Park操作进行线程挂起。
  3. 释放同步状态:

    • 线程在完成任务后调用release(int arg)来释放同步状态。
    • 调用子类重写的tryRelease(int releases)方法,如果该方法返回true,表示同步状态成功释放
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
  4. 唤醒等待线程:

    • unparkSuccessor(Node node)方法从同步队列中找到第一个处于等待状态且合法的节点(通常是头节点的下一个节点),然后调用LockSupport.unpark(s.thread)解除该节点关联线程的阻塞状态,使其有机会再次尝试获取同步状态。
  5. 可重入性支持:
    AQS通过维护一个计数器state来支持锁的可重入性。每当持有锁的线程再次请求时,state递增;当线程退出同步代码块时,state递减,直到state为0时其他线程才能获取到锁。

整个AQS的工作流程围绕着对state变量的操作以及同步队列的管理展开,有效地实现了锁的获取和释放以及线程间的同步协作。

AQS关键源码解析

以下是结合上述核心流程涉及到的关键源码解析:

  1. 状态管理(state)

    private volatile int state; // 核心状态变量,volatile保证可见性和有序性
    
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    

    state字段表示同步状态,通过CAS操作(compareAndSetState方法)来保证原子性的更新。

  2. 同步队列(CLH队列)

    static final class Node {
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        
    }
    
    transient volatile Node head;
    transient volatile Node tail;   
    

​ AQS内部维护了一个FIFO双向链表作为同步队列,节点类型为Node,每个节点代表一个等待获取同步状态的线程。其中head指向队列头节点,tail指向队列尾节点。

  1. 获取同步状态(acquire)

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    
    // 子类需要重写tryAcquire方法以实现自定义同步策略
    
    • acquire()方法尝试获取同步状态,首先调用子类实现的tryAcquire()方法尝试获取,若失败,则将当前线程包装成Node并加入到同步队列中,然后在队列中进行自旋或阻塞等待。
    • addWaiter()方法将线程封装为Node并插入队列尾部。
    • acquireQueued()方法让线程在队列中进行循环等待,直到获取到同步状态或被中断。
  2. 释放同步状态(release)

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    
    protected boolean tryRelease(int releases) {
        throw new UnsupportedOperationException();
    }
    
    // 子类需要重写tryRelease方法以实现同步状态的释放逻辑
    

    release()方法尝试释放同步状态,首先调用子类实现的tryRelease()方法释放资源,成功后检查头结点状态,并唤醒其后继节点上的线程。

  3. 唤醒等待线程(unparkSuccessor)

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
    
        Node s = node.next;
        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)
            LockSupport.unpark(s.thread);
    }
    

    当同步状态释放时,会调用unparkSuccessor()方法从同步队列中找到第一个等待状态合法的节点,并解除该节点所关联线程的阻塞状态。

总之,AQS通过上述机制提供了一种基础架构,使得开发者可以基于此实现各种复杂的同步组件,如ReentrantLock、Semaphore、CountDownLatch等。

与Synchronized对比

对比项 synchronized AQS
功能性 1. 内置关键字,无需额外引入类库。
2. 支持互斥性和可见性,确保同一时间只有一个线程可以访问同步代码块或方法。
3. 自动管理锁的获取和释放,支持可重入,即一个线程获取到锁后还能再次进入加锁区域。
4. 不提供超时等待锁的功能,且不支持中断请求。
1. 是一个底层框架,用于构建更高级别的并发工具如ReentrantLock、Semaphore、CountDownLatch等。
2. 同样支持互斥性和可见性,并通过state变量实现了可重入。
3. 提供了比synchronized更多的功能选项
灵活性 使用简单,语法直观,但控制粒度相对较粗,只能以整个对象或者方法为单位进行加锁。 更灵活,可以通过自定义同步器实现更多定制化的同步需求,比如复杂的条件等待、读写锁等功能。
性能 在JDK1.6及以后版本中进行了很多优化,如适应性自旋、锁消除、锁粗化等,性能已经相当高,在许多常见场景下与基于AQS的锁性能相近。 在某些特定场景下可能有更好的性能表现,如使用自旋锁避免上下文切换,以及通过“工作窃取”算法减少线程间的竞争。
但是,如果使用不当(如过于频繁地创建和销毁AQS实例),可能会导致性能下降。

总结

本文对AQS的基本原理和关键代码做了简单解析,同时对比了aqs和synchronized的区别。

创作不易,欢迎一键三连~~

你可能感兴趣的:(工作记录,java基础,学习,笔记,java,AQS)