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...

相关推荐
在努力的前端小白4 小时前
Spring Boot 敏感词过滤组件实现:基于DFA算法的高效敏感词检测与替换
java·数据库·spring boot·文本处理·敏感词过滤·dfa算法·组件开发
一叶飘零_sweeeet6 小时前
从繁琐到优雅:Java Lambda 表达式全解析与实战指南
java·lambda·java8
艾伦~耶格尔7 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试
一只叫煤球的猫7 小时前
🕰 一个案例带你彻底搞懂延迟双删
java·后端·面试
最初的↘那颗心7 小时前
Flink Stream API 源码走读 - print()
java·大数据·hadoop·flink·实时计算
JH30738 小时前
Maven的三种项目打包方式——pom,jar,war的区别
java·maven·jar
带刺的坐椅9 小时前
轻量级流程编排框架,Solon Flow v3.5.0 发布
java·solon·workflow·flow·solon-flow
David爱编程9 小时前
线程调度策略详解:时间片轮转 vs 优先级机制,面试常考!
java·后端
阿冲Runner10 小时前
创建一个生产可用的线程池
java·后端
写bug写bug10 小时前
你真的会用枚举吗
java·后端·设计模式