AbstractQueuedSynchronizer 源码分析
标签: AbstractQueuedSynchronizer 源码分析 Redis博客 51CTO博客
2023-04-02 18:23:34 131浏览
1. AQS 介绍
1.1 AQS介绍
AQS 是 AbstractQueuedSynchronizer 的简称。AQS 提供了一种实现阻塞锁和一系列依赖 FIFO 等待队列的同步器的框架,如下图所示。AQS 为一系列同步器依赖于一个单独的原子变量(state)的同步器提供了一个非常有用的基础。子类们必须定义改变 state 变量的 protected 方法,这些方法定义了state 是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的 state 变量,但是为了保证同步,必须原子地操作这些变量。
使用一个被 volatile 关键字修饰的 int 类型的 state 变量表示同步状态
通过内置的 FIFO 队列 CLH 完成资源获取的排队工作, 将排队线程对象封装为 Node
1.2 AQS 中的 Node 节点介绍
下面我们来看看 Node 节点各个字段的含义
waitStatus:存储节点的状态,可能的值有
0:同步队列节点初始化时的状态,默认为0
CANCELLED:表示当前节点的线程被中断或者因超时而取消操作,节点为该状态时不可再更新为其他状态了
SIGNAL:表示当前节点需要去唤醒下一个节点
CONDITIOIN:表示该节点属于等待状态(仅在条件队列中使用该状态)
PROPAGATE:表示下一个被获取对象应该无条件传播的状态(仅在共享模式时使用)
prev:存储当前节点上一个节点(前任节点或前驱节点)的指针反向遍历时可以根据尾节点向上遍历
next:存储当前节点下一个节点(后继节点)的指针
thread:存储线程信息
nextWaiter:存储节点的指针,可能出现两种情况
同步队列中,通过将该值设置成SHARED或EXCLUSIVE来区分该节点处于独占模式还是共享模式
条件队列中,用于存储下一个节点的指针(所以条件队列中使用的是单向链表,只能根据节点找到下一节点,无法反向遍历)
Node节点还提供了以下两个方法,以便在AQS中能更方便的使用Node
isShared():判断当前节点是否处于共享模式
predecessor():获取该节点的前任节点
通过 cas 改变 state 值
//返回当前值
protected final int getState() {
return state;
}
//设置同步值
protected final void setState(int newState) {
state = newState;
}
//原子操作 CAS
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS 同时提供了 互斥模式(exclusive)和共享模式(shared) 两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式(如: ReentrantLock、Semaphore ),当然也有同时实现两种模式的同步类,如ReadWriteLock。
2. AQS 和 ReetrantLock 的关系
AQS 是 JUC 实现锁/同步器实现的模板, 这里是一种典型的模板方法模式(行为型设计模式),使用者需要继承这个AQS并重写指定的方法,最后调用AQS提供的模板方法,而这些模板方法会调用使用者重写的方法。
3. ReentrantLock 重入锁
重入锁简单来说一个线程可以重复获取锁资源,虽然ReentrantLock不像synchronized关键字一样支持隐式的重入锁,但是在调用lock方法时,它会判断当前尝试获取锁的线程,是否等于已经拥有锁的线程,如果成立则不会被阻塞(下面讲源码的时候会讲到)。
还有ReentrantLock在创建的时候,可以通构造方法指定创建公平锁还是非公平锁。这里是个细节部分,如果知道有公平锁和非公平锁,但是不知道怎么创建,这样还敢说看过源码?
// ReentrantLock 构造方法
// 默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 传入true,创建公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
怎么理解公平锁和非公平锁呢?
先对锁进行获取的线程一定先拿到锁,那么这个锁是公平的,反之就是不公平的。
4. ReentrantLock 加锁过程
4.1 加锁和解锁流程
下面我们再来一起看看具体的实现
4.2 lock方法
以下就是一个简单锁的演示了,简单的加锁解锁。
public class ReentrantLockTest {
public static void main(String[] args) {
// 创建公平锁
ReentrantLock lock = new ReentrantLock();
// 加锁
lock.lock();
hello();
// 解锁
lock.unlock();
}
public static void hello() {
System.out.println("Say Hello World!");
}
}
既然我们是看加锁的过程,就从lock方法开始,点进去之后看到了调用了sync对象的lock方法,sync是我们ReentrantLock中的一个内部类,并且这个sync继承了AQS这个类。
public void lock() {
sync.lock();
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 抽象方法,由公平锁和非公平锁具体实现
abstract void lock();
// ..... 代码省略
}
有两个类对Sync中的lock方法进行了实现,我们先看非公平锁:NonfairSync
lock方法最后调用了acquire方法,并且传入了一个参数,值为:1
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
AQS 的 acquire 方法代码如下:
public final void acquire(int arg) {
// 第一个调用了tryAcquire方法,这方法判断能不能拿到锁
// 强调,这里的tryAcquire的结果,最后是取反,最前面加了 !运算
if (!tryAcquire(arg) &&
// 这里有两个方法 addWaiter 和 acquireQueued
// addWaiter 是入队
// acquireQueued 是获取锁和进入等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.3 tryAcquire方法
从acquire()点击tryAcquire方法进去看,AQS为我们提供了默认实现,默认如果没重写该方法,则抛出一个异常,这里就很突出模板方法模式这种设计模式的概念,提供了一个默认实现。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
同样,我们查看非公平锁的实现
最后来到了 NonfairSync 对象中的tryAcquire方法
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 尝试获取锁 拿到锁了返回:true,没拿到锁返回:false
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
然后会调用 父类 Sync 的 nonfairTryAcquire 方法
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//case 修改为 1
if (compareAndSetState(0, acquires)) {
// 获取锁成功记录获取到锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 锁重入判断
else if (current == getExclusiveOwnerThread()) {
// 重入次数累计
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 修改累计值
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
// ....
}
tryAcquire方法执行完成,又回到这里:tryAcquire方法拿到锁返回结果:true,没拿到锁返回:false。
一共分两种情况:
- 第一种情况,拿到锁了,结果为true,通过取反,最后结果为false,由于这里是&&运算,后面的方法则不会进行,直接返回,代码正常执行,线程也不会进入阻塞状态。
- 第二种情况,没有拿到锁,结果为false,通过取反,最后结果为true,这个时候,if判断会接着往下执行,执行这句代码:acquireQueued(addWaiter(Node.EXCLUSIVE), arg),先执行addWaiter方法。
public final void acquire(int arg) {
// tryAcquire执行完,回到这里
if (!tryAcquire(arg) &&
// Node.EXCLUSIVE 这里传进去的参数是为null,在Node类里面
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.4 addWaiter方法
我们AQS里面有个head、tail,以及Node子类,AQS在初始化的时候,数据大概是这个样子的,这个时候队列还没初始化的状态,所以head、tail都是为空。
addWaiiter这个方法总的来说做了什么事呢?
核心作用:把没有获取到的线程包装成Node节点,并且添加到队列中
具体逻辑分两步,判断队列尾部节点是不是为空,为空就去初始化队列,不为空就维护队列关系。
private Node addWaiter(Node mode) {
// 因为在AQS队列里面,节点元素是Node,所以需要把当前类包装成一个node节点
Node node = new Node(Thread.currentThread(), mode);
// 把尾节点,赋值给pred,这里一共分两种两种情况
Node pred = tail;
// 判断尾部节点等不等于null,如果队列没有被初始化,tail肯定是个空
// 反而言之,如果队列被初始化了,head和tail都不会为空
if (pred != null) {
// 整个就是维护链表关系
// 把当前需要加入队列元素的上一个节点,指向队列尾部
node.prev = pred;
// CAS操作,如果队列的尾部节点是等于pred的话,就把tail 设置成 node,这个时候node就是最后一个节点了
if (compareAndSetTail(pred, node)) {
// 把之前尾部节点的next指向最后的的node节点
pred.next = node;
return node;
}
}
// 初始化队列
enq(node);
return node;
}
什么情况下不会被初始化呢?
1、线程没有发生竞争的情况下,队列不会被初始化,由tryAcquire方法就可以体现出,如果拿到锁了,就直接返回了。
2、线程交替执行的情况下,队列不会被初始化,交替执行的意思是,线程执行完代码后,释放锁,线程二来了,可以直接获取锁。这种就是交替执行,你用完了,正好就轮到我用了。
4.5 enq方法
这个方式就是为了初始化队列,参数是由addWaiter方法把当前线程包装成的Node节点。
// 整个方法就是初始化队列,并且把node节点追加到队列尾部
private Node enq(final Node node) {
// 进来就是个死循环,这里看代码得知,一共循环两次
for (;;) {
Node t = tail;
// 第一次进来tail等于null
// 第二次进来由于下面代码已经把tail赋值成一个为空的node节点,所以t现在不等于null了
if (t == null) {
// CAS把head设置成一个空的Node节点
if (compareAndSetHead(new Node()))
// 把空的头节点赋值给tail节点
tail = head;
} else {
// 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
node.prev = t;
// CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
if (compareAndSetTail(t, node)) {
// 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
t.next = node;
return t;
}
}
}
}
通过enq代码我们可以得知一个很重要、很重要、很重要的知识点,在队列被初始化的时候,知道队列第一个元素是什么么?如果你认为是要等待线程的node节点,那么你就错了。
通过这两句代码得知,在队列初始化的时候,是new了一个空Node节点,赋值给了head,紧接着,又把head 赋值给tail。
if (compareAndSetHead(new Node()))
// 把空的头节点赋值给tail节点
tail = head;
初始化完成后,队列结构应该是这样子的。
队列初始化后,紧接着第二次循环对不对,t就是我们的尾部节点,node就是要被加入队列的node节点,也就是我们所谓要等待的线程的node节点,这里代码执行完后,直接return了,循环终止了。
// 第二次循环就走到这里,先把需要加入队列的上一个节点指向队列尾部
node.prev = t;
// CAS操作判断尾部是不是t如果是,则把node设置成队列尾部
if (compareAndSetTail(t, node)) {
// 再把之前链表尾部的next属性,连接刚刚更换的node尾部节点
t.next = node;
return t;
}
记住,这里队列初始化的时候,第一个元素是空,队列里面存在两个元素。
好了,最终addWaiter方法会返回一个初始化并且已经维护好,队列关系的Node节点出来。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// addWaiter返回Node,紧接着调用acquireQueued 方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4.5 acquireQueued方法
既然没拿到锁,就让线程进入阻塞状态,但是肯定不是直接就阻塞了,还需要经过一系列的操作,看源码:
// node == 需要进队列的节点、arg = 1
final boolean acquireQueued(final Node node, int arg) {
// 一个标记
boolean failed = true;
try {
// 一个标记
boolean interrupted = false;
// 又来一个死循环
for (;;) {
// 获取当前节点的上一个元素,要么就是为头部节点,要么就是其他排队等待的节点
final Node p = node.predecessor();
// 判断是不是头部节点,如果是头部节点则代表当前节点是排队在第一个,有资格去获取锁,也就是自旋获取锁
// 如果不是排队在第一个,前面的人还在排队后面的就更别说了去获取锁了
// 就比如食堂打饭,有人正在打饭,而排队在第一个人的可以人,可以去看看前面那个人打完了没
// 因为他前面没有没人排队,而后面的人就不同,前面的人还在排队,那么自己就只能老老实实排队了
if (p == head && tryAcquire(arg)) {
// 如果能进入到这里,则代表前面那个打饭的人已经搞完了,可以轮第一个排队的人打饭了
// 既然前面那个人打完饭了,就可以出队列了,会把thread、prev、next置空,等待GC回收
setHead(node);
p.next = null; // help GC
failed = false;
// 返回false,整个acquire方法返回false,就出去了
return interrupted;
}
// 如果不是头部节点,就要过来等待排队了
// shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
// 队列阻塞,整个线程就等待被唤醒了
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
看代码得知,如果当前传进来的节点的上一个节点,是等于head,那么又会调用tryAcquire方法,这里体现的就是自旋获取锁,为什么要这么做呢?
是为了避免进入阻塞的状态,假设线程一已经获取到锁了,然后线程二需要进入阻塞,但是由于线程二还在进入阻塞状态的路上,线程一就已经释放锁了。为了避免这种情况,第一个排队的线程,有必要在阻塞之前再次去尝试获取锁。
假设一:假设我们线程二在进入阻塞状态之前,尝试去获取锁,哎,竟然成功了,则会执行一下代码:
// 调用方法,代码在下面
setHead(node);
p.next = null; // help GC
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
如果拿到锁了,队列的内容,当然会发送变化,由图可见,我们会发现一个问题,队列的第一个节点,又是一个空节点。
因为当拿到锁之后,会把当前节点的内容,指针全部赋值为null,这也是个小细节哟。
假设二:如果当前节点的上一个节点,不是head,那么很遗憾,没有资格去尝试获取锁,那就走下面的代码。
在进入阻塞之前,会调用shouldParkAfterFailedAcquire方法,这个方法小编先告诉你,由于我们这里是死循环对吧,这个方法第一次调用会放回false,返回false则不会执行执行后续代码,再一次进入循环,经过一些列操作,还是没有资格获取锁,或者获取锁失败,则又会来到这里。
当第二次调用shouldParkAfterFailedAcquire方法,会返回ture,这个时候,线程才会调用parkAndCheckInterrupt方法,将线程进入阻塞状态,等待锁释放,然后被唤醒!!
// 如果不是头部节点,就要过来等待排队了
// shouldParkAfterFailedAcquire 这方法会使当前循环再循环一次,相当于自旋一次获取锁
if (shouldParkAfterFailedAcquire(p, node) &&
// 队列阻塞,整个线程就等待被唤醒了
parkAndCheckInterrupt())
interrupted = true;
4.5 parkAndCheckInterrupt方法
private final boolean parkAndCheckInterrupt() {
// 在这里被park,等待unpark,如果该线程被unpark,则继续从这里执行
LockSupport.park(this);
// 这个是获取该线程是否被中断过,这句代码需要结合lockInterruptibly方法来讲,小编就不详细说了,不然一篇文章讲太多了~~~~
return Thread.interrupted();
}
到这里我们ReentrantLock整个加锁的过程,就相当于讲完啦,但是这才是最最最简单的一部分,因为还有很多场景没考虑到。
4.6 shouldParkAfterFailedAcquire方法
上面说为什么这个方法第一次调用返回false,第二次调用返回ture ?
这个方法主要做了一件事:把当前节点的,上一个节点的waitStatus状态,改为 - 1。
当线程进入阻塞之后,自己不会把自己的状态改为等待状态,而是由后一个节点进行修改。
举个例子:你躺在床上睡觉,然后睡着了,这个时候,你能告诉别人你睡着了吗?当然不行,因为你已经睡着了,呼噜声和打雷一样,怎么告诉别人。只有当后一个人来了,看到你在呼呼大睡,它才可以告诉别人你在睡觉。
//pred 当前上一个节点,node 当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 第一次循环进来:获取上一个节点的线程状态,默认为0
// 第二次循环进来,这个状态就变成-1了,
int ws = pred.waitStatus;
// 判断是否等于-1,第一进来是0,并且会吧waitStatus状态改成-1,代码在else
// 第二次进来就是-1了,直接返回true,是当前线程进行阻塞
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 判断是否大于0,waitStatus分几种状态,这里其他几种状态的源码就不一一讲了。
// = 1:由于在同步队列中等待的线程,等待超时或者被中断,需要从同步队列中取消等待,该节点进入该状态不会再变化
// = -1:后续节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,使后续节点继续运行
// = -2:节点在等待队列中,节点线程在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态中获取
// = -3:表示下一次共享式同步状态获取将会无条件地被传播下去
// = 0 :初始状态
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
// 因为默认0,所以第一次会走到else方法里面
} else {
// CAS吧waitStatus修改成-1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false,外层方法接着循环操作
return false;
}
5. ReentrantLock 解锁过程
5.1 unlock方法
讲完加锁过程,就来解锁过程吧,说实话,看源码这种经历,必须要自己花时间去看,去做笔记,去理解,大脑最好有个整体的思路,这样才会印象深刻。
public class ReentrantLockTest {
public static void main(String[] args) {
// 创建公平锁
ReentrantLock lock = new ReentrantLock(true);
// 加锁
lock.lock();
hello();
// 解锁
lock.unlock();
}
public static void hello() {
System.out.println("Say Hello");
}
}
点击unlock解锁的方法,会调用到release方法,这个是AQS提供的模板方法,再来看tryRelease方法。
public void unlock() {
sync.release(1);
}
最终调用的 release 方法如下
public final boolean release(int arg) {
// tryRelease 释放锁,如果真正释放会把当前持有锁的线程赋值为空,否则只是计数器-1
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
5.1 tryRelease方法
发现又是个抽象类,我们选择ReentrantLock类实现的
这里要注意:
1、当前解锁的线程,必须是持有锁的线程
2、state状态,必须是等于0,才算是真正的解锁,否则只是代表重入次数-1.
protected final boolean tryRelease(int releases) {
// 获取锁计数器 - 1
int c = getState() - releases;
// 判断当前线程 是否等于 持有锁的线程,如果不是则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 返回标志
boolean free = false;
// 如果计算器等于0,则代表需要真正释放锁,否则是代表重入次数-1
if (c == 0) {
free = true;
// 将持有锁的线程赋值空
setExclusiveOwnerThread(null);
}
// 重新设置state状态
setState(c);
return free;
}
执行完tryRelease方法,返回到release,进行if判断,如果返回false,就直接返回了,否则进行解锁操作。
public final boolean release(int arg) {
// tryRelease方法返回true,则表示真的需要释放锁
if (tryRelease(arg)) {
// 如果是需要真正释放锁,先获取head节点
Node h = head;
// 第一种情况,假设队列没有被初始化,这个时候head是为空的,则不需要进行锁唤醒
// 第二种情况,队列被初始化了head不为空,并且只要有线程在队列中排队,waitStatus在被加入队列之前,会把当前节点的上一个节点的waitStatus改为-1
// 所以只有满足h != null && h.waitStatus != 0 这个条件表达式,才能真正代表有线程正在排队
if (h != null && h.waitStatus != 0)
// 解锁操作,传入头节点信息
unparkSuccessor(h);
return true;
}
return false;
}
5.2 unparkSuccessor方法
这里的参数传进来的是head的node节点信息,真正解锁的线程是head.next节点,然后调用unpark进行解锁。
private void unparkSuccessor(Node node) {
// 先获取head节点的状态,应该是等于-1,原因在shouldParkAfterFailedAcquire方法中有体现
int ws = node.waitStatus;
// 由于-1会小于0,所以重新改为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取第一个正常排队的队列
Node s = node.next;
// 这里涉及到其他场景,小编就不详细讲了,正常的解锁不会执行这里
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 正常来说第一个排队的节点不应该为空,所以直接把第一个排队的线程唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
}
如果这里调用unpark,线程被唤醒,会接着这个方法接着执行。到这里整个解锁的过程小编就讲完了。
private final boolean parkAndCheckInterrupt() {
// 在这里被park,等待unpark,如果该线程被unpark,则继续从这里执行
LockSupport.park(this);
// 这个是获取该线程是否被中断过,这句代码需要结合lockInterruptibly
return Thread.interrupted();
}
6. ReentrantLock 公平锁
6.1 hasQueuedPredecessors方法
看到这里,小编希望是小伙伴真的理解了ReentrantLock加锁和解锁的过程,并且在心里有整体流程,不然你看这个方法,会很蒙,这个方法虽然代码几行,但是要完全理解,比较困难。
// 不要小看以下几行代码,涉及的场景比较复杂
public final boolean hasQueuedPredecessors() {
// 分别把尾节点、头节点赋值给 t、h
Node t = tail;
Node h = head;
Node s;
// AQS队列如果没有发生竞争,刚开始都是未初始化的,所以一开始tail、head都是为null
// 第一种情况:AQS队列没有初始化的情况
// 假设线程一,第一个进来,这个时候t、h都是为null,所以在h != t,这个判断返回false,由于用的是&&所以整个判断返回fase
// 返回flase表示不需要排队。
// 但是也不排除可能会有两个线程同时进来判断的情况,假设两个线程发现自己都不需要排队,就跑去CAS进行修改计数器,这个时候肯定会有一个失败的
// CAS 是可以保证原子性操作的,假设线程一它CAS成功了,那么线程二就会去初始化队列,老老实实排队去了
// 第二种情况:AQS队列被初始化了
// 场景一:队列元素大于1个点情况,假设有一个线程在排队,在队列中应该有而个元素,一个是头节点、线程2
// 现在线程2之前的线程已经执行完,并且释放锁唤醒线程2.线程2又会继续醒来循环。并且线程二是第一个排队的,所以有资格获取锁
// 只要是获取锁就会来排队需不需要排队,代码又回到这里
// 现在 h = 等于头节点,而 tail = 线程2的node节点,所以 h != t 结果为true
// h表示头节点,并且h.next是线程2的节点,所以 (s = h.next) == null 返回 flase
// s 等于 h.next,也就是线程2的节点信息,并且当前执行的线程也是线程2,所以 s.thread != Thread.currentThread(),返回false
// 最后return的结果是 true && false,结果为false,代表不需要排队
// 场景二:队列元素等于1个,什么情况下队列被初始化了并且只有一个元素呢?
// 当有线程竞争初始化队列,之后队列又全部都被消费完了。最后剩下一个为空的node,并且head和tail都指向它
// 这个时候有新的线程进来,其实h != t,直接返回false,因为head 和tail都指向最后一个节点了
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
流程图如下:
好博客就要一起分享哦!分享海报
此处可发布评论
评论(0)展开评论
展开评论
您可能感兴趣的博客