J.U.C Review - AQS核心方法解析

文章目录


AQS简介

AQSAbstractQueuedSynchronizer)是一个用来构建锁和同步器的框架。

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)队列存储数据;
  • 同步:实现了同步的功能。

它是一个抽象类,提供了构建同步器的基础功能,子类可以通过实现一些关键的protected方法来创建自定义的同步器。

AQS 主要用于实现各种同步器,如:

  • ReentrantLock
  • Semaphore
  • ReentrantReadWriteLock
  • SynchronousQueue
  • FutureTask

通过AQS,开发者能够高效地构建各种同步器,满足不同的需求。


AQS的数据结构

AQS的核心数据结构包括:

state变量

  1. state变量 :用于标识资源的状态。它是一个volatile类型的整型变量。AQS通过以下方法来操作state变量:

    java 复制代码
    getState()
    setState()
    compareAndSetState()

这些方法都是原子操作,其中compareAndSetState使用了UnsafecompareAndSwapInt()方法来保证操作的原子性。

等待队列

  1. 等待队列 :AQS使用一个FIFO队列来管理线程的排队和阻塞。这个队列实际上存储的是Node节点,而不是线程对象。每个Node节点包含以下信息:
java 复制代码
   static final class Node {
    // 标记一个结点(对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点(对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null; 

    // waitStatus的值,表示该结点(对应的线程)已被取消
    static final int CANCELLED = 1; 
    // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的值,表示该结点(对应的线程)在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
    static final int PROPAGATE = -3;

    // 等待状态,取值范围,-3,-2,-1,0,1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点

    
    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }
    
    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }
    
    // 其它方法忽略,可以参考具体的源码
}

// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}
  • SHAREDEXCLUSIVE:分别表示共享模式和独占模式的标记。
  • waitStatus :表示节点的等待状态。它可以是CANCELLED(已取消)、SIGNAL(需要唤醒后继节点)、CONDITION(等待条件)或PROPAGATE(需要继续唤醒后继节点)。

Node节点通过prevnext实现双向队列,支持线程的排队。而通过nextWaiter实现条件队列,主要用于Condition的等待线程。


资源共享模式

AQS支持两种资源共享模式:

  1. 独占模式(Exclusive)

    • 资源是独占的,一次只能由一个线程获取。例如:ReentrantLock
  2. 共享模式(Share)

    • 资源可以被多个线程同时获取,具体的资源个数可以通过参数指定。例如:SemaphoreCountDownLatch

    子类通常只需实现其中一种模式的逻辑。但也有同步类同时实现两种模式,如 ReadWriteLock


AQS的主要方法源码解析

AQS的设计基于模板方法模式,提供了一些必须由子类实现的方法。这些方法包括:

  • isHeldExclusively():检查当前线程是否独占了资源。这通常与条件变量的使用有关。
  • tryAcquire(int arg):尝试以独占模式获取资源。
  • tryRelease(int arg):尝试释放资源(独占模式)。
  • tryAcquireShared(int arg):尝试以共享模式获取资源。
  • tryReleaseShared(int arg):尝试释放资源(共享模式)。

这些方法虽然是protected的,但AQS本身并不提供具体的实现,而是抛出UnsupportedOperationException。子类需要实现这些方法以定义具体的资源获取和释放逻辑。例如:

java 复制代码
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

获取资源

获取资源的入口方法是acquire(int arg)

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. tryAcquire(arg):尝试获取资源。如果获取失败,则将当前线程加入等待队列。
  2. addWaiter(Node.EXCLUSIVE) :将线程封装成Node节点并添加到等待队列的尾部。

addWaiter方法源码如下:

java 复制代码
 private Node addWaiter(Node mode) {
    // 生成该线程对应的Node节点
    Node node = new Node(Thread.currentThread(), mode);
    // 将Node插入队列中
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // 使用CAS尝试,如果成功就返回
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果等待队列为空或者上述CAS失败,再自旋CAS插入
    enq(node);
    return node;
}

// 自旋CAS插入等待队列
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;
            }
        }
    }
}

插入等待队列addWaiter方法通过compareAndSetTailenq方法将节点插入队列尾部。enq方法使用自旋CAS确保线程安全。

获取资源的核心逻辑在acquireQueued方法中:

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            final Node p = node.predecessor();
            // 如果node的前驱结点p是head,表示node是第二个结点,就可以尝试去获取资源了
            if (p == head && tryAcquire(arg)) {
                // 拿到资源后,将head指向该结点。
                // 所以head所指的结点,就是当前获取到资源的那个结点或null。
                setHead(node); 
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果自己可以休息了,就进入waiting状态,直到被unpark()
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
  • node.predecessor():获取节点的前驱节点。
  • tryAcquire(arg):尝试获取资源。
  • setHead(node):设置新的头节点。
  • parkAndCheckInterrupt():将当前线程挂起,直到被唤醒。

这里parkAndCheckInterrupt方法内部使用到了LockSupport.park(this),
LockSupport类是Java 6 引入的一个类,提供了基本的线程同步原语。LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数:
park(boolean isAbsolute, long time):阻塞当前线程

unpark(Thread jthread):使给定的线程停止阻塞


释放资源

释放资源的方法是release(int arg)

java 复制代码
 public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

private void unparkSuccessor(Node node) {
    // 如果状态是负数,尝试把它设置为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 得到头结点的后继结点head.next
    Node s = node.next;
    // 如果这个后继结点为空或者状态大于0
    // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消
    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);
}
  • tryRelease(arg):尝试释放资源。
  • unparkSuccessor(node):唤醒等待队列中的下一个线程。
相关推荐
南宫生5 分钟前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石13 分钟前
12/21java基础
java
李小白6621 分钟前
Spring MVC(上)
java·spring·mvc
GoodStudyAndDayDayUp34 分钟前
IDEA能够从mapper跳转到xml的插件
xml·java·intellij-idea
装不满的克莱因瓶1 小时前
【Redis经典面试题六】Redis的持久化机制是怎样的?
java·数据库·redis·持久化·aof·rdb
n北斗1 小时前
常用类晨考day15
java
骇客野人1 小时前
【JAVA】JAVA接口公共返回体ResponseData封装
java·开发语言
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
向宇it2 小时前
【从零开始入门unity游戏开发之——C#篇25】C#面向对象动态多态——virtual、override 和 base 关键字、抽象类和抽象方法
java·开发语言·unity·c#·游戏引擎
Lojarro3 小时前
【Spring】Spring框架之-AOP
java·mysql·spring