阅读AQS -- AbstractQueuedSynchronizer源码

什么是AQS?

具体的类位于 : java.util.concurrent.locks.AbstractQueuedSynchronizer

AQS是基于FIFO等待队列的,用来实现阻塞锁和相关同步机制的框架, 比如信号量(semaphroe), 事件(events), 可重入读写锁之类的,都是用这个实现的。其内部是使用原子int来表示状态的。子类需要覆盖其中的protected方法来实现同步机制。

AQS同时支持排他模式和共享模式。排他模式情况下,如果一个线程已经有锁了,其他线程是无法获取到锁的。共享模式则其他线程可能可以获取到锁。AQS本身不理解这些模式,而是需要开发人员在实现类中决定,在已经有线程获取到锁的情况下,正在等待的下一个线程应不应该获取到锁。

用法

需要重写以下方法

  • tryAcquire
  • tryRelease
  • tryAcquireShared
  • tryReleaseShared
  • isHeldExclusively

具体用

  • 用 getState 获取当前的状态,
  • 用 setState 或 compareAndSetState 更新当前的状态

这些方法默认的实现是抛出异常 UnsupportedOperationException, 同时要注意重写的时候应该是线程安全,并且通常很简短且非阻塞。重写这些方法是唯一的使用方式,其他的方法都被声明为了final,因为他们都是相互关联的(一个方法的调用涉及到其他的方法调用,才能保证代码实现想要的效果)所以随便改可能会出bug。

核心的同步机制主要是以下形式

获取锁:

while (!tryAcquire(arg)) {//尝试获取锁
   //获取失败的时候
   //如果线程还未入队列则入队列
   //根据需要可以阻塞当前线程
}

释放锁:

if (tryRelease(arg)) { //如果释放锁成功
	//唤醒等待队列中的第一个线程
}

共享模式是类似的,不过可能涉及到级联唤醒, 也就是说可能需要唤醒队列中多个等待中的线程

Node

等待队列是CLH(Craig, Landin, 和 Hagersten)锁队列的变体。CLH锁通常用于自旋锁。当前节点保存了其后继节点的控制信息。其中的status变量标识了一个线程是否要被阻塞。当一个节点的前驱节点被释放的时候,它会被通知(signaled)。队列中的每个节点表示某种特定通知类型的监视器,持有一个等待队列。但是status成员变量不控制线程是否会获取到锁。头结点不代表就一定会获取锁,只是说有机会去竞争锁而已。

入队列就是队列尾添加一个节点,出队列就是直接获取头结点。

         +------+  prev +-----+       +-----+
    head |      | <---- |     | <---- |     |  tail
         +------+       +-----+       +-----+

插入队列只需要对 tail 节点进行一个原子操作, 所以会有简单的原子点界定未入队列和已入队列,类似的,出队列涉及到 head 节点的操作。然后,为了确定节点的后继节点,需要额外的工作,部分是因为要处理由于超时或者中断导致的取消。

“pre” 指针是 CLH 队列原来没有的, 主要是用来处理取消逻辑的. 如果一个节点被取消了,需要把其后继节点连接到要取消节点的前驱节点(就是链表删除节点的逻辑)

“next” 指针用来实现阻塞机制, 每个节点都保存了thread id, 所以前驱节点根据 next 指向的节点来决定唤醒哪个线程。

整体的思路

以 java.util.concurrent.locks.ReentrantLock.Sync 为例可以看看AQS是怎么工作的, Sync 是是一个抽象类, 继承子AQS, 本身只留下 lock 一个方式未实现。

AQS内部有一个变量 state

  • state = 0 的时候,表示锁没有被获取,可以加锁
  • state 大于 0 的时候, 表示某个线程持有了锁(ownerThread)

注: state是被声明为 volatile 的, 但是 ownerThread 不需要是volatile的, 因为代码里面是判断了 state > 0 才会去使用 ownerThread.

加锁(state + acquires)

acquire 是一个 int 值, 从参数传过来的, (比如 ReentrantLock 里面用的是 1)

  1. 首先判断是否是未锁定状态 (state = 0)
  2. 如果是未锁定状态(state = 0), 则使用 CAS 操作更新 state 为 acquires, 如果成功, 则认为是获取锁成功, 保存当前线程为 ownerThread ; 如果获取失败, 则入队列并挂起, 等待唤醒
  3. 如果是已经锁定的状态(state > 0), 则判断当前线程是不是 ownerThread, 如果是的话, 更新 state = state + acquires, 这也就意味着, 锁是可以重入的, 而且加锁了几次,就需要释放(state - acquires)几次,这样才能使 state 回到 0, 也就是为加锁的状态
  4. 实现上是调用 AQS.acquire(args), 伪代码如下
function AQS.acquire(args) {
	(! tryAcquire(args)) && 入队列
}

function lock(args) {
	AQS.acquire(args)
}


上述截图代码是给非公平锁用的, 因为公平锁还需要判断队列中是否有等待的线程

释放(state - release)

release是一个 int 值, 从参数传过来的, (比如 ReentrantLock 里面用的是 1)

  1. 判断当前线程是否是 ownerThread,如果不是的话, 抛IllegalMonitorStateException 异常
  2. 更新 state = state - acquires, 如果 state 为 0, 则更新 ownerThread 为 null, 并出队列

在这里插入图片描述

获取非公平锁

以 java.util.concurrent.locks.ReentrantLock.NonfairSync 为例

直接使用CAS更新 state, 更新成功就设置 ownThread 为自己, 否则就调用上面的 AQS.acquire(args), 其中就包含了入队列的逻辑

在这里插入图片描述

获取公平锁

以 java.util.concurrent.locks.ReentrantLock.FairSync 为例

相比于获取非公平锁的简单粗暴, 一上来就直接CAS state, 公平锁会先去判断队列中是否有等待的线程,如果有的话,就不进行CAS尝试, 除非已经持有锁, 否则会将当前线程入等待队列.

在这里插入图片描述

AbstractQueuedSynchronizer 的方法

获取共享锁

acquireShared

acquireShared
调用 tryAcquireShared ,如果小于0 (也就是没有没有成功获取到锁), 则调用 doAcquireShared

tryAcquireShared (等待子类实现的protected方法)

尝试去获取共享锁, 这个方法应该检查当前对象的状态是否允许获取锁, 允许了才去获取锁。

线程会调用这个方法去获取共享锁,如果获取锁失败(返回值为负数或者抛异常),那么线程会被入队列(如果还没有入队列的话)直到被其他线程唤醒。

tryAcquireShared 是 protected 的,需要子类去实现,默认是抛出 UnsupportedOperationException 异常。

返回值int类型,

  • 负数表示获取失败
  • 0 表示获取锁成功,但是后续的获取共享锁会失败
  • 1 表示获取锁成功,后续的获取共享锁可能会成功,这种情况下需要检查后续线程的有效性

doAcquireShared (执行获取锁的逻辑,同时负责更新队列)

在这里插入图片描述

流程说明
1. 把node添加到链表的最后(addWaiter),然后开始循环:
1.1 获取节点的前驱,如果前驱是 head 节点,则调用 tryAcquireShared
1.1.1 返回值大于等于 0 则说明获取成功了,那就把当前节点作为头头结点并设置 r ( setHeadAndPropagate(node, r)),这意味着把前驱节点也就是之前的头结点出队列了,当前节点变成了头结点,然后跳出循环
1.2 前驱不是头结点或者头结点尝试获取锁失败,调用 shouldParkAfterFailedAcquire 检查是否应该挂起
1.2.1 如果前一个节点的waitStatus为Node.SIGNAL(-1), 则直接返回true, 应该挂起;
1.2.2 如果前一个节点的waitStatus大于0 ,也即是 Node.CANCEL(1)或其他, 则循环去掉前面所有WaitStatus大于 0 的链表的节点, 然后返回false, 不挂起
1.2.3 如果前一个节点的waitStatus小于0 但是不是 Node.SIGNAL,则应该是 0 或者 PROPAGATE, 表示应该至少唤醒1次,使用cas把节点的状态改成 Node.SIGNAL, 然后返回false不挂起
1.3 如果应该挂起,则调用 parkAndCheckInterrupt(), 其中调用了LockSupport.park(thread) 挂起队列,然后返回 thread.interrupted() 状态, 改变了循环里面的变量, 从而通过调用 selfInterrupt 用另一种方式跳出了循环.

addWaiter(Node mode)

参数mode是选择独占还是共享,常量在Node类中定义。
tail不为空的时候,首先尝试cas插入到tail后面(所谓的 fast path), 如果失败了或者tail为空则调用 enq进行循环cas。

enq(Node node)

新节点入队列,如果tail为null, 则head也是为null, 所以直接设置 tail 和 head 为新的节点(new Node 而不是参数的那个), 使用的是cas操作;如果tail不为null,则在末尾插入参数的node(也是cas操作)。然后进行循环直到节点入队列成功。返回值返回的是原来的tail节点而不是参数中的node。

释放共享锁

在这里插入图片描述

releaseShared

在这里插入图片描述
共享模式下的释放锁, 如果 tryReleaseShared返回true, 则可能会取消阻塞1个或多个线程。

tryReleaseShared

同样需要子类去实现, 返回是 boolean,如果允许释放则返回true, 否则返回false

doReleaseShared

共享模式下的释放 – 唤醒后继节点并确保Propagation(传播)
在这里插入图片描述

0. 进入循环
1. 如果头节点的状态是 Signal (-1)的话, 尝试更新Signal为0
1.1 如果更新成功, 则调用 unparkSuccessor 唤醒 head 节点的下一个节点;
1.2 如果更新waitStatus失败,则说明有其他线程修改了head的状态,则重新循环(continue)
2. 否则如果头结点的状态是 0 的话, 尝试更新为 Propagate, 如果失败,则重新循环.
3. 如果循环开始时候的获取的 head 和 现在的 head 节点是同一个的话, 则结束循环, 否则继续循环。因为循环的过程中可能头结点已经被其他线程改变了,所以如果被改变的话,需要重新循环。

unparkSuccessor(node)

唤醒node节点的后继节点,如果存在的话。如果node的waitStatus小于0 ,则设置为0(0表示未被锁定)。一般来说要唤醒的节点就是node的后继节点,但是节点有可能被取消获取为null,所以要从tail往前去找到实际的未被取消的后继节点。
唤醒调用的是 LockSupport.unpark(s.thread);
在这里插入图片描述

waitStauts小于0的情况是有以下3种waitStatus小于0的情况

独占锁

获取独占锁

在这里插入图片描述
调用 tryAcquire 方法, 这个方法是子类实现的,默认是抛异常, 如果失败了的话, 线程入队列

aquireQueued (独占模式的线程入队列)

在这里插入图片描述
直接开始循环, 如果新加节点的前驱节点是 head 节点, 则再次尝试获取锁(因为前驱是头结点的话, 说明下一个要唤醒的就是自己了,头结点本身不保存线程); 如果前驱不是头结点, 或 是头结点但是获取锁失败, 则检查是否应该挂起, 这部分和获取共享模式那里是一样的。

释放独占锁

在这里插入图片描述
调用 tryRelease, 如果成功, 则唤醒头结点的后继节点.

lock,tryLock,lockInterruptibly 的区别

A线程已经获取到锁的情况下, B线程尝试获取锁

方法返回值是否阻塞阻塞期间线程中断(调用interrupte)
lockvoid不抛异常
lockInterruptiblyvoid抛异常
tryLockboolean
tryLock(time)boolean最多阻塞time时间抛异常

todo

ConditionObject 是什么?

todo

readObject ?

这个类序列化的时候,只能保存原子状态变量, 反序列化之后的类Thread队列都会丢失。子类需要实现readObject方法来将其还原到初始状态。

todo

hasQueuedPredecessors?

专门给公平锁用的
todo

如何实现公平锁 (todo)

冲突(Barging)

因为获取锁操作是在入队列之前, 所以一个新的要来获取锁的线程可能会和已经在队列中等待的线程抢锁, 所以可能导致不公平。为了实现公平机制,通常可以重写 tryAcquire 或/和 tryAquireShared , 让其在hasQueuedPredecessors (一个专门为公平锁设计的方法) 返回 true 的时候, 返回false。

acquireSharedInterruptibly

如果Thread.interrupted 为true,抛出 InterruptedException 异常,然后调用 tryAcquireShared(arg) , 如果小于 0 (没有获取到锁) 则调用 doAcquireSharedInterruptibly(arg);
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值