Java - JUC核心类AbstractQueuedSynchronizer(AQS)底层实现


一.  AQS内部结构介绍

JUC是Java中一个包   java.util.concurrent 。在这个包下,基本存放了Java中一些有关并发的类,包括并发工具,并发集合,锁等。

AQS(抽象队列同步器)是JUC下的一个基础类,大多数的并发工具都是基于AQS实现的。

AQS本质并没有实现太多的业务功能,只是对外提供了三点核心内容,来帮助实现其他的并发内容。

三点核心内容:

  • int state

    • 比如ReentrantLock或者ReentrantReadWriteLock, 它们获取锁的方式,都是对state变量做修改实现的。
    • 比如CountDownLatch基于state作为计数器,同样的Semaphore也是用state记录资源个数。
  • Node对象组成的双向链表(AQS中)
    • 比如ReentrantLock,有一个线程没有拿到锁资源,当线程需要等待,则需要将线程封装为Node对象,将Node添加到双向链表,将线程挂起,等待即可。
  • Node对象组成的单向链表(AQS中的ConditionObject类中)
    • 比如ReentrantLock,一个线程持有锁资源时,执行了await方法(类比synchronized锁执行对象的wait方法),此时这个线程需要封装为Node对象,并添加到单向链表。

二.  Lock锁和AQS关系

ReentrantLock就是基于AQS实现的。ReentrantLock类中维护这个一个内部抽象类Sync,他继承了AQS类。ReentrantLock的lock和unlock方法就是调用的Sync的方法。

AQS流程(简述)
1. 当new了一个ReentrantLock时,AQS默认state值为0, head 和 tail 都为null;
2. A线程执行lock方法,获取锁资源。
3. A线程将state通过cas操作从0改为1,代表获取锁资源成功。
4. B线程要获取锁资源时,锁资源被A线程持有。
5. B线程获取锁资源失败,需要添加到双向链表中排队。
6. 挂起B线程,等待A线程释放锁资源,再唤醒挂起的B线程。
7. A线程释放锁资源,将state从1改为0,再唤醒head.next节点。
8. B线程就可以重新尝试获取锁资源。
注: 修改AQS双向链表时要保证一个私有属性变化和两个共有属性变化,只需要让tail变化保证原子性即可。不能先改tail(会破坏双向链表)

三.  AQS - Lock锁的tryAcquire方法

ReentrantLock中的lock方法实际是执行的Sync的lock方法。

Sync是一个抽象类,继承了AQS

Sync有两个子类实现:

  • FairSync: 公平锁
  • NonFairSync: 非公平锁

Sync的lock方法实现:

//  非公平锁
final void lock() {
// CAS操作,尝试将state从0改为1
// 成功就拿到锁资源, 失败执行acquire方法
if (compareAndSetState(0, 1))
     // 
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
} // 公平锁
final void lock() {
acquire(1);
}

如果CAS操作没有成功,需要执行acquire方法走后续

acquire方法是AQS提供的,公平和非公平都是走的这个方法

public final void acquire(int arg) {
// 1. tryAcquire方法: 再次尝试拿锁
// 2. addWaiter方法: 没有获取到锁资源,去排队
// 3. acquireQueued方法:挂起线程和后续被唤醒继续获取锁资源的逻辑
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
     // 如果这个过程中出现中断,在整个过程结束后再自我中断 
selfInterrupt();
}

在AQS中tryAcquire是没有具体实现逻辑的,AQS直接在tryAcquire方法中抛出异常

在公平锁和非公平锁中有自己的实现。

  • 非公平锁tryAcquire方法
//  非公平锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
} // 非公平锁再次尝试拿锁 (注:该方法属于Sync类中)
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程对象
final Thread current = Thread.currentThread();
// 获取state状态
int c = getState();
// state是不是没有线程持有锁资源,可以尝试获取锁
if (c == 0) {
// 再次CAS操作尝试修改state状态从0改为1
if (compareAndSetState(0, acquires)) {
// 成功就设置互斥锁的为当前线程拥有
setExclusiveOwnerThread(current);
return true;
}
}
// 锁资源是否被当前线程所持有 (可重入锁)
else if (current == getExclusiveOwnerThread()) {
// 持有锁资源为当前, 则对state + 1
int nextc = c + acquires;
// 健壮性判断
if (nextc < 0) // overflow
// 超过最大锁重入次数会抛异常(几率很小,理论上存在)
throw new Error("Maximum lock count exceeded");
// 设置state状态,代表锁重入成功
setState(nextc);
return true;
}
return false;
}
  • 公平锁tryAcquire方法
//  公平锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程对象
final Thread current = Thread.currentThread();
// 获取state状态
int c = getState();
// state是不是没有线程持有锁资源
if (c == 0) {
// 当前锁资源没有被其他线程持有
// hasQueuedPredecessors方法: 锁资源没有被持有,进入队列排队
// 排队规则:
// 1. 检查队列没有线程排队,抢锁。
// 2. 检查队列有线程排队,查看当前线程是否排在第一位,如果是抢锁,否则入队列(注:该方法只是判断,没有真正入队列)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 再次CAS操作尝试, 成功就设置互斥锁的为当前线程拥有
setExclusiveOwnerThread(current);
return true;
}
}
// 锁资源是否被当前线程所持有 (可重入锁)
else if (current == getExclusiveOwnerThread()) {
// 持有锁资源为当前, 则对state + 1
int nextc = c + acquires;
// 健壮性判断
if (nextc < 0)
// 超过最大锁重入次数会抛异常(几率很小,理论上存在)
throw new Error("Maximum lock count exceeded");
// 设置state状态,代表锁重入成功
setState(nextc);
return true;
}
return false;
}

四.  AQS的addWaiter方法

addWaiter方法,就是将当前线程封装为Node对象,并且插入到AQS的双向链表。

//  线程入队列排队
private Node addWaiter(Node mode) {
// 将当前对象封装为Node对象
// Node.EXCLUSIVE 表示互斥 Node.SHARED 表示共享
Node node = new Node(Thread.currentThread(), mode);
// 获取tail节点
Node pred = tail;
// 判断双向链表队列有没有初始化
if (pred != null) {
// 将当前线程封装的Node节点prev属性指向tail尾节点
node.prev = pred;
// 通过CAS操作设置当前线程封装的Node节点为尾节点
if (compareAndSetTail(pred, node)) {
// 成功则将上一个尾节点的next属性指向当前线程封装的Node节点
pred.next = node;
return node;
}
}
// 没有初始化head 和 tail 都等于null
// enq方法: 插入双向链表和初始化双向链表
enq(node);
// 完成节点插入
return node;
} // 插入双向链表和初始化双向链表
private Node enq(final Node node) {
// 死循环
for (;;) {
// 获取当前tail节点
Node t = tail;
// 判断尾节点是否初始
if (t == null) { // Must initialize
// 通过CAS操作初始化初始化一个虚拟的Node节点,赋给head节点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 完成当前线程Node节点加入AQS双向链表的过程
// 当前线程封装的Node的上一个prev属性指向tail节点
// 流程: 1. prev(私有) ---> 2. tail(共有) ---> 3. next (共有)
node.prev = t;
// 通过CAS操作修改tail尾节点指向当前线程封装的Node
if (compareAndSetTail(t, node)) {
// 将当前线程封装的Node节点赋给上一个Node的下一个next属性
t.next = node;
return t;
}
}
}
}

五.  AQS的acquireQueued方法

acquireQueued方法主要就是线程挂起以及重新尝试获取锁资源的地方

重新获取锁资源主要有两种情况:

  • 上来就排在head.next,就回去尝试拿锁
  • 唤醒之后尝试拿锁
//  当前线程Node添加到AQS队列后续操作
final boolean acquireQueued(final Node node, int arg) {
// 标记,记录拿锁状态 失败
boolean failed = true;
try {
// 中断状态
boolean interrupted = false;
// 死循环
for (;;) {
// 获取当前节点的上一个节点 prev
final Node p = node.predecessor();
// 判断当前节点是否是head,是则代表当前节点排在第一位
// 如果是第一位,执行tryAcquire方法尝试拿锁
if (p == head && tryAcquire(arg)) {
// 都成功,代表拿到锁资源
// 将当前线程Node设置为head节点,同时将Node的thread 和 prev属性设置为null
setHead(node);
// 将上一个head的next属性设置为null,等待GC回收
p.next = null; // help GC
// 拿锁状态 成功
failed = false;
// 返回中断状态
return interrupted;
}
// 没有获取到锁 --- 尝试挂起线程
// shouldParkAfterFailedAcquire方法: 挂起线程前的准备
// parkAndCheckInterrupt方法: 挂起当前线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 设置中断线程状态
interrupted = true;
}
} finally {
// 取消节点
if (failed)
cancelAcquire(node);
}
} // 检查并更新无法获取锁节点的状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取上一个节点的ws状态
/**
* SIGNAL(-1) 表示当前节点释放锁的时候,需要唤醒下一个节点。或者说后继节点在等待当前节点唤醒,后继节点入队时候,会将前驱节点更新给signal。
* CANCELLED(1) 表示当前节点已取消调度。当timeout或者中断情况下,会触发变更为此状态,进入该状态后的节点不再变化。
* CONDITION(-2) 当其他线程调用了condition的signal方法后,condition状态的节点会从等待队列转移到同步队列中,等待获取同步锁。
* PROPAGATE(-3) 表示共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能唤醒后继的后继节点。
* 默认(0) 新节点入队时候的默认状态。
*/
int ws = pred.waitStatus;
// 判断上个节点ws状态是否是 -1, 是则挂起
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
/**
* 判断上个节点是否是取消或者其他状态。
* 向前找到不是取消状态的节点,修改ws状态。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,
* 稍后就会被GC回收,这个操作实际是把队列中的cancelled节点剔除掉。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果前驱节点正常,那就把上一个节点的状态通过CAS的方式设置成-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
} // 挂起当前线程
private final boolean parkAndCheckInterrupt() {
// 挂起当前线程
LockSupport.park(this);
// 返回中断标志
return Thread.interrupted();
}

六.  AQS的Lock锁的release方法

//  互斥锁模式   解锁
public final boolean release(int arg) {
// 尝试是否可以解锁
if (tryRelease(arg)) {
Node h = head;
// 判断双链表是否存在线程排队
if (h != null && h.waitStatus != 0)
// 唤醒后续线程
unparkSuccessor(h);
return true;
}
return false;
} // 尝试是否可以解锁
protected final boolean tryRelease(int releases) {
// 锁状态 = 状态 - 1
int c = getState() - releases;
// 判断锁是是否是当前线程持有
if (Thread.currentThread() != getExclusiveOwnerThread())
// 当前线程没有持有抛出异常
throw new IllegalMonitorStateException();
boolean free = false;
// 当前锁状态变为0,则清空锁归属线程
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置锁状态为0
setState(c);
return free;
} // 唤醒线程
private void unparkSuccessor(Node node) {
// 获取头节点的状态
int ws = node.waitStatus;
if (ws < 0)
// 通过CAS将头节点的状态设置为初始状态
compareAndSetWaitStatus(node, ws, 0);
// 后继节点
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始往前遍历,寻找离头节点最近的等待状态正常的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 真正的唤醒操作
LockSupport.unpark(s.thread);
}

以上仅供参考!!

Java - ReentrantLock锁分析的更多相关文章

  1. Java 重入锁 ReentrantLock 原理分析

    1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似.所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生 ...

  2. JAVA源码分析------锁(1)

    http://870604904.iteye.com/blog/2258604 第一次写博客,也就是记录一些自己对于JAVA的一些理解,不足之处,请大家指出,一起探讨. 这篇博文我打算说一下JAVA中 ...

  3. ReentrantLock 锁释放源码分析

    ReentrantLock 锁释放源码分析: 调用的是unlock 的方法: public void unlock() { sync.release(1); } 接下来分析release() 方法: ...

  4. Java中锁的实现与内存语义

    目录 1. 概述 2. 锁的内存语义 3. 锁内存语义的实现 4. 总结 1. 概述 锁在实际使用时只是明白锁限制了并发访问, 但是锁是如何实现并发访问的, 同学们可能不太清楚, 下面这篇文章就来揭开 ...

  5. Java多线程--锁的优化

    Java多线程--锁的优化 提高锁的性能 减少锁的持有时间 一个线程如果持有锁太长时间,其他线程就必须等待相应的时间,如果有多个线程都在等待该资源,整体性能必然下降.所有有必要减少单个线程持有锁的时间 ...

  6. Java:锁笔记

    Java:锁笔记 本笔记是根据bilibili上 尚硅谷 的课程 Java大厂面试题第二季 而做的笔记 1. Java 锁之公平锁和非公平锁 公平锁 是指多个线程按照申请锁的顺序来获取锁,类似于排队买 ...

  7. Java线程问题分析定位

    Java线程问题分析定位 分析步骤: 1.使用top命令查看系统资源占用情况,发现Java进程占用大量CPU资源,PID为11572: 2.显示进程详细列表命令:ps -mp 11572 -o THR ...

  8. Java线程锁一个简单Lock

    /** * @author * * Lock 是java.util.concurrent.locks下提供的java线程锁,作用跟synchronized类似, * 单是比它更加面向对象,两个线程执行 ...

  9. 性能分析之-- JAVA Thread Dump 分析综述

    性能分析之-- JAVA Thread Dump 分析综述       一.Thread Dump介绍 1.1什么是Thread Dump? Thread Dump是非常有用的诊断Java应用问题的工 ...

  10. Java常用锁机制简介

    在开发Java多线程应用程序中,各个线程之间由于要共享资源,必须用到锁机制.Java提供了多种多线程锁机制的实现方式,常见的有synchronized.ReentrantLock.Semaphore. ...

随机推荐

  1. select_related一对一、多对一查询优化

    select_related一对一.多对一查询优化 Course.objects.all().select_related('teacher') 查询课程时顺带查出老师的信息

  2. idea 查看scala源代码

    使用idea编写spark程序,想要查看源代码,点进去是compiled code private[sql] def this(sc : org.apache.spark.SparkContext) ...

  3. 【实践篇】教你玩转JWT认证---从一个优惠券聊起

    引言 最近面试过程中,无意中跟候选人聊到了JWT相关的东西,也就联想到我自己关于JWT落地过的那些项目. 关于JWT,可以说是分布式系统下的一个利器,我在我的很多项目实践中,认证系统的第一选择都是JW ...

  4. sipp重放rtp数据测试FreeSWITCH

    环境:CentOS 7.6_x64 FreeSWITCH版本 :1.10.9 sipp版本:3.6.1 一.背景描述 sipp是一款VoIP测试工具,日常开发过程中会使用到该软件,但其自身携带的pca ...

  5. react中useRef的应用

    何为useRef useRef是随着react函数式组件发展而来的,是react众多官方hook中的一个,调用useRef可以返回一个伴随这组件整个声明周期不发生改变的对象,这个对象常见的用途有两个: ...

  6. 代码随想录算法训练营Day5 数组、链表复习

    数组部分 数组最重要的思维方式是双指针的使用. 快慢指针 在进行元素移除和元素操作时会使用两个for循环嵌套,此时时间复杂度为O(n²).在for循环中通过双指针(快慢指针)的使用可以使时间复杂度将为 ...

  7. ODOO13之六:Odoo 13开发之模型 – 结构化应用数据

    在本系列文章第三篇Odoo 13 开发之创建第一个 Odoo 应用中,我们概览了创建 Odoo 应用所需的所有组件.本文及接下来的一篇我们将深入到组成应用的每一层:模型层.视图层和业务逻辑层. 本文中 ...

  8. /etc/netplan/network-manager-all.yaml 配置服务器ip

    本文为博主原创,转载请注明出处: /etc/netplan 是用于配置 Ubuntu 系统网络接口的目录.在 Ubuntu 中,网络配置的默认工具为  Netplan,而 /etc/netplan 则 ...

  9. node版本问题:Error: error:0308010C:digital envelope routines::unsupported

    前言 出现这个错误是因为 node.js V17及以后版本中最近发布的OpenSSL3.0, 而OpenSSL3.0对允许算法和密钥大小增加了严格的限制,可能会对生态系统造成一些影响. 在node.j ...

  10. Enhancingdecisiontreeswithtransferlearningforsentimenta

    目录 1. 引言 2. 技术原理及概念 2.1 基本概念解释 2.2 技术原理介绍 2.3 相关技术比较 3. 实现步骤与流程 3.1 准备工作:环境配置与依赖安装 3.2 核心模块实现 3.3 集成 ...