ReentrantLock到AQS

新手学习,若有不对,欢迎大佬 调教

ReentrantLock

我们经常用的 *ReentrantLock*是干什么的呢 我认为这是一个前台/门面(类似设计模式中的门面模式)根据我们的入参创建一个FairSync OR NonfairSyncsync 担任锁的lock()和release()。

    private final Sync sync;

    public ReentrantLock() {
sync = new NonfairSync();
} public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

那有人可能就问了啥是公平锁(FairSync)? 啥是非公平锁(NonfairSync)?

就拿商场试吃举例子,前者就是大家都好好排队,后者是新来的看试吃小样还有,直接拿走不参与排队,那显然后面的人就会饥饿 啊。那非公平锁有什么意义呢。想象一下,当商场人满为患了,你去排到试吃的后面都要挤过来,挤过去。显然你在全局上影响了商场的客流动,如果你直接去 偷袭!(马保国音) 显然在商场全局上来说是最优的。

加锁

AQS入队

因为FairSyncNonfairSync 差的不是很大, 我们就着重讲NonfairSync

那你说那我缺的这块FairSync谁给我补啊,想要就自己来拿( 指自己看源码) 维吉尔音


//java.util.concurrent.locks.ReentrantLock
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L; final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

可见如果CAS成功线程就直接获得锁了,不成功就走了 acquire() 因为Sync extends AbstractQueuedSynchronizer让我们来看看acquire()

// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

tryAcquire() 获取锁失败进入AQS等待队列

AQS终于是露出鸡脚了acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

AQS(AbstractQueuedSynchronizer)抽象队列同步器,名字是不是很高大上,我们别管

就是商场老大爷、老大妈排队购物呢(先进先出的双向链表)。

让我们看看node具有的属性

    static final class Node {
// 共有锁?
static final Node SHARED = new Node();
// 独占锁?
static final Node EXCLUSIVE = null; // 线程被取消
static final int CANCELLED = 1;
// 线程处于激活态
static final int SIGNAL = -1;
// 线程在等待中
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;

让我们再看看addWaiter()

    private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 对AQS进行初始化再加入
return node;
}

enq() 对队列进行初始化,添加一个虚拟节点(避免空指针)

    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)) {
t.next = node;
return t;
}
}
}
}

AQS出队

让我们回到 acquire()

    public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

买菜大妈也挺急的,要排队就会催前面快点,于是拍拍前面的人,说往前催一下。(少数情况)前面的人也很急,看着时间来不及烧菜了,就自暴自弃,直接离开了,空出了位置。

    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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
} // 外部中断,或线程取消等待
} finally {
if (failed)
cancelAcquire(node);
}
}

后面的人看到前面有空位,就往前走再催前面的人。看到前面的人已经在催前面的人,他就不催了,催玩之后自己就能待机了(干着急也没用)。

为什么会 看到前面的人已经在催前面的人 可能有两个节点被同时加入

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)// 前面的人已经在问了
return true;
if (ws > 0) { // 取消节点,空出位置,往前挪
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

解锁

我们来看看锁的释放队列队列为空则调用unparkSuccessor(h) ,为什么 waitState以等于0做标记,且看下文

    public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0) // 检查AQS是否初始化,或队列是否为空
unparkSuccessor(h);
return true;
}
return false;
}

waitState等于0可简单看做,已经完成了他作为解锁信号的职责,同时这和 -1是不一样的,

-1 是未知的往前催(不知道前面好没好),0是肯定的说前面有一个空位,并且是head指针自发的,不会传递。

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 重置 waitStatus为 0
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 如果你观察到了这段的奇怪之处,我也没办法解释,看了文章也看到不是很明白,就不误导人了。相关内容在 java.util.concurrent.locks.AbstractQueuedSynchronizer#cancelAcquire
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒下一个线程
}

队列被 unpark() 唤醒,队伍可以向前移动了

如果觉得有帮到你

点个赞再走呗baby


参考文章:

不可不说的Java“锁”事

从ReentrantLock的实现看AQS的原理及应用

天天用lock,不好奇他到底怎么工作的吗 —ReentrantLock 大白话的更多相关文章

  1. 一文带你看懂Java中的Lock锁底层AQS到底是如何实现的

    前言 相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题.那你是不是很好奇,这些Lock锁api是如何实现的呢?本 ...

  2. DNS 到底怎么工作的? (How does dns work?)

    其实这个问题每次看的时候都觉得很明白,但是很久之后就忘记了,所以这次准备记录下来.深入到这个过程的各个细节之中,以后多看看. Step 1 请求缓存信息: 当你在开始访问一个 www.baidu.co ...

  3. CPU:网卡老哥,你到底怎么工作的?

    阿Q造访 我是一个网卡,居住在一个机箱内的主板上,负责整台计算机的网络通信,要是没有我,这里就成了一个信息孤岛了,那也太无聊了- 上个周末,服务器断电维护了,这是我难得的休息时间,我准备打个盹儿眯一会 ...

  4. C#基础之lock

    1.lock的本质 实现线程同步的第一种方式是我们经常使用的lock关键字,它将包围的语句块标记为临界区,这样一次只有一个线程进入临界区并执行代码.下面第一段的几行代码是关于lock关键字的使用方式, ...

  5. docker到底比LXC多了些什么

    看似docker主要的OS级虚拟化操作是借助LXC, AUFS只是锦上添花.那么肯定会有人好奇docker到底比LXC多了些什么.无意中发现 stackoverflow 上正好有人问这个问题, 回答者 ...

  6. 为什么Java有了synchronized之后还造了Lock锁这个轮子?

    众所周知,synchronized和Lock锁是java并发变成中两大利器,可以用来解决线程安全的问题.但是为什么Java有了synchronized之后还是提供了Lock接口这个api,难道仅仅只是 ...

  7. Lock的实现之ReentrantLock详解

    摘要 Lock在硬件层面依赖CPU指令,完全由Java代码完成,底层利用LockSupport类和Unsafe类进行操作: 虽然锁有很多实现,但是都依赖AbstractQueuedSynchroniz ...

  8. Java并发包中Lock的实现原理

    1. Lock 的简介及使用 Lock是java 1.5中引入的线程同步工具,它主要用于多线程下共享资源的控制.本质上Lock仅仅是一个接口(位于源码包中的java\util\concurrent\l ...

  9. java并发:线程同步机制之Lock

    一.初识Lock Lock是一个接口,提供了无条件的.可轮询的.定时的.可中断的锁获取操作,所有加锁和解锁的方法都是显式的,其包路径是:java.util.concurrent.locks.Lock, ...

  10. 5.Lock接口及其实现ReentrantLock

    jdk1.7.0_79 在java.util.concurrent.locks这个包中定义了和synchronized不一样的锁,重入锁——ReentrantLock,读写锁——ReadWriteLo ...

随机推荐

  1. Ubuntu24使用Wine运行Windows程序安装微信

    Ubuntu24使用Wine运行Windows程序安装微信 2024.11.8:好消息!微信发布Linux版本了,微信主站Linux版本客户端下载页面:https://linux.weixin.qq. ...

  2. SQL Server 查看版本信息

    SQL Server 查看版本信息3种方法: 1) 使用命令行查看 [Win + R]键 -> 打开cmd 2) 使用SSMS查看 打开并连接SSMS后查看 3) 通过服务器属性查看 使用SSM ...

  3. gitee 删库跑路的正确打开方式

    前言 又是一个周一, 阳光一点都不明媚... 码云 gitee.com 五群 (QQ群号: 515965326) 又发生了一起删库跑路事件(手动滑稽). 手动部分截图 看图说话 为了更好的复现完整的流 ...

  4. Win10资源管理器导航窗格显示/隐藏项目-注册表

    一.隐藏快速访问 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer HubMode(类型:REG_DWORD) ...

  5. 在 .NET 中 使用 ANTLR4

    前言 本文将介绍如何在 .NET 中使用 ANTLR4 构建语法分析器.由于篇幅限制,本文不会深入讲解 ANTLR4 的语法规则,相关内容可参考 ANTLR4 的官方文档或其他资料.本文将涵盖以下内容 ...

  6. ESP32+Arduino入门(一):开发环境搭建

    前言 作为一名硬件小白,在动手组装了一下小智AI之后,感觉还挺有意思的,也想开始学习ESP32了. ESP32介绍 ESP32 是乐鑫科技(Espressif Systems)推出的一款高性能.低功耗 ...

  7. Sentinel源码—4.FlowSlot实现流控的原理

    大纲 1.FlowSlot根据流控规则对请求进行限流 2.FlowSlot实现流控规则的快速失败效果的原理 3.FlowSlot实现流控规则中排队等待效果的原理 4.FlowSlot实现流控规则中Wa ...

  8. MySQL 中如何解决深度分页的问题?

    MySQL 中如何解决深度分页的问题? 在 MySQL 中,深度分页是指查询数据时,用户请求的是数据集中的较后部分,而不是从头开始的数据.这类分页查询常见于有大量数据的系统中,当页数很大时,查询效率会 ...

  9. Golang从0到1实现简易版expired LRU cache带图解

    1.支持Put.Get的LRU实现 想要实现一个带过期时间的LRU,从易到难,我们需要先学会如何实现一个普通的LRU,做到O(1)的Get.Put. 想要做到O(1)的Get,我们很容易想到使用哈希表 ...

  10. python打包exe自定义图标

    1.生成.ico图标 https://www.aconvert.com/cn/icon/jpg-to-ico/ 2.打包 pyinstaller -F -w -i 666.ico pdfToword. ...