春节越来越近了,疫情也越来越严重,但挡不住叫练携一家老小回老家(湖北)团聚的冲动。响应国家要求去我们做核酸检测了。

独占锁


早上叫练带着一家三口来到了南京市第一医院做核酸检测,护士小姐姐站在医院门口拦着告诉我们人比较多,无论大人小孩,需要排队一个个等待医生采集唾液检测,OK,下面我们用代码+图看看我们一家三口是怎么排队的!

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* @author :jiaolian
* @date :Created in 2021-01-22 10:33
* @description:独占锁测试
* @modified By:
* 公众号:叫练
*/
public class ExclusiveLockTest { private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //医院
private static class Hospital { private String name; public Hospital(String name) {
this.name = name;
} //核酸检测排队测试
public void checkUp() {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName()+"正在做核酸检测");
//核酸过程...难受...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
} public static void main(String[] args) throws InterruptedException {
Hospital hospital = new Hospital("南京市第一医院");
Thread JLWife = new Thread(()->hospital.checkUp(),"叫练妻");
JLWife.start();
//睡眠100毫秒是让一家三口是有顺序的排队去检测
Thread.sleep(100);
Thread JLSon = new Thread(()->hospital.checkUp(),"叫练子");
JLSon.start();
Thread.sleep(100);
Thread JL = new Thread(()->hospital.checkUp(),"叫练");
JL.start();
}
}

如上代码:在主线程启动三个线程去医院门口排队,女士优先,叫练妻是排在最前面的,中间站的是叫练的孩子,最后就是叫练自己了。我们假设模拟了下核酸检测一次需要3秒。代码中我们用了独占锁,独占锁可以理解成医院只有一个医生,一个医生同时只能为一个人做核酸,所以需要逐个排队检测,所以代码执行完毕一共需要花费9秒,核酸检测就可以全部做完。代码逻辑还是比较简单,和我们之前文章描述synchronized同理。核酸排队我们用图描述下吧!

AQS全称是AbstractQueueSynchroniz,意为队列同步器,本质上是一个双向链表,在AQS里面每个线程都被封装成一个Node节点,每个节点都通过尾插法添加。另外节点还有还封装状态信息,比如是独占的还是共享的,如上面的案例就表示独占Node,医生他本身是一种共享资源,在AQS内部里面叫它state,用int类型表示,线程都会通过CAS的方式争抢state。线程抢到锁了,就自增,没有抢到锁的线程会阻塞等待时机被唤醒。如下图:根据我们理解抽象出来AQS的内部结构。



**根据上面描述,大家看AQS不就是用Node封装线程,然后把线程按照先来后到(非公平锁除外)连接起来的双向链表嘛!关于非公平锁我之前写《排队打饭》案例中也通过简单例子描述过。有兴趣童鞋可以翻看下!

**

**

共享锁


上面我们做核酸的过程是同步执行的,叫独占锁。那共享锁是什么意思呢?现在叫练孩子只有3岁,不能独立完成核酸检测,护士小姐姐感同身受,观察叫练子是排在叫练妻后面的,就让他们一起同时做核酸检测。这种同时做核酸的操作,相当于同时去获取医生资源,我们称之为共享锁。下面是我们测试代码。

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* @author :jiaolian
* @date :Created in 2021-01-21 19:54
* @description:共享锁测试
* @modified By:
* 公众号:叫练
*/
public class SharedLockTest { private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); //医院
private static class Hospital { private String name; public Hospital(String name) {
this.name = name;
} //核酸检测排队测试
public void checkUp() {
try {
readLock.lock();
System.out.println(Thread.currentThread().getName()+"正在做核酸检测");
//核酸过程...难受...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
} public static void main(String[] args) throws InterruptedException {
Hospital hospital = new Hospital("南京市第一医院");
Thread JLWife = new Thread(()->hospital.checkUp(),"叫练妻");
JLWife.start();
//睡眠100毫秒是让一家三口是有顺序的排队去检测
Thread.sleep(100);
Thread JLSon = new Thread(()->hospital.checkUp(),"叫练子");
JLSon.start();
/*Thread.sleep(100);
Thread JL = new Thread(()->hospital.checkUp(),"叫练");
JL.start();*/
} }

上面代码我们用ReentrantReadWriteLock.ReadLock作为读锁,在主线程启动“叫练妻”和“叫练”两个线程,本来母子俩一共需要6秒才能完成的事情,现在只需要3秒就可以做完,共享锁好处是效率比较高。如下图,是AQS内部某一时刻Node节点状态。对比上图,Node的状态变为了共享状态,这些节点可以同时去共享医生资源

synchronized锁不响应中断


/**
* @author :jiaolian
* @date :Created in 2020-12-31 18:17
* @description:sync不响应中断
* @modified By:
* 公众号:叫练
*/
public class SynchronizedInterrputedTest { private static class MyService { public synchronized void lockInterrupt() {
try {
System.out.println(Thread.currentThread().getName()+" 获取到了锁");
while (true) {
//System.out.println();
}
} catch (Exception e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
MyService myService = new MyService();
//先启动线程A,让线程A先拥有锁
Thread threadA = new Thread(()->{
myService.lockInterrupt();
});
threadA.start();
Thread.sleep(1000);
//启动线程B,中断,synchronized不响应中断!
Thread threadB = new Thread(()->{
myService.lockInterrupt();
});
threadB.start();
Thread.sleep(1000);
threadB.interrupt();
}
}

如上述代码:先启动A线程,让线程A先拥有锁,睡眠1秒再启动线程B是让B线程处于可运行状态,隔1秒后再中断B线程。在控制台输出如下:A线程获取到了锁,等待2秒后控制台并没有立刻输出报错信息,程序一直未结束执行,说明synchronized锁不响应中断,需要B线程获取锁后才会输出线程中断报错信息!

AQS响应中断


经常做比较知识才会融会贯通,在Lock提供lock和lockInterruptibly两种获取锁的方式,其中lock方法和synchronized是不响应中断的,那下面我们看看lockInterruptibly响应中断是什么意思。我们还是用核酸案例说明。

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* @author :jiaolian
* @date :Created in 2021-01-22 15:18
* @description:AQS响应中断代码测试
* @modified By:
* 公众号:叫练
*/
public class AQSInterrputedTest { private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //医院
private static class Hospital { private String name; public Hospital(String name) {
this.name = name;
} //核酸检测排队测试
public void checkUp() {
try {
writeLock.lockInterruptibly();
System.out.println(Thread.currentThread().getName()+"正在做核酸检测");
//核酸过程...难受...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
} public static void main(String[] args) throws InterruptedException {
Hospital hospital = new Hospital("南京市第一医院");
Thread JLWife = new Thread(()->hospital.checkUp(),"叫练妻");
JLWife.start();
//睡眠100毫秒是让一家三口是有顺序的排队去检测
Thread.sleep(100);
Thread JLSon = new Thread(()->hospital.checkUp(),"叫练子");
JLSon.start();
Thread.sleep(100);
Thread JL = new Thread(()->hospital.checkUp(),"叫练");
JL.start();
//等待1秒,中断叫练线程
System.out.println("护士小姐姐想和叫练私聊会!");
Thread.sleep(1000);
JL.interrupt();
}
}

如上代码:叫练一家三口采用的是独占锁排队去做核酸,叫练线程等待一秒后,护士小姐姐想和叫练私聊会!莫非小姐姐会有啥想法,于是叫练立刻中断了这次的核酸检测,注意是立刻中断。控制台打印结果如下:叫练妻线程和叫练子线程都做了核酸,但叫练却没有做成功!因为被护士小姐姐中断了,结果如下图所示。所以我们能得出结论,在aqs中锁是可以响应中断的。现在如果将上述代码中lockInterruptibly方法换成lock方法会发生什么情况呢,如果换成这种方式,小姐姐再来撩我,叫练要先成功获取锁,也就说叫练已经到医生旁边准备做核酸了,小姐姐突然说有事找叫练,最终导致叫练没有做核酸,碰上这样的事,只能说小姐姐是存心的,小姐姐太坏了。关于lock方法不响应中断的测试大家可以自己测试下。看看我是不是冤枉护士小姐姐了。

我们可以得出结论:在aqs中如果一个线程正在获取锁或者处于等待状态,另一个线程中断了该线程,响应中断的意思是该线程立刻中断,而不响应中断的意思是该线程需要获取锁后再中断。



条件队列


人生或许有那么些不如意。漫长的一个小时排队等待终于过去了,轮到我们准备做核酸了,你说气不气,每次叫练妻出门都带身份证,可偏偏回家这次忘记了?我们用代码看看叫练一家三口在做核酸的过程中到底发生了啥事情?又是怎么处理的!

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantReadWriteLock; /**
* @author :jiaolian
* @date :Created in 2021-01-22 16:10
* @description:条件队列测试
* @modified By:
* 公众号:叫练
*/
public class ConditionTest { private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); //条件队列
private static Condition condition = writeLock.newCondition(); //医院
private static class Hospital { private String name; public Hospital(String name) {
this.name = name;
} //核酸检测排队测试
public void checkUp(boolean isIdCard) {
try {
writeLock.lock();
validateIdCard(isIdCard);
System.out.println(Thread.currentThread().getName()+"正在做核酸检测");
//核酸过程...难受...
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+"核酸检测完成");
}
} //校验身份信息;
private void validateIdCard(boolean isIdCard) {
//如果没有身份信息,需要等待
if (!isIdCard) {
try {
System.out.println(Thread.currentThread().getName()+"忘记带身份证了");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} //通知所有等待的人
public void singleAll() {
try {
writeLock.lock();
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
} } public static void main(String[] args) throws InterruptedException {
Hospital hospital = new Hospital("南京市第一医院");
Thread.currentThread().setName("护士小姐姐线程");
Thread JLWife = new Thread(()->{
hospital.checkUp(false);
},"叫练妻");
JLWife.start();
//睡眠100毫秒是让一家三口是有顺序的排队去检测
Thread.sleep(100);
Thread JLSon = new Thread(()->hospital.checkUp(true),"叫练子");
JLSon.start();
Thread.sleep(100);
Thread JL = new Thread(()->{
hospital.checkUp(true);
},"叫练");
JL.start();
//等待叫练线程执行完毕
JL.join();
hospital.singleAll();
} }

如上代码:一家人获取独占锁需要排队检测,叫练妻先进去准备核酸,护士小姐姐说先要刷身份证才能进去,叫练妻突然回想起来,出门走得急身份证忘记带了,这可咋办,需要重新排队吗?叫练妻很恐慌,护士小姐姐说,要不这样吧,你先赶紧回家拿,等叫练子,叫练先检测完,我就赶紧安排你进去在做核酸,那样你就不需要重新排队了,这就是上述这段代码的表达意思。我们看看执行结果如下图,和我们分析的结果一致,下图最后画红圈的地方叫练妻最后完成核酸检测。下面我们看看AQS内部经历的过程。

如下图,当叫练妻先获取锁,发现身份证忘带调用await方法会释放持有的锁,并把自己当做node节点放入条件队列的尾部,此时条件队列为空,所以条件队列中只有叫练妻一个线程在里面,接着护士小姐姐会将核酸医生这个资源释放分配给下一个等待者,也就是叫练子线程,同理,叫练子执行完毕释放锁之后会唤醒叫练线程,底层是用LockSupport.unpark来完成唤醒的的操作,相当于基础系列里的wait/notify/notifyAll等方法。当叫练线程执行完毕,后面没有线程了,护士小姐姐调用singleAll方法会见条件队列的叫练妻线程唤醒,并加入到AQS的尾部,等待执行。其中条件队列是一个单向链表,一个AQS可以通过newCondition()对应多个条件队列。这里我们就不单独用代码做测试了。

总结


今天我们用代码+图片+故事的方式说明了AQS重要的几个概念,整理出来希望能对你有帮助,写的比不全,同时还有许多需要修正的地方,希望亲们加以指正和点评,年前这段时间会继续输出实现AQS高级锁,如:ReentrantLock,线程池这些概念等。最后喜欢的请点赞加关注哦。我是叫练【公众号】,边叫边练。

注意:本故事是自己虚构出来的,仅供大家参考理解。希望大家过年都能顺利回家团聚!

核酸检测:让我明白AQS原理的更多相关文章

  1. AQS 原理以及 AQS 同步组件总结

    1 AQS 简单介绍 AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面. AQS 是一个用来构建锁和同步 ...

  2. 当我们谈论算法我们在谈论什么:由疫情核酸检测想到的分治算法(Divide-and-Conquer)

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_159 北京的疫情一波未平一波又起,由此看来,战"疫"将是一场旷日持久的战争,绝不能掉以轻心.轻易言胜.病毒随时 ...

  3. AQS原理以及AQS同步组件总结

    1 AQS简单介绍 2 AQS原理

  4. 并发之AQS原理(三) 如何保证并发

    并发之AQS原理(三) 如何保证并发 1. 如何保证并发 AbstractQueuedSynchronizer 维护了一个state(代表了共享资源)和一个FIFO线程等待队列(多线程竞争资源被阻塞时 ...

  5. 并发之AQS原理(一) 原理介绍简单使用

    并发之AQS原理(一) 如果说每一个同步的工具各有各的强大,那么这个强大背后是一个相同的动力,它就是AQS. AQS是什么 AQS是指java.util.concurrent.locks包里的Abst ...

  6. 简述AQS原理

    这是一道面试题:简述AQS原理 AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态.如果被请求的共享资源被占用,那么就需要一套线程阻塞 ...

  7. 看完您如果还不明白 Kerberos 原理,算我输!

    系统环境 操作系统:CentOS 6 或 CentOS 7 JDK 版本:1.8.0_151 Ambari 版本:2.6.1 HDP 版本:2.6.4.0 扩展链接 Kerberos原理--经典对话 ...

  8. AQS原理

    1. AQS原理 1.1. 是什么 AQS全程AbstractQueuedSynchronizer抽象队列同步器,它是并发包中的基础类 ReetrantLock,ReentrantReadWriteL ...

  9. 并发之AQS原理(二) CLH队列与Node解析

    并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...

随机推荐

  1. 浅入 AutoMapper

    目录 浅入 AutoMapper AutoMapper 基本使用 映射配置 映射检查 性能 Profile 配置 依赖注入 表达式与 DTO 浅入 AutoMapper 在 Nuget 搜索即可安装, ...

  2. rocketMq 消息偏移量 Offset

    消息偏移量 Offset queue0 offset 0   0-20  offset 4  20-40 纠错:每条消息的tag对应的HashCode. queue1 offset 1  0-20   ...

  3. Atlas 2.1.0 实践(1)—— 编译Atlas

    为什么要做数据治理? 业务繁多,数据繁多,业务数据不断迭代.人员流动,文档不全,逻辑不清楚,对于数据很难直观理解,后期很难维护. 在大数据研发中,原始数据就有着非常多的数据库,数据表. 而经过数据的聚 ...

  4. C#中获取DataTable某一列的值转换为集合

    直接使用 //Linqvar l1 = (from d in dt.AsEnumerable() select d.Field<int>("ID")).ToList() ...

  5. 将Maven镜像更换为国内阿里云仓库

    1.国内访问maven默认远程中央镜像特别慢 2.用阿里的镜像替代远程中央镜像 3.大部分jar包都可以在阿里镜像中找到,部分jar包在阿里镜像中没有,需要单独配置镜像 换为国内镜像,让你感受飞一般的 ...

  6. JavaWeb代码复用

    servlet部分,可能用得到的复用的代码: 1.dopost设置字符 request.setCharacterEncoding("utf-8"); response.setCha ...

  7. centos7安装手册

    一:centos7下载 下载centos可以访问阿里云完成centos 7.7下载(建议使用迅雷下载):http://mirrors.aliyun.com/centos/7.9.2009/isos/x ...

  8. java数组之基本语义

    A[] a; B[] b=new B[5];print(b)print(b.length)a={new A(),new A()}//聚合初始化:隐式使用new在堆中创建A[] d=new A[];a= ...

  9. C++模板元编程----堆排序

    目录 目录 前言 实现的一些小细节 Debug 惰性求值 总结 Ref 前言 经过前两次经验的积累,终于来到了麻烦的堆排序.在一开始接触模板元编程的时候,我就期望有一天能够写出元编程堆排序的代码.原因 ...

  10. Spring源码深度解析之数据库连接JDBC

    Spring源码深度解析之数据库连接JDBC JDBC(Java Data Base Connectivity,Java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供 ...