JDK14的AbstractQueuedSynchronizer(AQS)源码解析

介绍

基于JDK14的源码进行解析,需要看过源码后,再来理解本文会简单很多。

doc文档说明

AQS类提供了框架来实现阻塞锁和相关的同步器FIFO等待队列,用AtomicInteger代替state,继承该类必须保护好state,state代表的是获取锁(acquire)或释放锁(release)。其他的方法都是入队列和阻塞机制。

Node结点有三种状态:

WAITING   = 1;          // must be 1
CANCELLED = 0x80000000; // must be negative
COND      = 2;          // in a condition wait

处于head结点的,是持有资源的线程。

方法

JDK14流程
enqueue方法:for循环 + cas的方式,入队列。
isEnqueued方法:从链表尾部查找node,若有返回true。

获取排他锁流程

acquire方法:先调用tryAcquire获取锁,若返回false,则调用acquire(私有)方法。

acquire(私有)方法:
重复干如下事情:

  • 检查当前的前继结点是否是头结点,如果是,确保头结点稳定,否则确保有效的前继结点
  • 如果前继结点是头结点,或尚未加入队列,则尝试获取锁,若获取锁成功则结束
  • 如果结点尚未创建,则创建它;否则,如果结点尚未入队,请尝试一次入队;否则从park中唤醒,重试(到postSpin时间);否则,如果等待状态未设置,则设置并重试;否则暂停当前并清除等待状态,并检查取消;

若超时,则调用cancelAcquire取消当前结点。

不是
不是
成功
失败
不是
为空
不为空
不是
不是
不是
不是
没有
超时
被unpark
获取锁流程
检查是否该结点的前继结点是否是头结点
检查前继结点状态是否小于0
调用cleanQueue方法清空小于0的结点
检查前继结点的前继结点是否为null
让CPU自旋一会
前置结点为null或前置结点为head结点
尝试获取锁
持有该资源
若结点为空则创建结点
前置结点为null
cas入队列
前置结点为头结点且已经再次获取资源失败
若当前结点的状态为0
设置状态为WAITING
是否设置过超时时间
线程park
是否超时
调用cancelAcquire取消当前结点
设置状态为0

cancelAcquire方法:设置当前node状态为WAITING状态,调用cleanQueue方法。
cleanQueue方法:清空所有状态为CANCELLED的node结点,并调用signalNext方法尝试unpark下一个结点。
signalNext方法:唤醒该结点的后继结点,若后继结点状态不等于0,则unpark,它从acquire(私有)方法里面执行清空等待状态然后重复去申请一次资源看是否能成功。不用担心后继结点为CANCELLED状态,因为后续发现这个状态会调用cleanQueue方法清空所有状态为CANCELLED的node结点。而且CANCELLED状态只有线程持有state时候,才回去调用cleanQueue,所以不用考虑并发。

释放排它锁流程

release方法:调用自定义的tryRelease释放资源,若为true,则调用signalNext方法唤醒该结点的后继结点。

获取共享锁流程

和排它锁类似,只要tryAcquireShared(arg) >= 0就能拿到共享锁。
signalNextIfShared方法:signalNext方法类似,只是多了个判断是否是共享锁对象的条件。

ConditionObject

该类需要在AQS中使用,类似于wait,sleep,该类维护着一个单向链表ConditionNode。只用了排它锁。

await方法

创建一个ConditionNode结点,若当前结点的等待线程是该锁,设置状态为00000011(COND | WAITING),添加到链表中,释放当前锁。若当前线程已经有结点进入等待队列了,等待队列中结点释放,然后清空状态为0,入等待队列。

singal方法

遍历ConditionObject链表,若遍历时对应的结点的状态是3,即00000011(代表着该结点已经处于等待状态),通过cas方式入等待队列。

jdk实现类

CountDownLatch(共享锁)

构造函数中先设置初始状态为入参。

  • 获取锁的时候,state减一,直到为0。
  • 释放锁的时候,判断state是否为0,为0才允许释放。

使用场景例子: 一个任务的某个步骤需要很多小任务,主任务就相当于主线程,分出来各个小任务给其他线程,就可以使用CountDownLatch等待所有的小任务都执行完毕,主任务再往下执行。

Semaphore(共享锁)

构造函数中先设置初始状态为入参。有着公平锁和非公平锁实现类。
公平锁:直接先去竞争state,竞争失败入队列。
非公平锁:如果有前驱结点在队列中,直接入队列。

使用场景:Semaphore可以用于做流量控制,特别公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发的读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有十个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,我们就可以使用Semaphore来做流控。

ThreadPoolExecutor.Worker(排它锁)

一个Worker线程对应一个等待队列。

ReentrantLock(排它锁)

前置条件,可重入锁,若发现持有者和当前线程一致,state++,然后执行公平锁和非公平锁的逻辑。
公平锁:直接先去竞争state,竞争失败入队列。
非公平锁:如果有前驱结点在队列中,直接入队列。

ReentrantReadWriteLock(排它锁和共享锁)

公平锁:直接先去竞争state,竞争失败入队列。
非公平锁:如果有前驱结点在队列中,直接入队列。
使用的是同一个等待队列,共用一个state。

WriteLock(排它锁、可重入)

获取锁流程:

  • 如果state不为0
    • state和65535与运算等于0,进入等待队列。
    • 当前线程和持有线程不一致,进入等待队列。
    • 如果state + acquires大于65535,抛出异常。
    • 以上都没有,state设置为state + acquires,继续持有该锁。
  • 否则说明当前没有资源的竞争
    • 如果是非公平锁,直接获取锁,若成功持有锁,若失败则进入等待队列。
    • 如果是公平锁且有前驱结点在队列中,直接入队列;否则持有锁。

释放锁流程,和前面大体一致。

ReadLock(共享锁)

维护着一个HoldCounter的ThreadLocal,用于记录每个线程所重入的次数。
获取锁流程:

  • 如果有WriteLock锁且持有锁者与当前线程不一致,则进入等待队列。
  • 调用~readerShouldBlock方法,
    • 如果是非公平锁且head的后继结点不为共享锁,继续流程。
    • 如果是公平锁且有没有前驱结点在队列中,继续流程。
    • HoldCounter++,如果是第一个读线程,记录该结点。
    • 继续持有锁,结束流程。
  • 如果有WriteLock锁且持有锁者与当前线程不一致,则进入等待队列。
  • 调用readerShouldBlock方法
    • 如果是非公平锁且head的后继结点不为共享锁,继续流程。
    • 如果是公平锁且有没有前驱结点在队列中,继续流程。
    • 如果第一个读线程是当前线程,继续流程。
    • 如果当前读线程所重入次数==0,进入等待队列,结束流程。
  • HoldCounter++,如果是第一个读线程,记录该结点。

释放锁流程,HoldCounter–,如果是第一个读线程,清除该结点,等待state == 0时,才会释放所有读线程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章