作者:薛8
来源:https://juejin.im/post/5c890b4a51882501351d5929
AQS简介
AbstractQueuedSynchronizer
抽象队列同步器,简称为AQS
,可用于构建阻塞锁或者其他相关同步器的基础框,是Java并发包的基础工具类。通过AQS
这个框架可以对同步状态原子性管理、线程的阻塞和解除阻塞、队列的管理进行统一管理。
AQS
是抽象类,并不能直接实例化,当需要使用AQS
的时候需要继承AQS
抽象类并且重写指定的方法,这些重写方法包括线程获取资源和释放资源的方式(如ReentractLock通过分别重写线程获取和释放资源的方式实现了公平锁和非公平锁),同时子类还需要负责共享变量state的维护,如当state为0时表示该锁没有被占,大于0时候代表该锁被一个或多个线程占领(重入锁),而队列的维护(获取资源失败入队、线程唤醒、线程的状态等)不需要我们考虑,AQS
已经帮我们实现好了。AQS
的这种设计模式采用的正是模板方法模式。
总结起来子类的任务有:
- 通过
CAS
操作维护共享变量state
。 - 重写资源的获取方式。
- 重写资源释放的方式。
如果对CAS和Java内存模型还不清楚的,建议先了解这两者之后再食用本文,效果更佳!CAS原理分析及ABA问题详解 什么是Java内存模型?
完成以上三个任务即可实现自己的锁。
AQS
作为J.U.C
的工具类,面向的是需要实现锁的实现者,而锁面向的是锁的使用者,这两者的区别还是需要搞清楚的。
AQS数据结构
先看AQS
有哪些重要的成员变量。
1 | // 头结点,你直接把它当做 当前持有锁的线程 可能是最好理解的 |
然后再看看AQS
的内部结构,AQS
内部数据结构为一个双向链表和一个单向链表,双链表为同步队列,队列中的每个节点对应一个Node
内部类,AQS
通过控制链表的节点而达到阻塞、同步的目的,单链表为条件队列,可以把同步队列和条件队列理解成储存等待状态的线程的队列,但是条件队列中的线程并不能直接去获取资源,而要先从条件队列转到同步队列中排队获取,同步队列的唤醒结果是线程去尝试获取锁,而条件队列的唤醒结果是把线程从条件队列移到同步队列中,一个线程要么是在同步队列中,要么是在条件队列中,不可能同时存在这两个队列里面。
Java阻塞状态和等待状态的线程从Linux内核来看,都是阻塞(等待)状态,它们都会让出CPU时间片。Java为了方便管理线程将“阻塞(等待)”状态细分成了阻塞状态和等待状态,这两个状态的区别在于由谁去唤醒,是操作系统还是其他线程。Java线程请求某一个资源失败的时候就会进入阻塞状态,处于阻塞态的线程会不断请求资源,一旦请求成功,就会进入就绪队列,等待执行。而当线程调用
wait
、join
、pack
函数时候会进入等待状态,需要其它线程显性的唤醒否则会无限期的处于等待状态。
Java线程6状态图:
内部类Node
详解:
1 | static final class Node { |
每个线程都关联一个节点,节点的状态也代表着线程的状态,AQS
通过对同步队列的管理而达到对线程的管理。
AQS的功能
AQS
提供了2
大功能,基于双链表的同步队列和基于单链表的条件队列,同步队列维护的是阻塞状态的线程对应的节点,这些线程都是阻塞着排队获取锁的,条件队列维护的是等待状态的线程对应的节点。
同步队列
AQS
提供了两种方式去获取资源,分别是共享模式和独占模式,但是一般锁只会去继承其中一种模式,不会在一个锁里同时存在共享模式和独占模式两种模式。
资源指锁、IO、Socket等
当一个线程以共享模式或独占模式去获取资源的时候,如果获取失败则将该线程封装成Node
节点(同时将该节点标识为共享模式或独占模式)加入到同步队列的尾部,AQS
实时维护着这个同步队列,这个队列以FIFO(先进先出)来管理节点的排队,即资源的转移(获取再释放)的顺序是从头结点开始到尾节点。
共享模式和独占模式去获取、释放资源都分别对应着一套API
,以下分别分析这两套API
独占模式即获取资源的排他锁,共享模式及获取资源的共享锁。
独占模式
独占模式即一个线程获取到资源后,其他线程不能再对资源进行任何操作,只能阻塞获得资源。
获取资源
- 线程调用子类重写的
tryAcquire
方法获取资源,如果获取成功,则流程结束,否则继续往下执行。 - 调用
addWaiter
方法(详细过程看下面的源码解析),将该线程封装成Node节点,并添加到队列队尾。 - 调用
acquireQueued
方法让节点以”死循环”方式进行获取资源,为什么死循环加了双引号呢?因为循环并不是一直让节点无间断的去获取资源,节点会经历 获取资源->失败->线程进入等待状态->唤醒->获取资源……,线程在死循环的过程会不断等待和唤醒,节点进入到自旋状态(详细过程看下面的源码解析),再循环过程中还会将标识为取消的前驱节点移除队列,同时标识前驱节点状态为SIGNAL。 - 线程的等待状态是通过调用
LockSupport.lock()
方法实现的,这个方法会响应Thread.interrupt
,但是不会抛出InterruptedException异常,这点与Thread.sleep
、Thread.wait
不一样。
可以看到节点和节点之间在自旋过程中除了前驱节点会唤醒该节点之外基本不会互相通讯
源码分析:
1 | public final void acquire(int arg) { |
释放资源
- 线程调用子类重写的
tryRelease
方法进行释放资源,如果释放成功则继续检查线程(节点)的是否有后继节点,有后继几点则去唤醒。 - 调用
unparkSuccessor
方法进行后继节点的唤醒,如果后继节点为取消状态,则从队列的队尾往前遍历,找到一个离节点最近且不为取消状态的节点进行唤醒,如果后继节点不为取消状态则直接唤醒。
源码解析:
1 | public final boolean release(int arg) { |
共享模式
共享模式下,线程无论是获取资源还是释放资源,都可能会唤醒后继节点。
获取资源
- 调用子类重写的
tryAcquireShared
方法进行资源获取,获取失败则调用doAcquireShared
将线程封装Node节点加入到同步队列队尾。 - 调用
doAcquireShared
方法让节点以”死循环”方式进行获取资源,为什么死循环加了双引号呢?因为循环并不是一直让节点无间断的去获取资源,节点会经历获取资源->失败->线程进入等待状态->唤醒->获取资源……,线程在死循环的过程会不断等待和唤醒,节点进入到自旋状态(详细过程看下面的源码解析)。如果线程节点被唤醒后,且获取资源成功,且后继节点为共享模式,那么会唤醒后继节点……唤醒会一直传递下去,直到后继节点不是共享模式,唤醒的节点同样会去获取资源,这点和独占模式不一样。
共享模式资源的获取和独占模式资源的获取流程差不多,就是在获取资源成功后,会唤醒为共享模式的后继节点,然后被唤醒的后继节点也去获取资源。
1 | public final void acquireShared(int arg) { |
释放资源
- 调用子类重写的
tryReleaseShared
方法释放资源,释放成功则调用doReleaseShared
方法进行后继节点的唤醒。 - 如果后继节点为共享模式,则持续唤醒。
共享模式下资源释放流程和独占模式下资源释放的流程差不多,就是在释放后唤醒后继为共享模式的节点,且唤醒的动作是传播下去的,直到后继节点出现不是共享模式的,这个唤醒的过程和共享模式的获取资源的唤醒过程一样。
1 | //调用子类重写的tryReleaseShared方法进行以共享模式释放资源,释放失败则调用doReleaseShared。 |
条件队列
条件队列又称等待队列、条件队列等,条件队列的实现是通过ConditionObject
的内之类来完成的,,一开始就介绍了同步队列条件队列的去,不过这里再啰嗦一下,可以把同步队和条件队列理解成储存等待状态的线程的队列,条件队列中的线程并不能直接去获取资源,而要先从条件队列转到同步队列中排队获取,一个线程要么是在同步队列中,要么是在条件队列中,不可能同时存在这两个队列里面。
1 | /* |
可以看到,其作用与Object原生的wait()/notify()/notifyAll()很相似,但是增加了更多的功能。下面以awaitUninterruptibly()、signal()为例,阐述一下其内部实现。
同步队列和条件队列的关系
线程执行condition.await()
方法,将节点1从同步队列转移到条件队列中。
线程执行condition.signal()
方法,将节点1从条件队列中转移到同步队列。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。