Java——详解ReentrantLock与AQS的关联以及AQS的数据结构和同步状态State

前言

Java中大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为 AQS)实现的。
AQS 是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
本文会先介绍应用层,再逐渐深入介绍原理层。通过介绍ReentrantLock 的基本特性和 ReentrantLock 与 AQS 的关联,来深入解读 AQS 相关独占锁的知识点。
本篇文章主要阐述 AQS 中独占锁的逻辑和 Sync Queue, 不包含共享锁和 Condition Queue 的部分(本篇文章核心为 AQS 原理剖析,只是简单介绍了 ReentrantLock,感兴趣同学可以阅读一下 ReentrantLock 的源码)。
下面列出本篇文章的大纲和思路,以便于大家更好地理解:
image.png

ReentrantLock

ReentrantLock 与synchronized的区别和使用

ReentrantLock 意思为可重入锁,指的是一个线程能够对一个临界资源重复加锁。为了帮助大家更好地理解 ReentrantLock 的特性,我们先将 ReentrantLock 跟常用的 Synchronized 进行比较,其特性如下(蓝色部分为本篇文章主要剖析的点):

ReentrantLockSynchronized
锁实现机制依赖AQS监视器模式(monitor)
灵活性支持响应中断、超时、尝试获取锁不灵活
锁释放方式必须显式调用unloc()自动释放
锁类型公平锁&非公平锁非公平锁
条件队列可关联多个条件队列关联一个
可重入性可重入可重入

ReentrantLock

public void test () throw Exception {
    // 1. 初始化选择公平锁、非公平锁
    ReentrantLock lock = new ReentrantLock(true);
    // 2. 可用于代码块
    lock.lock();
    try {
        try {
            // 3. 支持多种加锁方式,比较灵活 ; 具有可重入特性
            if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
        } finally {
            // 4. 手动释放锁
            lock.unlock()
        }
    } finally {
        lock.unlock();
    }
}

Synchronized

// **************************Synchronized 的使用方式
**************************
// 1. 用于代码块
synchronized (this) {}
// 2. 用于对象
synchronized (object) {}
// 3. 用于方法
public synchronized void test () {}
// 4. 可重入
for (int i = 0; i < 100; i++) {
 synchronized (this) {}
}
// **************************ReentrantLock 的使用方式
**************************

ReentrantLock 与 AQS 的关联

我们知道ReentrantLock 支持公平锁和非公平锁,并且 ReentrantLock的底层就是由 AQS 来实现的。那么 ReentrantLock 是如何利用AQS实现公平锁和非公平锁呢? 我们着重从这两者的加锁过程来理解一下它们与 AQS 之间的关系(加锁过程中与 AQS 的关联比较明显,解锁流程后续会介绍)
非公平锁源码中的加锁流程如下:

// java.util.concurrent.locks.ReentrantLock#NonfairSync
// 非公平锁
static final class NonfairSync extends Sync {
     ...
    final void lock() {
         if (compareAndSetState(0, 1))
         	setExclusiveOwnerThread(Thread.currentThread());
         else
         	acquire(1);
     }
     ...
}

上述代码解释如下:

  • 若通过 CAS 设置变量 State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
  • 若通过 CAS 设置变量 State(同步状态)失败,也就是获取锁失败,则进入Acquire 方法进行后续处理

但第二步获取锁失败后,后续的处理策略是怎么样的呢?这块可能会有以问题:

  1. 某个线程获取锁失败的后续流程是什么呢?有以下两种可能:
    1. 将当前线程获锁结果设置为失败,获取锁流程结束。

这种设计会极大降低系统的并发度,并不满足我们实际的需求。所以就需要下面这种流程,也就是AQS 框架的处理流程。

  1. 存在某种排队等候机制,线程继续等待,仍然保留获取锁的可能,获取锁流程仍在继续

  2. 对于问题 1 的第二种情况,既然说到了排队等候机制,那么就一定会有某种队列结构。

  • 这种队列是什么数据结构呢?
  • 处于排队等候机制中的线程,什么时候可以有机会获取锁呢?
  • 如果处于排队等候机制中的线程一直无法获取锁,还是需要一直等待吗,还是有别的策略来解决这一问题?

带着非公平锁的这些问题,再看下公平锁源码中获锁的方式:

// java.util.concurrent.locks.ReentrantLock#FairSync
static final class FairSync extends Sync {
    ... 
    final void lock() {
     	acquire(1);
    }
    ...
}

对这块代码,我们可能会存在这种疑问:Lock 函数通过 Acquire 方法进行加锁,但是具体是如何加锁的呢?
结合公平锁和非公平锁的加锁流程,虽然流程上有一定的不同,但是都调用了Acquire 方法,而 Acquire 方法是 FairSync 和 UnfairSync 的父类 AQS 中的核心方法

对于上边提到的问题,其实在 ReentrantLock 类源码中都无法解答,而这些问题的答案都在 Acquire 方法所在的类 AbstractQueuedSynchronizer 中,也就是本文的核心——AQS。下面我们会对 AQS 以及ReentrantLock 和 AQS 的关联做详细介绍。

AQS

首先,简单描述下 AQS 框架总的来说,AQS 框架共分为五层:
API层, 锁获取方法层,队列方法层,排队方法层,数据提供层
当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。
当自定义同步器进行加锁或者解锁操作时,先经过第一层的 API 进入 AQS 内部方法,然后经过第二层进行锁的获取,接着对于获取锁失败的流程,进入第三层和第四层的等待队列处理,而这些处理方式均依赖于第五层的基础数据提供层。

下面我们会从整体到细节,从流程到方法逐一剖析 AQS 框架,主要分析过程
如下:
image.png

原理概览

AQS 核心思想是:

  • 如果被请求的共享资源是空闲状态,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态
  • 如果共享资源被占用了,就需要一定的阻塞等待唤醒机制来保证锁分配

这个机制主要用的是 CLH 队列的变体实现的,将暂时获取不到锁的线程加入到队列中

CLH:Craig、Landin and Hagersten 队列,是单向链表,AQS 中的队列是CLH 变体的虚拟双向队列(FIFO)

AQS 是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

主要原理图如下:
image.png
AQS 使用一个** Volatile 的 int 类型的成员变量来表示同步状态**,通过内置的FIFO 队列来完成资源获取的排队工作,通过 CAS 完成对 State 值的修改

AQS 数据结构

这里先看下 AQS 中最基本的数据结构——Node,Node 即为上面 CLH 变体队列中的节点。
image.png

方法和属性值含义
waitStatus当前节点在队列中的状态
thread表示处于该节点的线程
prev前驱指针
predecessor返回前驱节点,没有的话抛出 npe
nextWaiter指向下一个处于 CONDITION 状态的节点(由于本文不讲述 Condition Queue 队列,这个里不多介绍)
next后继指针

线程获取/等待的锁的两种模式:

模式含义
SHARED共享锁:表示线程以共享的模式等待锁
EXCLUSIVE独占锁:表示线程正在以独占的方式等待锁

waitStatus 有下面几个枚举值

枚举含义
0当一个 Node 被初始化的时候的默认值
CANCELLED为 1,表示线程获取锁的请求已经取消了
CONDITION为 -2,表示节点在等待队列中,节点线程等待唤醒
PROPAGATE为 -3,当前线程处在 SHARED 情况下,该字段才会使用
SIGNAL为 -1,表示线程已经准备好了,就等资源释放了

同步状态 State

在了解数据结构后,接下来了解一下 AQS 的同步状态——State。
AQS 中维护了一个** state 的字段,意为同步状态,是由 Volatile 修饰的,用于展示当前临界资源的获锁情况**。

// java.util.concurrent.locks.AbstractQueuedSynchronizer
private volatile int state;

下面提供了几个访问这个字段的方法:

方法名描述
protected final int getState()获取 State 的值
protected final void setState(int newState)设置 State 的值
protected final boolean compareAndSetState(int expect, int update)使用 CAS 方式更新 State

这几个方法都是 Final 修饰的,说明子类中无法重写它们。
我们可以通过修改 State 字段表示的同步状态来实现多线程的独占模式和共享模式(加锁过程)

独占模式

共享模式

如果我们要自定义的同步工具,需要自定义获取同步状态和释放状态的方式,也就 是 AQS 架构图中的第一层:API 层

参考了这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值