争时金融java_Java高并发编程基础之AQS

引言

曾经有一道比较比较经典的面试题“你能够说说java的并发包下面有哪些常见的类?”大多数人应该都可以说出

CountDownLatch、CyclicBarrier、Sempahore多线程并发三大利器。这三大利器都是通过AbstractQueuedSynchronizer抽象类(下面简写AQS)来实现的,所以学习三大利器之前我们有必要先来学习下AQS。

AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架

AQS结构

说到同步我们如何来保证同步?大家第一印象肯定是加锁了,说到锁的话大家肯定首先会想到的是Synchronized。

Synchronized大家应该基本上都会使用,加锁和释放锁都是jvm 来帮我们实现的,我们只需要简单的加个 Synchronized关键字就可以了。

用起来超级方便。但是有没有一种情况我们设置一个锁的超时时间Synchronized就有点实现不了,这时候我们就可以用ReentrantLock来实现,ReentrantLock是通过aqs来实现的,今天我们就通过ReentrantLock来学习一下aqs。

CAS && 公平锁和非公平锁

AQS里面用到了大量的CAS学习AQS之前我们还是有必要简单的先了解下CAS、公平锁和非公平锁。

CAS

CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制。CAS 操作包含三个操作数 -- 内存位置、预期数值和新值。CAS 的实现逻辑是将内存位置处的数值与预期数值想比较,若相等,则将内存位置处的值替换为新值。若不相等,则不做任何操作,这个操作是个原子性操作,java里面的AtomicInteger等类都是通过cas来实现的。

公平锁和非公平锁

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,队列中第一个才能获得到锁。

优点:等待锁的线程不会饿死,每个线程都可以获取到锁。

缺点:整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

文字有点拗口,我们来个实际的例子说明下。比如我们去食堂就餐的时候都要排队,大家都按照先来后到的顺序排队打饭,这就是公平锁。如果等到你准备拿盘子打饭的时候

直接蹦出了一个五大三粗的胖子插队到你前面,你看打不赢他只能忍气吞声让他插队,等胖子打完饭了又来个小个子也来插你队,这时候你没法忍了,直接大吼一声让他滚,这个

小个子只能屁颠屁颠到队尾去排队了这就是非公平锁。

我们先来看看AQS有哪些属性

// 头结点

private transient volatile Node head;

// 阻塞的尾节点,每个新的节点进来,都插入到最后,也就形成了一个链表

private transient volatile Node tail;

// 这个是最重要的,代表当前锁的状态,0代表没有被占用,大于 0 代表有线程持有当前锁

// 这个值可以大于 1,是因为锁可以重入,每次重入都加上 1

private volatile int state;

// 代表当前持有独占锁的线程,举个最重要的使用例子,因为锁可以重入

// reentrantLock.lock()可以嵌套调用多次,所以每次用这个来判断当前线程是否已经拥有了锁

// if (currentThread == getExclusiveOwnerThread()) {state++}

private transient Thread exclusiveOwnerThread; //继承自AbstractOwnableSynchronizer

下面我们来写一个demo分析下lock 加锁和释放锁的过程

```java

final void lock() {

// 上来先试试直接把状态置位1,如果此时没人获取锁就直接

if (compareAndSetState(0, 1))

// 争抢成功则修改获得锁状态的线程

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

cas尝试失败,说明已经有人再持有锁,所以进入acquire方法

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

tryAcquire方法,看名字大概能猜出什么意思,就是试一试。

tryAcquire实际上是调用了父类Sync的nonfairTryAcquire方法

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

// 获取下当前锁的状态

int c = getState();

// 这个if 逻辑跟前面一进来就获取锁的逻辑一样都是通过cas尝试获取下锁

if (c == 0) {

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

// 进入这个判断说明 锁重入了 状态需要进行+1

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

// 如果锁的重入次数大于int的最大值,直接就抛出异常了,正常情况应该不存在这种情况,不过jdk还是严谨的

if (nextc < 0) // overflow

throw new Error("Maximum lock count exceeded");

setState(nextc);

return true;

}

// 返回false 说明尝试获取锁失败了,失败了就要进行acquireQueued方法了

return false;

}

tryAcquire方法如果获取锁失败了,那么肯定就要排队等待获取锁。排队的线程需要待在哪里等待获取锁?这个就跟我们线程池执行任务一样,线程池把任务都封装成一个work,然后当线程处理任务不过来的时候,就把任务放到队列里面。AQS同样也是类似的,把排队等待获取锁的线程封装成一个NODE。然后再把NODE放入到一个队列里面。队列如下所示,不过需要注意一点head是不存NODE的。

f3516da1f4ef32a5a59ecac5f1f70eab.png

接下来我们继续分析源码,看下获取锁失败是如何被加入队列的。

就要执行acquireQueued方法,执行acquireQueued方法之前需要先执行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;

// cas 加入队列队尾

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

// 尾结点不为空 || cas 加入尾结点失败

enq(node);

return node;

}

enq

接下来再看看enq方法

// 通过自旋和CAS一定要当前node加入队尾

private Node enq(final Node node) {

for (;;) {

Node t = tail;

// 尾结点为空说明队列还是空的,还没有被初始化,所以初始化头结点,可以看到头结点的node 是没有绑定线程的也就是不存数据的

if (t == null) { // Must initialize

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

通过addWaiter方法已经把获取锁的线程通过封装成一个NODE加入对列。上述方法的一个执行流程图如下:

8b9c2c72436a5a41deff3619870e6279.png

,接下来就是继续执行acquireQueued方法

acquireQueued

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

for (;;) {

// 通过自旋去获取锁 前驱节点==head的时候去尝试获取锁,这个方法在前面已经分析过了。

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {

setHead(node);

p.next = null; // help GC

failed = false;

return interrupted;

}

// 进入这个if说明node的前驱节点不等于head 或者尝试获取锁失败了

// 判断是否需要挂起当前线程

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

// 异常情况进入cancelAcquire,在jdk11的时候这个源码直接是catch (Throwable e){ cancelAcquire(node);} 简单明了

if (failed)

cancelAcquire(node);

}

}

setHead

这个方法每当有一个node获取到锁了,就把当前node节点设置为头节点,可以简单的看做当前节点获取到锁了就把当前节点”移除“(变为头结点)队列。

shouldParkAfterFailedAcquire

说到这个方法我们就要先看下NODE可能会有哪些状态在源码里面我们可以看到总共会有四种状态

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

// 前驱节点状态 如果这个状态为-1 则返回true,把当前线程挂起

if (ws == Node.SIGNAL)

return true;

// 大于0,说明状态为CANCELLED

if (ws > 0) {

do {

// 删除被取消的node(让被取消的node成为一个没有引用的node等着下次GC被回收)

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

} else {

// 进入这里只能是 0,-2,-3。NODE节点初始化的时候waitStatus默认值是0,所以只有这里才有修改waitStatus的地方

// 通过cas 把前驱节点的状态设置为-1,然后返回false ,外面调用这个方法的是个循环,又会调用一次这个方法

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

parkAndCheckInterrupt

挂起当前线程,并且阻塞

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this); // 挂起当前线程,阻塞

return Thread.interrupted();

}

9937e14fc0851f50a6217acc285dca06.png

解锁

加锁成功了,那锁用完了就应该释放锁了,释放锁重点看下unparkSuccessor这个方法就好了

private void unparkSuccessor(Node node) {

// 头结点状态

int ws = node.waitStatus;

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

Node s = node.next;

// s==null head的successor节点获取锁成功后,执行了head.next=null的操作后,解锁线程读取了head.next,因此s==null

// head的successor节点被取消(cancelAcquire)时,执行了如下操作:successor.waitStatus=1 ; successor.next = successor;

if (s == null || s.waitStatus > 0) {

s = null;

// 从尾节点开始往前找,找到最前面的非取消的节点 这里没有break 哦

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

// 唤醒线程 ,唤醒的线程会从acquireQueued去获取锁

LockSupport.unpark(s.thread);

}

释放锁代码比较简单,基本都写在代码注释里面了,流程如下:

76190dd9bc00c126b92670cc7d412dd1.png

这段代码里面有一个比较经典的面试题:

如果头结点的下一个节点为空或者头结点的下一个节点的状态为取消的时候为什么要从后往前找,找到最前面非取消的节点?

node.prev = pred; compareAndSetTail(pred, node) 这两个地方可以看作Tail入队的原子操作,但是此时pred.next = node;还没执行,如果这个时候执行了unparkSuccessor方法,就没办法从前往后找了,所以需要从后往前找。

在产生CANCELLED状态节点的时候,先断开的是Next指针,Prev指针并未断开,因此也是必须要从后往前遍历才能够遍历完全部的Node

总结

reentrantLock的获取锁和释放锁基本就讲完了,里面还涉及多比较多的细节,感兴趣的同学可以对着源码一行一行去debug试试。

适当的了解aqs才能更好的学习CountDownLatch、CyclicBarrier、Sempahore,因为这三个利器都是基于aqs来实现的。

结束

由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。

如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/289807.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android UI设计系统-android selector 开始自定义样式

Selector的结构描述&#xff1a; <?xml version"1.0" encoding"utf-8"?> <selector xmlns:android"http://schemas.android.com/apk/res/android" > <item android:color"hex_color" android:state_pressed"tru…

Xamarin效果第十三篇之弹窗PopupPage

在上一篇文章中使用Xamarin实现控制了一下祖传的PLC;基本的功能也就完事了,这不总觉得少点最基本的配置;那就趁着激情还在赶紧再去完善一下,不然激情已过就懒得去摸索了;来看看最终咱实现的视频效果:1、关于弹窗,直接使用开源的PopupPageInstall-Package Rg.Plugins.Popup -Ver…

UIView中常用的方法

UIView 中常见的方法总结 addSubview: 添加一个子视图到接收者并让它在最上面显示出来。 - (void)addSubview:(UIView *)view 讨论 这方法同样设置了接收者为下一个视图响应对象。接收者保留视图。如果你使用removeFromSuperview方法用来把视图移除他的显示列表&#xff0c;那么…

Android studio之如何快速查看页面的布局

Android studio  &#xff0d;&#xff1e; &#xff54;&#xff4f;&#xff4f;&#xff4c;&#xff53; &#xff0d;&#xff1e; &#xff21;&#xff4e;&#xff44;&#xff52;&#xff4f;&#xff49;&#xff44; &#xff0d;&#xff1e; &#xff2c;&…

你们是不是对QQ总在后台莫名其妙更新一些东西很反感

开始 - 运行 - gpedit.msc 打开策略组依次进入 计算机配置——Windows设置——安全设置——软件限制策略——其他规则有人说软件限制策略下面是空的 你在软件限制策略上右键——新建 下面的目录自然就出来了右键——新建路径规则 依次输入如下路径并选择不允许<系统路径>…

GTKmm 学习资料

GTK2.0 中的容器控件与布局技巧 GTK图形化应用程序开发学习笔记&#xff08;一&#xff09;—概述

Android之在linux终端执行shell脚本文件(通过aapt)得到apk包名

1、问题 我们在ubuntu上经常想看到apk的包名,然后在终端通过pidcat.py packageName 过滤日志,我们常用的办法手机连接电脑,然后打开这个app adb shell dumpsys activity top 我们可以看到当前activity的名字和包名,但是如果这是一个apk呢?还没有运行呢?我们可以通过工…

C# 线程问题之争用条件

用多个线程编程并不容易。在启动访问相同数据的多个线程时&#xff0c;会间歇性地遇到难以发现的问题。如果使用任务、并行 LINQ 或 Parallel 类&#xff0c;也会遇到这些问题。为了避免这些问题&#xff0c;必须特别注意同步问题和多个线程可能发生的其他问题。下面探讨与线程…

BFS HDOJ 2102 A计划

题目传送门 题意:中文题面 分析:双层BFS,之前写过类似的题.总结坑点: 1.步数小于等于T都是YES  2. 传送门的另一侧还是传送门或者墙都会死  3. 走到传送门也需要一步 #include <bits/stdc.h> using namespace std;char maze[2][11][11]; int dx[4] {-1, 1, 0, 0}; i…

MVC 之 Partial View 用法

Partial View 顾名思义就是Html代码片段&#xff0c;因此可以用Partial View 把部分的Html或显示逻辑包装起来&#xff0c;方便多次使用。Partial View 需要放在Views/Shared 目录下&#xff0c;任何Controlller 下的Action 或 View 都可以载入。如何载入Partial View?MVC 的 …

Matlab 7.1安装及打不开问题解决

一、安装方法 1、解压[MATLAB.V7.1.Windows版本].MATLAB.V7.1.R14.SP3.CD1.iso,双击setup进行安装,输入用户名,单位,找到crack下的PLP number.txt文件夹的PLP序列,复制粘贴。 2、安装过程中选择[MATLAB.V7.1.Windows版本].MATLAB.V7.1.R14.SP3.CD2和[MATLAB.V7.1.…

Android之在linux终端执行shell脚本直接打印当前运行app的日志

1、问题 我们一般很多时候会需要在ubuntu终端上打印当前运行app的日志,我们一般常见的做法是 1)、获取包名 打开当前运行的app,然后输入如下命令,然后在第一行TASK后面的就可以看到包名 adb shell dumpsys activity top 2)、我们的终端安装了pidcat.py脚本,然后执行如下…

尾调用优化 java_为什么JVM仍然不支持尾调用优化?

拉丁的传说也许您已经知道这一点&#xff0c;但是这个功能并不像听起来那么简单&#xff0c;因为Java语言实际上将堆栈跟踪暴露给程序员。考虑以下程序&#xff1a;public class Test {public static String f() {String s Math.random() > .5 ? f() : g();return s;}publ…

【AngularJS】—— 2 初识AngularJs(续)

前一篇了解了AngularJS的一些简单的使用&#xff0c;这里继续跟着w3c学习一下剩下的内容。 本篇根据w3cschool.cc继续学习AngularJS剩余的内容&#xff0c;包括&#xff1a; 1 事件 2 模块 3 表单 4 数据验证 5 bootstrap CSS风格 6 include包含其他页面 7 应用程序 8 参考手册…

08_drain a node on the swarm

在之前的小节&#xff0c;所有的节点的状态都是运行着的可用状态。swarm manager 可以分配任务给任意可用的节点。有时候&#xff0c;你可能需要对某台服务器进行维护&#xff0c;你需要配置某个节点为drain状态&#xff0c;即排干该节点上面的所有运行的容器。drain状态可以防…

特斯拉为何使用.NET 技术栈?

【精选转载】| 来源/知乎在知乎上有一个帖子非常热闹&#xff1a;“为何特使拉使用.net core技术栈 而不用 java&#xff1f;”1回答1&#xff1a;Kasim作者&#xff1a;Kasim链接&#xff1a;https://www.zhihu.com/question/496204534/answer/2269157872这题我熟啊&#xff0…

java之DocumentBuilderFactory解析xml

1、About documentBuilderFactory API description 1&#xff09;、 javax.xml.parsers 包DocumentBuilderFactory创建DOM模式的解析器对象, DocumentBuilderFactory是抽象工厂类&#xff0c;不能直接实例化&#xff0c;但是有newInstance方法 2&#xff09;、DocumentBuilderF…

java jdk实现快速排序_Java实现快速排序过程分析

快速排序过程没有既不浪费空间又可以快一点的排序算法呢&#xff1f;那就是“快速排序”&#xff01;光听这个名字是不是就觉得很高端呢。假设我们现在对“52 39 67 95 70 8 2552”这个8个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了&#xff0c…

深入理解计算机系统读书笔记

由于这本书的前半部分习题大多是相关计算和简单汇编代码编写&#xff0c;所以当时都是在稿纸上练习的&#xff0c;不过现在那些稿纸似乎也不见了: ( 所以现在仅有后半部分的课后习题代码以及示例练习代码&#xff08;家里作业习题当时并没有做&#xff0c;准备阅读第二遍时再做…

Blazor University (6)组件 — 组件事件

原文链接&#xff1a;https://blazor-university.com/components/component-events/组件事件源代码[1]EventCallback<T> 类是一个特殊的 Blazor 类&#xff0c;可以作为参数公开&#xff0c;以便组件可以在发生感兴趣的事情时轻松通知使用者。一旦声明了 EventCallback&l…