AQS这样学就很简单了

2022-05-09 15:08:53 浏览数 (1)

哈喽,我是子牙。十余年技术生涯,一路披荆斩棘从技术小白到技术总监到JVM专家到创业。技术栈如汇编、C语言、C 、Windows内核、Linux内核。特别喜欢研究虚拟机底层实现,对JVM有深入研究。分享的文章偏硬核,很硬的那种。

手撸过JVM、内存池、垃圾回收算法、synchronized、线程池、NIO、三色标记算法…

AQS,抽象队列同步器,为了解决Java多线程环境下的互斥与同步而生。JUC包中的很多类都是基于AQS实现的,可见AQS的重要性。但是很多同学在学AQS的过程中觉得很难很抽象,在这里分享下我是如何精通AQS的。

精通AQS需要结合线程互斥、线程同步两套场景去理解,本篇文章主要讲AQS针对互斥场景是如何处理的。

如果你是道格李

如果你是道格李,你要实现一套机制来保证线程互斥,你会如何实现呢?你肯定不会一上来就写代码对吧,你会想有哪些场景会出现线程互斥、针对每个场景抽象出需要实现的功能、针对这些功能底层选择什么样的数据结构什么算法……

这其实是一种非常好的学习方法,回归到问题本身去思考问题,然后带着问题去找答案,而不是一上来就一头扎进代码中。

核心功能

如果你要实现一套机制保证线程互斥,核心是实现这六个功能:抢锁、释放锁、入队、出队、阻塞、唤醒。下面分别说下这六个功能如何实现以及在实现的时候需要考虑哪些特殊情况。

抢锁

如果当前没有任何线程持有锁,那进来的线程就可以去抢锁。

因为存在多个线程同时上锁,所以需要通过机制来保证同一时刻只能有一个线程能够操作锁标志位,这个机制就是CAS。

如果有线程持有了锁,这时候进来的线程如果是持有锁的线程,就发生了重入,需要记录重入次数,因为解锁的时候也要解锁相应的次数;如果不是持有锁的线程,那有两个选择:让线程运行结束、让线程阻塞等其他线程用完锁再唤醒运行。

显然,让线程运行结束不合适,因为线程该执行的任务还未执行。正确的做法肯定是先把这个线程保存下来,然后让线程阻塞。待持有锁的线程运行结束再唤醒执行。这里就是公平锁与非公平锁的唯一差异所在,一个临界点。

释放锁

持有锁的线程将该执行的任务执行完就得释放锁。当然肯定不是这么阶段,还得考虑锁重入、线程唤醒。

入队

未抢到锁且不是重入的线程就需要被阻塞等待唤醒,那唤醒的时候去哪找这个线程呢?很明显这些线程满足先阻塞先唤醒原则,与数据结构队列的FIFO特性相吻合。所以在实现的时候需要借助队列,将这部分线程封装成节点入队。

入队就得考虑从头部入队还是从尾部入队。如果队列中还没有数据,队列的头尾两个节点都指向这个节点。如果队列中有数据,就从尾部插入。

大家在看AQS源码的时候会发现AQS不是这样做的,它会多一个节点。AQS队列的头结点永远是当前持有锁的线程占用,为什么要这样做呢?这就需要对CPU内部结构及工作机制有深入理解,感兴趣的同学可以深入学习下:CLHLock、MCSLock。

出队

线程对应的节点何时出队呢?肯定是唤醒以后。那由谁来操作出队呢?是唤醒你的线程还是你自己?可想而知,由自己来操作更合理。那自己什么时候操作出队呢?肯定是唤醒以后。只有理解了这个逻辑,你才能看得懂AQS中阻塞那块的代码为什么那样写。

正常的情况下出队都是从头部出,但是有特殊情况会移除队列中部的节点或尾部节点。

阻塞

线程如果没有抢到锁依然在那尝试抢锁这就是所谓的自旋锁,很显然,这样很浪费资源,肯定没有抢到锁的线程执行完任务唤醒高效。那如何不占用资源呢?就是阻塞自己,让出资源,不被调度。

唤醒

未抢到锁的线程为了不占用资源阻塞了自己,拿到锁的线程执行完任务需要来唤醒,不然就会出现奇怪的现象:抢到锁的线程执行完任务退出了,未抢到锁的线程全部阻塞在那里等待唤醒。

如果你现在想问了解这些有什么用?我的答案是这些是思想,了解了这些才能看得懂AQS的源码。

AQS三大核心机制

别看AQS代码挺多,其实搞懂这三个机制,代码理解起来就非常easy。

state

这个属性就是锁标志位。如果有线程抢到了锁,这个值就会通过CAS改成1。何时改回来呢?持有锁的线程执行完任务释放锁的时候。

waitStatus

这个属性有五个值:0,-1,-2,-3,1。这里介绍其中三个。其他的值跟互斥没关系,我会在讲到相关的技术时讲到。这几个值直接的转换后面会写文章细致,本篇文章大致讲下。

未抢到锁的线程被封装成Node插入队列,这个属性默认值为0。

插入队列后会进行两次自旋,如果都没抢到锁,就会将它的前置节点的waitStatus改为-1。注意一下这里,改的不是自己的waitStatus,而是它前置节点的。为什么要这样做呢?这就是自旋锁算法CLH的理论。相当于在它的前置节点上设置了一个闹钟,这样在唤醒的时候就不需要去队列取数据,直接判断自己的该属性就可以了。

如果是队列中的第二个节点但是抢锁失败了,这个时候就将自己的waitStatus设置为1。这样的线程就得不到调度机会了,会被其他线程从队列中移除。

队列

如果想看懂AQS源码,必须对队列的相关操作算法非常熟悉,比如初始化、判空、入队、出队……建议在看AQS源码之前自己用Java实现一遍队列。

结语

这篇文章虽然没有讲任何AQS的源码,但是如果你真的看懂了这篇文章,你去读读AQS的源码,你会发现读起来非常轻松。

题外话

子牙手写JVM小班四期招生即将结束。四期新增了字节码增强 Agent,学完你就可以做JVM相关的工作,如二开arthas,自研类hsdb调试器、自研实现热更新热部署零侵入日志等黑科技…

四期完整课程包含七大专题 一个增值专题,约50多个课时。完整学完你就可以:1、用Java写一个Java虚拟机,从而深入理解运行系统的底层细节;2、有能力自行研究Hotspot源码及其他用C语言、C 写的中间件源码;3、能够用C语言、C 写任何你感兴趣的基础算法如:内存池、垃圾回收算法、主从同步算法、执行引擎、存储引擎;4、就有底子跟着我学习下半年准备开的操作系统内核班……

这套课程,横跨多个计算机学科,但只是一个学科的价格。这套课程,JVM专家、功力深厚、经验丰富的子牙老师亲授,跟我学习不踩坑,全网唯一教授虚拟机的课程…

感兴趣小伙伴可以加班班微信咨询(jvm-anan),真诚招生,无任何套路。课程试看,问题真诚解答,全部了解清楚再上车。一二三期共500多VIP加入,无一人退费,好评不断

0 人点赞