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

32次阅读

共计 8061 个字符,预计需要花费 21 分钟才能阅读完成。

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

独占锁


早上叫练带着一家三口来到了南京市第一医院做核酸检测,护士小姐姐站在医院门口拦着通知咱们人比拟多,无论小孩儿小孩,须要排队一个个期待医生采集唾液检测,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,线程池这些概念等。最初喜爱的请点赞加关注哦。我是 叫练【公众号】,边叫边练。

留神:本故事是本人虚构进去的,仅供大家参考了解。心愿大家过年都能顺利回家团圆!

正文完
 0