9

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

 3 years ago
source link: https://segmentfault.com/a/1190000039071472
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

AzuaEzn.png!mobile

独占锁

早上叫练带着一家三口来到了南京市第一医院做核酸检测,护士小姐姐站在医院门口拦着告诉我们人比较多,无论大人小孩,需要排队一个个等待医生采集唾液检测,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同理。核酸排队我们用图描述下吧!

fI7zYnE.png!mobile

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

ABFveyJ.png!mobile

根据上面描述,大家看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的状态变为了共享状态,这些节点可以同时去共享医生资源

BreMvee.png!mobile

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线程获取锁后才会输出线程中断报错信息!

yaEraiR.png!mobile

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中如果一个线程正在获取锁或者处于等待状态,另一个线程中断了该线程,响应中断的意思是该线程立刻中断,而不响应中断的意思是该线程需要获取锁后再中断。

y6neiqE.png!mobile

vaiaErM.png!mobile

条件队列

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

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内部经历的过程。

uIrq2m.png!mobile

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

32Mfe27.png!mobile

总结

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

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

vMfeym3.gif!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK