AQS之ReentrantLock

45 阅读7分钟

1、前置知识点

1.1、volatile

volatile 是 Java 中的一个关键字,用于修饰变量,主要解决多线程环境下的可见性和有序性问题。

1、volatile不能保证复合操作的原子性(如 i++) 2、过度是用volatile可能会影响性能,因为它阻止了缓存优化 3、volatile的可见性保障仅限于它修饰的变量本身,不适用于该遍历引用的对象内部字段 (简单概括)

1.1.1、主要特性

  • 可见性保障

    • 当一个线程修改了volatile变量的值,新值会立即被写入主内存
    • 其他线程读取该遍历时,会直接从主内存读取最新值,而不是是用本地缓存
  • 禁止指令重排序

    • volatile修饰的变量会禁止 JVM和吹起对其进行指令重排序优化
    • 确保代码的执行顺序与程序顺序是一致的

1.2、CAS

CAS 是并发编程中的一种无锁原子操作,全称为 Compare-And-Swap(比较并交换),它是现代 CPU 提供的一种原子指令,用于实现多线程同步。

在JVM层面,CAS操作最终会转换为CPU指令(如x86的cmpxchg指令),现代CPU都提供了类似的原子指令支持。 (简单概括)

  • 核心原理 三个操作数
    • 内存位置 V
    • 预期值 A
    • 新增 B
  • CAS的特点
    • 原子性:整个操作是不可分割的
    • 无锁不需要传统的锁机制
    • 乐观锁并发控制 先尝试更新,失败责重试
    • 自旋 spin:通常配合循环实现重试机制
  • ABA问题
    • 问题现象
      • 线程1读取遍历值为A,
      • 线程2将值从A改为B然后又改回A,
      • 线程1执行CAS操作时仍然认为值没有变化
    • 解决方法:
      • 使用版本号或时间戳
      • Java提供了AtomicMarkableReference 和 AtomicStampedReference

1.3、LockSupport

LockSupport 是 Java 并发包 (java.util.concurrent.locks) 中的一个工具类,提供了线程阻塞和唤醒的基本操作,它是构建更高级同步工具(如锁和同步器)的基础。 (简单概括)

  • 核心功能

    • 阻塞线程
      • park() 阻塞当前线程
      • parkNanos() 阻塞指定纳秒的时间
      • parkUntile() 阻塞制动指定的时间戳
    • 唤醒线程
      • unpark() 唤醒指定指定线程
  • 特点优势

    • 精准控制:可以指定要唤醒的特点线程

    • 无竞态天剑:unpark 可以先于 park调用

    • 底层实现:依赖于unsafe 类直接操作线程

2、ReentrantLock

2.1、核心方法

// 内部类 重要的类 实现了 AQS
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
    // lock 加锁方法
    final boolean nonfairTryAcquire(int acquires) {}
    // unlock最终调用的是 该方法
    protected final boolean tryRelease(int releases) {}
}
// 非公平锁实现类
static final class NonfairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {}
}
// 公平锁实现类
static final class FairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
}

// ReentrantLock 无惨构造方法 默认使用的是非公平锁
public ReentrantLock() {
   sync = new NonfairSync();
}
// ReentrantLock 有参构造方法,根据传参决定使用哪种锁
public ReentrantLock(boolean fair) {
   sync = fair ? new FairSync() : new NonfairSync();
}

//加锁方法
public void lock() {
   sync.acquire(1);
}

//释放锁方法
public void unlock() {
    sync.release(1);
}

ReentrantLock 的核心功能都委托给内部 Sync 类实现,而 Sync 继承自 AQS。它也就是AQS的具体实现

3、AbstractQueuedSynchronizer

3.1、核心变量&类&方法

// 链表中的节点
static final class Node{
}
// 链表头节点
private transient volatile Node head;
// 链表 尾结点
private transient volatile Node tail;
// 锁状态 锁的获取释放都是通过该状态来确定的
private volatile int state;

// 如果获取不到锁的线程通过此方法创建链表或者添加到链表最后
private Node addWaiter(Node mode) {}

// 链表添加成功后在该方法中会再次尝试获取一次,
// 如果还是获取不到责阻塞住当前线程,等待被唤醒
// 通过LockSupport.park(this);
final boolean acquireQueued(final Node node, int arg) {}

// 判断前驱节点的状态 来确定当前节点是阻塞,还是可以获取到锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

// 释放锁成功后,调用该方法来唤醒被阻塞的下一个节点
private void unparkSuccessor(Node node) {}

3.2、AQS中的Node

static final class Node {
    //标记节点为共享模式
    static final Node SHARED = new Node();
    //标记节点为独占模式
    static final Node EXCLUSIVE = null;
    
    //下面的4个常量是给waitStatus用的
    //当前节点被取消 这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点
    static final int CANCELLED =  1;
    //表示前继节点释放锁之后,需要对新节点进行唤醒操作
    //如果唤醒signal状态的后续节点,会将signal状态更新为0
    static final int SIGNAL    = -1;
    //当前节点进行等待队列中
    static final int CONDITION = -2;
    //表示下一次共享式同步状态获取将无条件传播下去
    static final int PROPAGATE = -3;
    
    /*
     * 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
     * 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
     * 即被一个线程修改后,状态会立马让其他线程可见。
     * 新增初始化的节点状态为:0
     */
    volatile int waitStatus;
    
    //当前节点的前驱节点
    volatile Node prev;
    //当前节点的后节点
    volatile Node next;
    //当前节点的线程
    volatile Thread thread;
}

3.2、AQS的核心思想

aqs的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么久需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制AQS是基于CLH锁(Craig Landin and Hagersten logcks)进一步优化实现。 AQS的CLH变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH变体队列和原本的CLH队列区别主要有两点

  • 自旋优化为【自旋+ 阻塞】:自旋操作的性能很高,但大量的自旋操作比较占用CPU资源,因此CLH变体队列中会先通过自旋尝试获取,如果失败再进行阻塞等待
  • 单向队列优化为【双向队列】:在CLH变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了next指针,成为了双向队列

AQS是用state变量表示同步状态,通过内置的FIFO线程等待/等待队列来完成获取资源线程的排队工作

3.3、JUC中AQS框架的具体实现

实现类使用和关联关系
ReentrantLock使用AQS保存锁重复持有的次数。当一个线程获取锁时ReentrantLock记录当前获得锁的线程标识
Semaphore使用AQS同步状态来保存信号量的当前计数
CountDownLatch使用AQS同步状态来表示计数,计数为0时,所有的Acquire操作才可以通过
ReentrantReadWriteLock使用AQS同步状态中的16位保存写锁持有的次数,剩下的16为用于保存读锁的持有次数
ThreadPoolExecutorworker利用AQS同步状态实现对独占线程变量的设置

4、以ReentrantLock加锁和解锁来进行画图理解

4.1、简易版加锁解锁流程

开始 加锁
│
├─ 尝试获取锁 (lock())
│   │
│   ├─ 如果锁未被占用 (state == 0)
│   │   ├─ CAS操作设置state为1
│   │   ├─ 设置当前线程为独占线程
│   │   └─ 获取锁成功 → 结束
│   │
│   ├─ 如果当前线程是独占线程 (重入)
│   │   ├─ state增加1
│   │   └─ 获取锁成功 → 结束
│   │
│   └─ 如果锁已被其他线程占用
│       ├─ 将当前线程加入等待队列
│       ├─ 阻塞当前线程
│       └─ 等待被唤醒
│
└─ 尝试非阻塞获取锁 (tryLock())
    │
    ├─ 如果锁可用 → 立即获取锁并返回true
    └─ 如果锁不可用 → 立即返回false
开始调用unlock() 解锁
│
├─ 调用sync.release(1) [AQS方法]
│   │
│   ├─ 尝试释放锁 tryRelease(1) [Sync实现]
│   │   │
│   │   ├─ 获取当前state值
│   │   ├─ 计算释放后的state: c = state - releases
│   │   │
│   │   ├─ 如果当前线程不是锁持有者 → 抛出IllegalMonitorStateException
│   │   │
│   │   ├─ 如果c == 0 (完全释放)
│   │   │   ├─ 设置独占线程为null
│   │   │   ├─ 设置state为0
│   │   │   └─ 返回true
│   │   │
│   │   └─ 如果c > 0 (部分释放,重入情况)
│   │       ├─ 设置state为c
│   │       └─ 返回false
│   │
│   ├─ 如果tryRelease返回true (完全释放)
│   │   ├─ 检查等待队列是否有等待线程
│   │   ├─ 如果有 → 唤醒后继节点线程
│   │   └─ 返回true
│   │
│   └─ 如果tryRelease返回false (部分释放)
│       └─ 返回false
│
└─ 流程结束

4、2流程图

image.png

image.png

image.png

image.png