引言

曾经有一道比较比较经典的面试题“你能够说说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的。

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

就要执行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加入对列。上述方法的一个执行流程图如下:

,接下来就是继续执行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();

}

解锁

加锁成功了,那锁用完了就应该释放锁了,释放锁重点看下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);

}

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

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

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

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

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

总结

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

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

结束

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

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

争时金融java_Java高并发编程基础之AQS相关推荐

  1. Java高并发编程基础之AQS

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

  2. 高并发编程基础(线程池基础)

    线程池简单基础介绍: Executor: Executor是Java工具类,执行提交给它的Runnable任务.该接口提供了一种基于任务运行机制的任务提交方法,包括线程使用详细信息,时序等等.Exec ...

  3. 高并发编程基础(java.util.concurrent包常见类基础)

    JDK5中添加了新的java.util.concurrent包,相对同步容器而言,并发容器通过一些机制改进了并发性能.因为同步容器将所有对容器状态的访问都串行化了,这样保证了线程的安全性,所以这种方法 ...

  4. Java 面试知识点解析(二)——高并发编程篇

    前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...

  5. 高并发编程系列:NIO、BIO、AIO的区别,及NIO的应用和框架选型

    谈到并发编程就不得不提到NIO,以及相关的Java NIO框架Netty等,并且在很多面试中也经常提到NIO和AIO.同步和异步.阻塞和非阻塞等的区别.我先简短介绍下几个NIO相关的概念,然后再谈NI ...

  6. libevent c++高并发网络编程_高并发编程学习(2)——线程通信详解

    前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...

  7. Java并发编程的艺术-Java并发编程基础

    第4章 Java并发编程基础 ​ Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势.线程作为操作系统调度的最小单元,多个线程能够同时执行,这将 ...

  8. java线程高并发编程

    java线程详解及高并发编程庖丁解牛 线程概述: 祖宗: 说起java高并发编程,就不得不提起一位老先生Doug Lea,这位老先生可不得了,看看百度百科对他的评价,一点也不为过: 如果IT的历史,是 ...

  9. 高并发编程学习(2)——线程通信详解

    前序文章 高并发编程学习(1)--并发基础 - https://www.wmyskxz.com/2019/11/26/gao-bing-fa-bian-cheng-xue-xi-1-bing-fa-j ...

最新文章

  1. 利用BH1750光度传感器测量一些发光体
  2. PHP 表单验证--安全性--小记
  3. c# 字符串排序 (面试题)
  4. 怎么快速掌握一门新技术
  5. 清北学堂(2019 4 28 ) part 1
  6. Adobe PhotoShop(PS) for Mac 如何隐藏切片框?
  7. 统计出每个班分别有男女生各多少名
  8. 安装maven过程并配置IDEA的全过程
  9. 静态VLAN及配置实例详解
  10. java speex转码_微信Speex转wav,Speex to wav
  11. ios kb转m_字节、kb、M怎么换算
  12. 十二、项目收尾(华为项目管理法-孙科炎读书摘要)
  13. 《C++ primer》学习笔记(第二章)——变量和基本类型
  14. Java实现视频通话
  15. 卸载xampp并重装mysql
  16. 网页版第三方登录操作——微信登录
  17. [渝粤教育] 西南科技大学 中国当代文学 在线考试复习资料
  18. 微信小程序图片转发到微信
  19. 1521端口已被占用解决方案
  20. 35岁了 软件测试我还可以做多久,往后我怎么发展

热门文章

  1. 这是“我”的故事 —— 董彬
  2. BeetleX.WebFamily针对Web SPA应用的改进
  3. 再记一次 应用服务器 CPU 暴高事故分析
  4. 聊一聊mongodb中的 explain 和 hint
  5. ASP.NET Core中的内存缓存
  6. .net core 中通过 PostConfigure 验证 Options 参数
  7. SonarQube系列一、Linux安装与部署
  8. 高性能微服务网关.NETCore客户端Kong.Net开源发布
  9. .Net资讯 | 一大波开发者福利来了, 一份微软官方Github上发布的开源项目清单等你签收...
  10. MongoDB发布4.0版本,支持ACID事务