深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念
一、AQS框架简介
AQS诞生于Jdk1.5,在当时低效且功能单一的
synchronized
的年代,某种意义上讲,她拯救了Java
注:本系列文章所有测试用例均基于jdk1.8,操作系统为macOS
1.1、思考
我们去学习一个知识点或开启一个新课题时,最好是带着问题去学习,这样针对性比较强,且印象比较深刻,主动思考带给我们带来了无穷的好处
抛开AQS,设想以下问题:
- Q:如果我们遇到 thread 无法获取所需资源时,该如何操作?
- A:不断重试呗,一旦资源释放可快速尝试获取
- Q:那如果资源持有时长较长,不断循环获取,是否比较浪费CPU ?
- A:的确,那就让线程休息1秒钟,再尝试获取,这样就不会导致CPU空转了
- Q:那如果资源在第0.1秒时被释放,那线程岂不是要白白等待0.9秒了 ?
- A:实在不行就让当前线程挂起,等释放资源的线程去通知当前线程,这样就不存在等待时间长短的问题了
- Q:但如果资源持有时间很短,每次都挂起、唤醒线程成为了一个很大的开销
- A:那就依情况而定,lock时间短的,就不断循环重试,时间长的就挂起
- Q:如何界定lock的时间长短?还有就是如果lock的时间不固定,也无法预期呢?
- A:唔。。。这是个问题
- Q:如果线程等待期间,我想放弃呢?
- A:。。。。。。
- Q:还有很多问题
- 如果我想动态增加资源呢?
- 如何我不想产生饥饿,而保证加锁的有序性呢?
- 或者我要支持/不支持可重入特性呢?
- 我要查看所有等待资源的线程状态呢?
- 。。。。。。
我们发现,一个简单的等待资源的问题,牵扯出后续诸多庞杂且无头绪的问题;加锁不仅依赖一套完善的框架体系,还要具体根据使用场景而定,才能接近最优解;那我们即将要引出的AQS能完美解决上述这些问题吗?
答案是肯定的:不能
其实Doug Lea也意识到问题的复杂性,不可能出一个超级工具来解决所有问题,所以他把AQS设计为一个abstract类,并提供一系列子类去解决不同场景的问题,例如ReentrantLock
、Semaphore
等;当我们发现这些子类也不能满足我们加锁需求时,我们可以定义自己的子类,通过重写两三个方法,寥寥几行代码,实现强大的功能,这一切都得益于AQS作者敏锐的前瞻性
指的一提的是,虽然我们可以用某个子类去实现另一个子类所提供的功能(例如使用Semaphore
替代CountDownLatch
),但其易用、简洁、高效性等能否达到理想效果,都值得商榷;就好比在陆地上穿着雪橇走路,虽能前进,却低效易摔跤
1.2、并发框架
本小节仅带大家对AQS架构有个初步了解,在后文的独占锁、共享锁等中会详细阐述。下图为AQS框架的主体结构
从上图中我们看到了AQS中非常关键的一个概念:“阻塞队列”。即AQS的理念是当线程无法获取资源时,提供一个FIFO类型的有序队列,用来维护所有处于“等待中”的线程。看似无解可击的框架设计,同时也牵出另外的一个问题:阻塞队列一定高效吗?
当“同步块逻辑”执行很快时,我们列出两种场景
- 场景1:直接使用AQS框架,例如试用其子类
ReentrantLock
,遇到资源争抢,放阻塞队列 - 场景2:因为锁占用时间短,无限重试
针对这2种场景,我们写测试用例比较一下
package org.xijiu.share.aqs.compare;
import org.junit.Test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author likangning
* @since 2021/3/9 上午8:58
*/
public class CompareTest {
private class MyReentrantLock extends AbstractQueuedSynchronizer {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
while (true) {
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
}
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
/**
* 使用AQS框架
*/
@Test
public void test1() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
long begin = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 2; i++) {
executorService.submit(() -> {
for (int j = 0; j < 50000000; j++) {
reentrantLock.lock();
doBusiness();
reentrantLock.unlock();
}
});
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin));
}
/**
* 无限重试
*/
@Test
public void test2() throws InterruptedException {
MyReentrantLock myReentrantLock = new MyReentrantLock();
long begin = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 2; i++) {
executorService.submit(() -> {
for (int j = 0; j < 50000000; j++) {
myReentrantLock.tryAcquire(1);
doBusiness();
myReentrantLock.tryRelease(1);
}
});
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
System.out.println("MyReentrantLock cost : " + (System.currentTimeMillis() - begin));
}
private void doBusiness() {
// 空实现,模拟程序快速运行
}
}
上例,虽然MyReentrantLock
继承了AbstractQueuedSynchronizer
,但没有使用其阻塞队列。我们每种情况跑5次,看下两者在耗时层面的表现
类 | 耗时1 | 耗时2 | 耗时3 | 耗时4 | 耗时5 | 平均耗时(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
11425 | 12301 | 12289 | 10262 | 11461 | 11548 |
MyReentrantLock |
8717 | 8957 | 10283 | 8445 | 8928 | 9066 |
上例只是拿独占锁举例,共享锁也同理。可以简单概括为:线程挂起、唤醒的时间占整个加锁周期比重较大,导致每次挂起、唤醒已经成为一种负担。当然此处并不是说AQS设计有什么缺陷,只是想表达并没有一种万能的框架能应对所有情况,一切都要靠使用者灵活理解、应用
1.3、拓扑结构及如何使用
我们常用的锁并发类,基本上都是AQS的子类或通过组合方式实现,可见AQS在Java并发体系的重要性
至于如何使用,是需要区分子类是想实现独占锁还是共享锁
独占锁
tryAcquire()
tryRelease()
isHeldExclusively()
-- 可不实现
共享锁
tryAcquireShared()
tryReleaseShared()
AQS本身是一个abstract
类,将主要并发逻辑进行了封装,我们定义自己的并发控制类,仅需要实现其中的两三个方法即可。而在对外(public
方法)表现形式上,可依据自己的业务特性来定义;例如Semaphore
定义为acquire
、release
,而ReentrantLock
定义为lock
、unlock
二、锁
相信大家经常会被各种各样锁的定义搞乱,叫法儿也五花八门,为了后续行文的方便,此章我们把一些锁概念阐述一下
2.1、独占锁
独占锁,顾名思义,即在同一时刻,仅允许一个线程执行同步块代码。好比一伙儿人想要过河,但只有一根独木桥,且只能承受一人的重量
JDK支持的典型独占锁:ReentrantLock
、ReentrantReadWriteLock
2.2、共享锁
共享锁其实是相对独占锁而言的,涉及到共享锁就要聊到并发度,即同一时刻最多允许同时执行线程的数量。上图所述的并发度为3,即在同一时刻,最多可有3个人在同时过河。
但共享锁的并发度也可以设置为1,此时它可以看作是独占锁
JDK支持的典型独占锁:Semaphore
、CountDownLatch
2.3、公平锁
虽然叫做公平锁,但我们知道任何事情都是相对的,此处也不例外,我们也只能做到相对公平,后文会涉及,此处不再赘述
线程在进入时,首先要检查阻塞队列中是否为空,如果发现已有线程在排队,那么主动添加至队尾并等待被逐一唤起;如果发现阻塞队列为空,才会尝试去获取资源。公平锁相对非公平锁效率较低,通常来讲,加锁时间越短,表现越明显
2.4、非公平锁
任何一个刚进入的线程,都会尝试去获取资源,释放资源后,还会通知头节点去尝试获取资源,这样可能导致饥饿发生,即某一个阻塞队列中的线程一直得不到调度。
那为什么我们会说,非公平锁的效率要高于公平锁呢?假设一个独占锁,阻塞队列中已经有10个线程在排队,线程A抢到资源并执行完毕后,去唤醒头结点head,head线程唤醒需要时间,head唤醒后才尝试去获取资源,而在整个过程中,没有线程在执行加锁代码
因为线程唤起需要引发用户态及内核态的切换,故是一个相对比较耗时的操作。
我们再举一个不恰当的例子:行政部在操场上为同学们办理业务,因为天气炎热,故让排队的同学在场边一个凉亭等待,凉亭距离业务点约300米,且无法直接看到业务点,需要等待上一个办理完毕的同学来通知。假定平均办理一个业务耗时约30秒
- 公平锁:所有新来办理业务的同学都被告知去排队,上一个办理完业务的同学需要去300米外通知下一个同学,来回600米的路程(线程唤醒)预估耗时2分钟,在这2分钟里,因为没有同学过来办理业务,业务点处于等待状态
- 非公平锁:新来办理业务的同学首先看一下业务点是否有人正在在办理,如果有人正在办理,那么主动进入排队,如果办理点空闲,那么直接开始办理业务。明显非公平锁更高效,队首的同学接到通知,过来办理的时间片内,业务点可能已经处理了2个同学的业务
AQS框架是支持公平、非公平两种模式的,使用者可以根据自身的情况做选择,而Java中的内置锁synchronized
是非公平锁
2.5、可重入锁
即某个线程获取到锁后、在释放锁之前,再次尝试获取锁,能成功获取到,不会出现死锁,便是可重入锁;需要注意的是,加锁次数需要跟释放次数一样
synchronized
、ReentrantLock
均为可重入锁
2.6、偏向锁 / 轻量级锁 / 重量级锁
之所以将这三个锁放在一起论述,是因为它们都是synchronized
引入的概念,为了描述流畅,我们把它们放在一起
- 偏向锁:JVM设计者发现,在大多数场景中,在同一时刻争抢
synchronized
锁只有一个线程,而且总是被这一个线程反复加锁、解锁;故引入偏向锁,且向对象头的MarkWord
部分中, 标记上线程id,值得一提的是,在线程加锁结束后,并没有解锁的动作,这样带来的好处首先是少了一次CAS操作,其次当这个线程再次尝试加锁时,仅仅比较MarkWord
部分中的线程id与当前线程的id是否一致,如果一致则加锁成功。偏向锁因此而得名,它偏向于占有它的线程,对其非常友好。当上一个线程释放锁后,如果有另一个线程尝试加锁,偏向锁会重新偏向新的线程。而当一个线程正占有锁,又有一个新的线程试图加锁时,便进入了轻量级锁 - 轻量级锁:所谓轻量级锁,是针对重量级锁而言的,这个阶段也有人叫自旋锁。其本质就是不会马上挂起线程,而是反复重试10(可使用参数-XX:PreBlockSpin来修改)次。因为线程挂起、唤醒也是相当耗时的,在锁并发不高、加锁时间短时,采用自旋可以得到更好的效果,具体可以参考1.2章的测试用例
- 重量级锁:线程挂起并进入阻塞队列,等待被唤醒
这3层锁是逐级膨胀的,且过程不可回逆,即某个锁一旦进入重量级锁,便不可回退至轻量级锁或偏向锁。虽然synchronized
不是本文的重点,但既然提起来了,我们可以把其特性简单罗列一下
synchronized
独占锁、非公平锁、可重入;内部做了很多优化
那synchronized
锁的性能究竟如何呢?我们跟AQS框架中的ReentrantLock
做个简单对比
public class SynchronizedAndReentrant {
private static int THREAD_NUM = 5;
private static int EXECUTE_COUNT = 30000000;
/**
* 模拟ReentrantLock处理业务
*/
@Test
public void test() throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
long begin = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < THREAD_NUM; i++) {
executorService.submit(() -> {
for (int j = 0; j < EXECUTE_COUNT; j++) {
reentrantLock.lock();
doBusiness();
reentrantLock.unlock();
}
});
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
System.out.println("ReentrantLock cost : " + (System.currentTimeMillis() - begin));
}
private void doBusiness() {
}
/**
* 模拟synchronized处理业务
*/
@Test
public void test2() throws InterruptedException {
long begin = System.currentTimeMillis();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < THREAD_NUM; i++) {
executorService.submit(() -> {
for (int j = 0; j < EXECUTE_COUNT; j++) {
synchronized (SynchronizedAndReentrant.class) {
doBusiness();
}
}
});
}
executorService.shutdown();
executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
System.out.println("synchronized cost : " + (System.currentTimeMillis() - begin));
}
}
锁 | 耗时1 | 耗时2 | 耗时3 | 耗时4 | 耗时5 | 平均耗时(ms) |
---|---|---|---|---|---|---|
ReentrantLock |
5876 | 5879 | 5601 | 5939 | 5925 | 5844 |
synchronized |
5551 | 5611 | 5794 | 5397 | 5445 | 5559 |
在JDK1.8的ConcurrentHashMap
中,作者已经将分段锁摒弃,进而采用synchronized
为分桶加锁。synchronized
已日趋成熟,我们应该摒弃对它低性能的偏见,放心大胆地去使用它
深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念的更多相关文章
- 深入理解Java并发框架AQS系列(一):线程
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 一.概述 1.1.前言 重剑无锋,大巧不工 读j.u.c包下的源码,永远无法绕开的经典 ...
- 深入理解Java并发框架AQS系列(四):共享锁(Shared Lock)
深入理解Java并发框架AQS系列(一):线程 深入理解Java并发框架AQS系列(二):AQS框架简介及锁概念 深入理解Java并发框架AQS系列(三):独占锁(Exclusive Lock) 深入 ...
- 深入理解Java内存模型之系列篇
深入理解Java内存模型(一)——基础 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来 ...
- Java并发编程之CAS二源码追根溯源
Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...
- 【Java并发编程】之二:线程中断
[Java并发编程]之二:线程中断 使用interrupt()中断线程 当一个线程运行时,另一个线程可以调用对应的Thread对象的interrupt()方法来中断它,该方法只是在目标线程中设置一 ...
- java并发编程笔记(二)——并发工具
java并发编程笔记(二)--并发工具 工具: Postman:http请求模拟工具 Apache Bench(AB):Apache附带的工具,测试网站性能 JMeter:Apache组织开发的压力测 ...
- JAVA基础再回首(二十五)——Lock锁的使用、死锁问题、多线程生产者和消费者、线程池、匿名内部类使用多线程、定时器、面试题
JAVA基础再回首(二十五)--Lock锁的使用.死锁问题.多线程生产者和消费者.线程池.匿名内部类使用多线程.定时器.面试题 版权声明:转载必须注明本文转自程序猿杜鹏程的博客:http://blog ...
- 《Java并发编程实战》读书笔记一 -- 简介
<Java并发编程实战>读书笔记一 -- 简介 并发的历史 并发的历史,也是人类利用有限的资源去提高生产效率的一个的例子. 设想现在有台计算机,这台计算机具有以下的资源: 单核CPU一个 ...
- “全栈2019”Java多线程第三十二章:显式锁Lock等待唤醒机制详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
随机推荐
- Linux-平均负载指数
目录 系统平均负载 什么是平均负载 平均负载多少合理 如何观察平均负载 平均负载和CPU的使用率的区别 平均负载分析 执行CPU密集型任务 执行I/O密集型任务 大量进程调度 关于平均负载的总结 系统 ...
- Linux 驱动框架---input子系统框架
前面从具体(Linux 驱动框架---input子系统)的工作过程学习了Linux的input子系统相关的架构知识,但是前面的学习比较实际缺少总结,所以今天就来总结一下输入子系统的架构分层,站到远处来 ...
- 硬盘测试工具fio用法总结
一 fio介绍 linux下的一种常用的磁盘测试工具,支持裸盘和文件形式进行测试 二 硬盘测试常用名词 延迟:io的发起到返回写入成功的时间成为延迟,fio中延迟分为lat,slat,clat ...
- Linux Bash Script conditions
Linux Bash Script conditions shell 编程之条件判断 条件判断式语句.单分支 if 语句.双分支 if 语句.多分支 if 语句.case 语句 refs http:/ ...
- macOS & Catalina vs Big Sur
macOS & Catalina vs Big Sur 乍一看,macOS的色彩更加丰富,最大的变化就是明亮,略带卡通风格的iOS形状的图标. 一切都变得更加圆润,感觉一切都变得更大了. 这可 ...
- 使用 js 实现一个简易版的 async 库
使用 js 实现一个简易版的 async 库 具有挑战性的前端面试题 series & parallel 串行,并行 refs https://www.infoq.cn/article/0NU ...
- project generators & project scaffold
project generators & project scaffold how to write a node cli & Project Scaffold https://www ...
- DBA 的效率加速器——CloudQuery v1.3.2 上线!
嘿,兄弟,我们好久不见,你在哪里 嘿,朋友,如果真的是你,请打声招呼 我说好久不见,你去哪里 你却对我说,我去江湖 我去看 CloudQuery v1.3.2,看看新增了哪些好用的小功能! 一.自动/ ...
- Python安装教程
1.下载好Python安装包后,双击打开(第一个是32位,第二个是64位,根据自己电脑位数进行选择): 2.打开后如下,先将下方的Python添加到系统环境变量勾选上,再点击第一个默认安装即可: 3. ...
- 基于3.X版本的脚手架创建VUE项目
一.基于交互式命令行的方式,创建vue项目 1.命令:vue create 项目名称.项目名称必须是英文的.不要包含中文.特殊的字符和符号.在cmd中输入命令:vue create vue_proje ...