深入理解AQS之独占锁ReentrantLock

1. 管程 --- Java同步的设计思想

管程:指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。

  • 互斥:同一时刻只允许一个线程访问共享资源
  • 同步:线程之间如何通信、协作

在管程的发展史上,先后出现过三种不同的管程模型,分别是 Hasen 模型Hoare 模型MESA 模型 。现在正在广泛使用的是 MESA 模型

管程中引入了条件变量 的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

Java 中针对管程有两种实现:

实现方式 说明
基于 Object 的 Monitor 机制 用于 synchronized 内置锁的实现
抽象队列同步器 AQS 用于 JUC 包下 Lock 锁机制的实现

2. AQS原理分析

2.1 什么是AQS

java.util.concurrent 包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取 等,而这些行为的抽象就是基于 AbstractQueuedSynchronizer(简称 AQS)实现的。AQS 是一个抽象同步框架,可以用来实现一个依赖状态的同步器。

JDK 中提供的大多数的同步器如 Lock, Latch, Barrier 等,都是基于 AQS 框架来实现的。

AQS 具备的特性:

  • 阻塞等待队列
  • 共享/独占
  • 公平/非公平
  • 可重入
  • 允许中断

一般是通过一个内部类 Sync 继承 AQS,将同步器所有调用都映射到 Sync 对应的方法。


2.2 AQS核心结构

AQS 内部维护属性 volatile int state

java 复制代码
private volatile int state;  // 共享变量,使用volatile修饰保证线程可见性

State 三种访问方式:

java 复制代码
// 返回同步状态的当前值
protected final int getState() {
    return state;
}

// 设置同步状态的值
protected final void setState(int newState) {
    state = newState;
}

// 原子地(CAS操作)将同步状态值设置为给定值update
// 如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

定义了两种资源访问方式:

模式 说明 示例
Exclusive - 独占 只有一个线程能执行 ReentrantLock
Share - 共享 多个线程可以同时执行 Semaphore/CountDownLatch

AQS 实现时主要实现以下几种方法:

方法 说明
isHeldExclusively() 该线程是否正在独占资源。只有用到 condition 才需要去实现它
tryAcquire(int) 独占方式。尝试获取资源,成功则返回 true,失败则返回 false
tryRelease(int) 独占方式。尝试释放资源,成功则返回 true,失败则返回 false
tryAcquireShared(int) 共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
tryReleaseShared(int) 共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false

2.3 AQS定义两种队列

同步等待队列(CLH队列)

AQS 当中的同步等待队列也称 CLH 队列 ,是 Craig、Landin、Hagersten 三人发明的一种基于双向链表数据结构的队列,是 FIFO 先进先出线程等待队列。Java 中的 CLH 队列是原 CLH 队列的一个变种,线程由原自旋机制改为阻塞机制。

AQS 依赖 CLH 同步队列来完成同步状态的管理:

  • 当前线程如果获取同步状态失败时,AQS 则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到 CLH 同步队列,同时会阻塞当前线程
  • 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态

条件等待队列

条件队列是使用单向列表 保存的,用 nextWaiter 来连接:

  • 调用 await() 的时候会释放锁,然后线程会加入到条件队列
  • 调用 signal() 唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁

AQS 定义了 5 个队列中节点状态:

状态 说明
初始化 0 当前节点在 sync 队列中,等待着获取锁
CANCELLED 1 当前线程被取消
SIGNAL -1 当前节点的后继节点包含的线程需要运行(unpark)
CONDITION -2 当前节点在等待 condition,即在 condition 队列中
PROPAGATE -3 当前场景下后续的 acquireShared 能够得以执行

(图:AQS 同步等待队列与条件等待队列关系图)

思考:基于 AQS 如何设计一把独占锁?


2.4 基于AQS实现一把独占锁

java 复制代码
/**
 * @author wlliam
 * 基于AQS实现一把独占锁
 */
public class WlliamLock extends AbstractQueuedSynchronizer {

    @Override
    protected boolean tryAcquire(int unused) {
        // cas 加锁 state=0
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    @Override
    protected boolean tryRelease(int unused) {
        // 释放锁
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

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

    public boolean tryLock() {
        return tryAcquire(1);
    }

    public void unlock() {
        release(1);
    }

    public boolean isLocked() {
        return getState() != 0;
    }
}

3. ReentrantLock源码分析

3.1 ReentrantLock原理

ReentrantLock 是一种基于 AQS 框架的应用实现,是 JDK 中的一种线程并发访问的同步手段,它的功能类似于 synchronized,是一种互斥锁,可以保证线程安全。

ReentrantLock 基于 AQS + CAS 实现。

java 复制代码
public class ReentrantLockTest {
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        lock.lock();  // block until condition holds
        try {
            // ... method body
        } finally {
            lock.unlock();
        }
    }
}

lock() 流程

ReentrantLock基于抽象队列同步器AQS + CAS 实现的加锁、释放锁。ReentrantLock实现了公平锁、非公平锁,公平锁与非公平锁唯一的区别在于,非公平锁不会判断等待队列中是否节点等待获取锁,而是直接尝试获取锁,获取不到,再将当前线程节点添加进等待队列的尾节点,判断当前线程节点是否挂起。

unlock() 流程

ReentrantLock释放锁的流程较为简单,优先判断持有锁资源的线程是否为当前线程,若不为当前线程抛出异常;若为当前线程,AQS的state的属性值减1,再判断减1后的值是否为0,若为0表示当前线程彻底释放锁资源,唤醒等待队列中的挂起线程节点,开始抢占锁资源。

核心设计:

ReentrantLock 基于抽象队列同步器 AQS + CAS 实现的加锁、释放锁。ReentrantLock 实现了公平锁非公平锁,公平锁与非公平锁唯一的区别在于,非公平锁不会判断等待队列中是否有节点等待获取锁,而是直接尝试获取锁,获取不到,再将当前线程节点添加进等待队列的尾节点,判断当前线程节点是否挂起。

ReentrantLock 释放锁的流程较为简单,优先判断持有锁资源的线程是否为当前线程,若不为当前线程抛出异常;若为当前线程,AQS 的 state 的属性值减1,再判断减1后的值是否为0,若为0表示当前线程彻底释放锁资源,唤醒等待队列中的挂起线程节点,开始抢占锁资源。

类结构:

说明
Sync ReentrantLock 的抽象静态内部类,继承自 AQS。AQS 中用 volatile 修饰的 state 表示当前锁重入的次数
NonfairSync 静态内部类,继承 Sync,实现非公平锁
FairSync 静态内部类,继承 Sync,实现公平锁

3.2 ReentrantLock源码分析

构造函数

java 复制代码
private final Sync sync;

// 默认使用非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}

// fair=true,公平锁;否则,非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

lock() 加锁

java 复制代码
// 加锁
public void lock() {
    sync.lock();
}

公平锁 FairSync#lock():

java 复制代码
// 加锁
final void lock() {
    acquire(1);
}

非公平锁 NonfairSync#lock():

java 复制代码
// 加锁
final void lock() {
    // 获取锁资源,CAS 修改 AQS 的 state 属性值,获取成功,设置当前线程
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    // 获取失败,执行AQS的acquire
    else
        acquire(1);
}

公平锁直接调用 acquire(1),非公平锁先尝试 CAS 抢锁,失败才调用 acquire(1)


acquire()

java 复制代码
public final void acquire(int arg) {
    // 尝试获取锁资源
    if (!tryAcquire(arg) &&
        // 当前线程未获取到锁资源,加入等待队列,同时挂起线程,等待唤醒
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire()

公平锁 FairSync#tryAcquire():

java 复制代码
protected final boolean tryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取AQS的 state
    int c = getState();
    // state == 0 当前没有线程占用锁资源
    if (c == 0) {
        // 判断是否有线程在排队,若有线程在排队,返回true
        if (!hasQueuedPredecessors() &&
            // 尝试抢锁
            compareAndSetState(0, acquires)) {
            // 无线程排队,将线程属性设置为当前线程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // state != 0 有线程占用锁资源
    // 占用锁资源的线程是否为当前线程
    else if (current == getExclusiveOwnerThread()) {
        // state + 1
        int nextc = c + acquires;
        // 锁重入超出最大限制 (int的最大值),抛异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 将 state + 1 设置给 state
        setState(nextc);
        // 当前线程拿到锁资源,返回true
        return true;
    }
    return false;
}

非公平锁 NonFairSync#tryAcquire():

java 复制代码
protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

非公平锁 Sync#nonfairTryAcquire():

java 复制代码
final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取AQS的 state
    int c = getState();
    // 无线程占用锁资源
    if (c == 0) {
        // CAS 修改 state 的值,修改成功,设置线程属性为当前线程,返回占用锁资源标识
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 有线程占用锁资源
    // 占用锁资源的线程是当前线程(重入)
    else if (current == getExclusiveOwnerThread()) {
        // AQS 的 state + acquires
        int nextc = c + acquires;
        // 超出锁重入的上限(int的最大值),抛异常
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 将 state + acquires 设置到 state 属性
        setState(nextc);
        return true;
    }
    return false;
}

公平锁 vs 非公平锁 tryAcquire 核心区别:

  • 公平锁 :在 c == 0 时,先调用 hasQueuedPredecessors() 判断是否有线程在排队,有则放弃抢锁
  • 非公平锁 :在 c == 0 时,直接 CAS 抢锁,不判断是否有线程在排队

addWaiter()

为当前线程创建入队节点 AbstractQueuedSynchronizer$Node。入参 mode 表示锁类型,在 AQS 的静态内部类 Node 中有 SHAREEXCLUSIVE 两个属性。

等待队列不为空,将当前线程封装的 Node 节点添加进队列尾部;若等待队列为空,先初始化等待队列,然后再将 Node 节点添加进队列尾部。

java 复制代码
// 等待队列的尾节点,懒加载,只能通过enq方法添加节点
private transient volatile Node tail;

private Node addWaiter(Node mode) {
    // 当前线程、获取的锁类型封装为Node对象
    Node node = new Node(Thread.currentThread(), mode);
    // 获取等待队列的尾节点
    Node pred = tail;
    // 尾节点不为null
    if (pred != null) {
        // 将当前节点设置为等待队列的尾节点
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 等待队列为空,初始化等待队列节点信息
    enq(node);
    // 返回当前线程节点
    return node;
}

enq()

等待队列尾节点为空时,执行 enq() 方法初始化等待队列,并将 Node 节点添加进等待队列中。

java 复制代码
private Node enq(final Node node) {
    for (;;) {
        // 获取等待队列的尾节点
        Node t = tail;
        // 等待队列为空,初始化等待队列
        if (t == null) {
            // 初始化等待队列头尾节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 当前线程的Node添加到等待队列中
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued()

查看当前排队的 Node 是否是 head 的 next,如果是,尝试获取锁资源;如果不是或者获取锁资源失败,那么就尝试将当前 Node 的线程挂起(unsafe.park())。

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    // 获取锁资源标识
    boolean failed = true;
    try {
        boolean interrupted = false;
        // 自旋
        for (;;) {
            // 获取当前节点的前驱节点
            final Node p = node.predecessor();
            // 当前节点的前驱节点为头节点,并获取锁资源成功
            if (p == head && tryAcquire(arg)) {
                // 将当前节点设置到head - 头节点
                setHead(node);
                // 原头节点的下一节点指向设置为null,GC回收
                p.next = null;
                // 设置获取锁资源成功
                failed = false;
                // 不管线程GC
                return interrupted;
            }
            // 如果当前节点不是head的下一节点,获取锁资源失败,尝试将线程挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 线程挂起,UNSAFE.park()
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire()

shouldParkAfterFailedAcquire 检查并更新未成功获取锁资源的状态,返回 true 表示线程被挂起。

在挂起线程前,确认当前节点的上一个节点的状态:

  • 若为 1(CANCELLED),代表是取消的节点,不能挂起
  • 若为 -1(SIGNAL),代表后续节点中有挂起的线程,可以挂起
  • 若为 -2(CONDITION)-3(PROPAGATE),需要将状态改为 -1 之后,才能挂起当前线程
java 复制代码
// 获取锁资源失败,挂起线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 获取当前节点的上一个节点的状态
    int ws = pred.waitStatus;
    // 上一节点被挂起
    if (ws == Node.SIGNAL)
        // 返回true,挂起当前线程
        return true;
    if (ws > 0) {
        // 上一节点被取消,获取最近的线程挂起节点,
        // 并将当前节点的上一节点指向最近的线程挂起节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 最近线程挂起节点的下一节点指向当前节点
        pred.next = node;
    } else {
        // 上一节点状态小于等于0,存在线程处于等待状态,但未被挂起的场景
        // 通过CAS将处于等待的线程挂起,避免在挂起前节点获取到锁资源
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    // 返回false,不挂起当前线程
    return false;
}

Node 节点状态常量:

java 复制代码
static final class Node {
    // 线程被取消
    static final int CANCELLED =  1;
    // 等待队列中存在待被唤醒的挂起线程
    static final int SIGNAL    = -1;
    // 当前线程在Condition队列中,未在AQS队列中
    static final int CONDITION = -2;
    // 解决JDK1.5的BUG。共享锁在释放资源后,若头节点为0,无法确定真的没有后继节点
    // 如果头节点为0,需要将头节点的状态改为 -3,当最新拿到锁资源的线程查看
    // 是否有后继节点并且为当前锁为共享锁,需唤醒排队的线程。
    static final int PROPAGATE = -3;
}

unlock() 释放锁

java 复制代码
// 释放锁
public void unlock() {
    sync.release(1);
}

release()

java 复制代码
// 等待队列的头节点,懒加载,通过setHead方法初始化
private transient volatile Node head;

// 释放锁
public final boolean release(int arg) {
    // 当前线程释放锁资源的计数值
    if (tryRelease(arg)) {
        // 当前线程完全释放锁资源,获取等待队列头节点
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒等待队列中待唤醒的节点
            unparkSuccessor(h);
        // 完全释放锁资源
        return true;
    }
    // 当前线程未完全释放锁资源
    return false;
}

tryRelease()

java 复制代码
// 释放锁
protected final boolean tryRelease(int releases) {
    // 修改 AQS 的 state
    int c = getState() - releases;
    // 当前线程不是持有锁的线程,抛出异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否成功的将锁资源完全释放标识 (state == 0)
    boolean free = false;
    // 锁资源完全释放
    if (c == 0) {
        // 修改标识
        free = true;
        // 将占用锁资源的属性设置为null
        setExclusiveOwnerThread(null);
    }
    // state赋值
    setState(c);
    // 返回true表示当前线程完全释放锁资源;
    // 返回false表示当前线程有锁资源,持有计数值减少
    return free;
}

附:Condition 使用示例

java 复制代码
@Slf4j
public class ConditionDemo2 {

    private static final ReentrantLock lock = new ReentrantLock();
    private static final Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            log.debug("t1开始执行....");
            lock.lock();
            try {
                log.debug("t1获取锁....");
                // 让线程在condition上一直等待下去
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                log.debug("t1执行完成....");
            }
        }, "t1").start();

        new Thread(() -> {
            log.debug("t2开始执行....");
            lock.lock();
            try {
                log.debug("t2获取锁....");
                // 让线程在condition上一直等待下去
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                log.debug("t2执行完成....");
            }
        }, "t2").start();

        // 主线程两秒后执行
        Thread.sleep(2000);
        log.debug("准备获取锁,去唤醒 condition上阻塞的线程");
        lock.lock();
        try {
            // 唤醒condition上所有阻塞的线程
            condition.signalAll();
            log.debug("唤醒condition上阻塞的线程");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}
相关推荐
像我这样帅的人丶你还1 小时前
Java 后端详解(二):注解、参数绑定、评论与用户认证
后端
用户762352425911 小时前
理解 CAS & Atomic 原子操作类
后端
SimonKing1 小时前
铁子,IntelliJ IDEA 2026.1.3来了,升不升?
java·后端·程序员
铁皮饭盒2 小时前
@kognitivedev/rag, 用js做AI Agent开发
javascript·后端
IT_陈寒2 小时前
JavaScript的默认参数挖坑实录,我掉进去了
前端·人工智能·后端
陈明勇3 小时前
Go 1.26 新特性回顾:语言增强、工具升级与 Green Tea GC 默认启用
后端·go
咖啡八杯12 小时前
GoF设计模式——策略模式
java·后端·spring·设计模式
lizhongxuan13 小时前
AI Agent 上下文压缩利器 Headroom
后端
Csvn15 小时前
SSH 远程管理与安全加固 — 运维的守门之道
后端