核酸检测:让我明白AQS原理

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

独占锁


早上叫练带着一家三口来到了南京市第一医院做核酸检测,护士小姐姐站在医院门口拦着告诉们人比较多,无论大人小孩,需要排队一个个等待医生采集唾液检测,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();
                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的内部结构。

image.png
**根据上面描述,大家看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();
                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();
                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中如果一个线程正在获取锁或者处于等待状态,另一个线程中断了该线程,响应中断的意思是该线程立刻中断,而不响应中断的意思是该线程需要获取锁后再中断。

image.png

条件队列


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

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();
                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();
                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,线程池这些概念等。最后喜欢的请点赞加关注哦。是叫练【公众号】,边叫边练。 注意:本故事是自己虚构出来的,仅供大家参考理解。希望大家过年都能顺利回家团聚!
tempImage1611306633088.gif

[] ) [](top

原文创作:叫练

原文链接:https://www.cnblogs.com/jiaolian/p/14319024.html

文章列表

更多推荐

更多
  • Azure上Linux管理-十、使用 Azure Kubernetes 服务 技术要求,开始使用 AKS,与 Helm 一起工作,使用草稿,管理 Kubernetes,问题,进一步,使用 WSL 和 VS Code,安装依赖,kubectl 安装,使用 Azure CLI 创建集群,AKS 首次部署,创建服务,多
    Apache CN

  • Azure上Linux管理-十一、故障排除和监控您的工作负载 module(load="imuxsock"),技术要求,访问您的系统,Azure 日志分析,性能监控,问题,进一步,不允许远程访问,正在端口上工作,使用 nftables,引导诊断,Linux 登录,配置日志分析服务,安装 Azure
    Apache CN

  • Azure上Linux管理-十二、附录 第一章:探索微软 Azure 云,第二章:Azure 云入门,第三章:Linux 基础管理,第 4 章:管理 Azure,第五章:高级 Linux 管理,第七章:部署虚拟机,第八章:探索连续配置自动化,第 9 章:Azure 中的容器虚
    Apache CN

  • Azure上Linux管理-九、Azure 中的容器虚拟化 cloudconfig,集装箱技术导论,系统生成,Docker,Azure 容器实例,Buildah, Podman, and Skopeo,容器和储存,问题,进一步,容器历史,chroot 环境,OpenVZ,LXC,创建一个带启动的
    Apache CN

  • Azure上Linux管理-七、部署你的虚拟机 ResourceGroup 不存在,创建它:,vnet 不存在,创建 vnet,cloudconfig,Vagrant.config 结束,部署场景,Azure 中的自动部署选项,初始配置,流浪,封隔器,自定义虚拟机和 vhd,问题,进
    Apache CN

  • Azure上Linux管理-八、探索持续配置自动化 了解配置管理,使用 Ansible,使用地球形态,使用 PowerShell DSC,Azure 策略客户端配置,其他解决方案,问题,进一步,技术要求,Ansible 的安装,SSH 配置,裸最小配置,库存文件,Ansible 剧本和模
    Apache CN

  • Azure上Linux管理-五、高级 Linux 管理 技术要求,软件管理,网络,存储,systemd,问题,进一步,RPM 软件管理器,YUM 软件管理,基于 DNF 的软件管理,DPKG 软件管理器,运用 apt 进行软件管理,ZYpp 软件管理,识别网络接口,IP 地址识别,显示路由表
    Apache CN

  • Azure上Linux管理-六、管理 Linux 安全与身份 SELINUX=可以接受以下三个值之一:,permissiveSELinux 打印警告而不是强制执行。,disabled 没有加载 SELinux 策略。,SELINUXTYPE=可以接受以下三个值之一:,targeted 目标进程
    Apache CN

  • Azure上Linux管理-四、管理 Azure 使用 Azure CLI 和 PowerShell 管理 Azure 资源,技术要求,管理存储资源,管理网络资源,管理计算资源,虚拟机资源,问题,进一步,存储帐户,托管磁盘,Azure 文件,Azure Blob,虚拟网络,子网,网络安
    Apache CN

  • Azure上Linux管理-三、Linux 基础管理 Linux Shell,获取帮助,使用文本文件,在文件系统中找到你的方式,流程管理,自由访问控制,问题,执行命令,命令行编辑,与历史一起工作,自动完成,球状,重定向,处理变量,Bash 配置文件,使用手册页,使用信息文档,其他文档,文本
    Apache CN

  • 近期文章

    更多
    文章目录

      推荐作者

      更多