AQS

AQS

AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个底层同步工具类,它是一个用来构建锁和同步器的框架。

AQS维护了一个共享资源状态(用volatile int State表示),并通过CAS操作完成对State值的修改。它使用一个先进先出(FIFO)的线程等待队列来完成资源获取的排队工作。当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。同时,Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。

AQS 总体结构图

AQS 中 维护了一个 volatile 修饰的 state 变量(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)通过 CAS来操作 state 保证在多线程情况下的安全性

java 复制代码
private transient volatile Node head;

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;

/**
 * The synchronization state.
 */
private volatile int state;

AQS 有两种队列,一种是阻塞队列,另一种是等待队列,等待队列都是通过双向链表来实现的,阻塞队列是通过单向链表来实现的,其中等待队列只有一个,阻塞队列可以有多个

需要注意的是:二者复用了同一种数据结构 Node

等待队列:非公平锁情况下线程首先尝试获取资源,失败后会进入等待队列队尾,给前继节点设置一个唤醒信号后,自身进入等待状态,直到被前继节点唤醒;公平锁的情况下,线程不会尝试获取资源,而是直接进入等待队列队尾

条件队列:是为 Condition 实现的一个同步器,一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。需要注意的是,如果一个线程被唤醒后,它会从条件队列转移到同步队列来等待获取锁,后面对条件队列进行源码分析时会再详细讲解。

在看源码之前,可以做一些猜测

  1. 为什么获取 Condition 时需要先获取锁?

需要将阻塞队列绑定到特定的锁当中?还是别的原因?

  1. 独占锁的实现方式是怎样的?

每次仅允许等待队列的队头去访问 state ,因此可以保证每次仅有一个线程操作 state 变量。那么是如何保证仅允许队头元素去访问 state 呢?是通过 CAS 吗?

ReentrantLock

由于 AQS 对锁的操作没有实现,因此我们通过分析 ReentrantLock 中的加锁代码

java 复制代码
// ReentrantLock.java 文件
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
      * Performs lock.  Try immediate barge, backing up to normal
      * acquire on failure.
      */
    final void lock() { 
        if (compareAndSetState(0, 1)) // 因为是非公平锁,所以在一开始的时候会先尝试获取锁,获取失败再与等待队列的线程进行争抢
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1); 
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}


static final class FairSync extends Sync {  // 公平锁
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1); 
    }

    /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&  // 判断是否有等待队列,如果有等待队列且队首元素不是当前线程,那么便不会进行锁的争抢
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) { // 如果锁的持有者是当前线程,则可以重复加锁并且由于已经加锁了不会再出现多线程情况,因此不需要多线程控制
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}


protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {  // 如果此时没有线程获取锁,那么当前线程可以再次尝试获取锁
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {  // 如果锁的持有者是当前线程,则可以重复加锁并且由于已经加锁了不会再出现多线程情况,因此不需要多线程控制
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;  // 获取锁失败
}
java 复制代码
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&  // FairSync 和 UnfairSync 有各自的实现
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败时,才会走到这,将其加入等待队列队尾,并设置唤醒标记给前一个节点
        selfInterrupt(); // 进入阻塞状态
}

整个加锁的逻辑

非公平锁:

  1. 尝试获取锁(没有线程加锁)
  2. 再次尝试获取锁(没有线程加锁)
  3. 重复加锁(加锁线程是当前线程)
  4. 加入等待队列,并将唤醒标记给等待队列的前一个节点
  5. 自身进入阻塞状态

公平锁:

  1. 尝试加锁(没有线程加锁,等待队列为空或等待队列对头线程为当前线程)
  2. 重复加锁(加锁线程是当前线程)

释放锁的逻辑:

  1. 尝试释放锁
  2. 释放锁成功后从队头元素开始往后唤醒阻塞的节点,如果后继节点被取消了,那么会从后往前开始唤醒

为什么会从后往前唤醒呢?

这是因为由于 next 指针的不可靠性所决定的,我们来看一下 enq 方法

java 复制代码
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)) { // 由于 CAS 的存在,只会存在一个线程执行完该方法,最终会导致 多个线程的 prev 指向了 t 。但是 t 的后驱只有一个
                t.next = node;
                return t;
            }
        }
    }
}

当处于第二种状态的时候,通过从后往前遍历可以找到线程 1 ,若从从前往后遍历会导致找不到线程 1

参考

AQS深入理解系列(一) 独占锁的获取过程_获取独占锁之前-CSDN博客

jianshu.com/p/a8d27ba5d...

相关推荐
青云交16 分钟前
Java 大视界 -- Java 大数据在智能医疗远程手术机器人操作数据记录与分析中的应用(342)
java·大数据·数据记录·远程手术机器人·基层医疗·跨院协作·弱网络适配
知北游天22 分钟前
Linux:多线程---同步&&生产者消费者模型
java·linux·网络
钢铁男儿32 分钟前
C#接口实现详解:从理论到实践,掌握面向对象编程的核心技巧
java·前端·c#
深栈解码35 分钟前
第二章:Class文件解剖:字节码的二进制密码
java·后端
TeamDev44 分钟前
从 JavaFX WebView 迁移至 JxBrowser
java·后端·webview
麦兜*44 分钟前
【SpringBoot 】Spring Boot OAuth2 六大安全隐患深度分析报告,包含渗透测试复现、漏洞原理、风险等级及完整修复方案
java·jvm·spring boot·后端·spring·系统架构
用户40315986396631 小时前
在工作中学算法——专线配置
java·算法
用户40315986396631 小时前
在工作中学算法——基于日志的系统故障预测
java·算法
浩瀚星辰20241 小时前
C++树状数组详解
java·数据结构·算法
h0l10w1 小时前
【Java】MongoDB
java·开发语言·mongodb