Java并发:AbstractQueuedSynchronizer详解(独占模式)

static final int CONDITION = -2;

// 表示下一次共享模式同步状态获取讲会无条件地被传播下去

static final int PROPAGATE = -3;

// 即上面的CANCELLED/SIGNAL/CONDITION/PROPAGATE,初始状态为0

volatile int waitStatus; // 等待状态

volatile Node prev; // 前驱节点

volatile Node next; // 后继节点

volatile Thread thread; // 节点的线程(获取同步状态的线程)

// 条件队列(注意和同步队列区分)中的后继节点:参见addConditionWaiter方法,

// 表示下一个等待Condition的Node,如果当前节点是共享的,那么这个字段将是一个

// SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用一个字段。

Node nextWaiter;

final boolean isShared() { // 如果节点在共享模式下等待,则返回true。

return nextWaiter == SHARED;

}

// 返回节点的前驱节点,如果为null,则抛出NullPointerException

final Node predecessor() throws NullPointerException {

Node p = prev;

if (p == null)

throw new NullPointerException();

else

return p;

}

Node() { // 用于创建头节点或SHARED标记

}

Node(Thread thread, Node mode) { // Used by addWaiter

this.nextWaiter = mode;

this.thread = thread;

}

Node(Thread thread, int waitStatus) { // Used by Condition

this.waitStatus = waitStatus;

this.thread = thread;

}

}

// 同步队列的头节点,使用懒汉模式初始化。 除了初始化,它只能通过setHead方法修改。

// 注意:如果头节点存在,其waitStatus保证不是CANCELLED。

private transient volatile Node head;

// 同步队列的尾节点,使用懒汉模式初始化。仅通过enq方法修改,用于添加新的等待节点。

private transient volatile Node tail;

// 同步状态, volatile修饰,很多同步类的实现都用到了该变量,

// 例如:ReentrantLock、CountDownLatch等

private volatile int 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中Node是组成队列的数据结构,如下图是队列的数据结构图:

acquire方法

=========

public final void acquire(int arg) {

// tryAcquire(arg)方法:提供给子类实现的,主要用于以独占模式尝试acquire

if (!tryAcquire(arg) &&

// addWaiter方法:添加一个独占模式的节点到同步队列的尾部;

// acquireQueued:该节点尝试acquire

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt(); // 中断当前线程

}

  1. 首先是调用tryAcquire方法,在AQS中该方法是没有实现的,子类必须实现,主要用于以独占模式尝试acquire。例如在ReentrantLock中的实现逻辑是:先获取当前的同步状态,再使用CAS尝试将同步状态修改成期望值,如果修改成功将拥有独占访问权的线程设置为当前线程。在ReentrantLock中,acquire指的是获取锁,而tryAcquire即为尝试获取锁。

  2. 如果tryAcquire返回false,则尝试acquire失败了,则会调用addWaiter方法(详解见下文代码块1),添加一个独占模式的节点到同步队列尾部。 并调用acquireQueued方法(详解见下文代码块3)尝试acquire。

  3. 最后,如果acquireQueued返回true,则调用selfInterrupt方法中断当前线程,这是因为acquireQueued返回true就是代表线程被中断。

代码块1:addWaiter方法


private Node addWaiter(Node mode) {

// 以当前线程和mode为参数,创建一个节点

Node node = new Node(Thread.currentThread(), mode);

Node pred = tail; // 将pred赋值为当前尾节点

if (pred != null) { // pred不为空

// 将新创建的节点的前驱节点设置为pred,即将刚创建的节点放到尾部

node.prev = pred;

// 使用CAS将尾节点修改为新节点

if (compareAndSetTail(pred, node)) {

// 尾节点修改成功后,将pred的后继节点设置为新节点,与上文node.prev=pred对应

pred.next = node;

return node;

}

}

// 如果pred为空,代表此时同步队列为空,调用enq方法将新节点添加到同步队列

enq(node);

return node;

}

根据当前线程和入参mode创建一个新的Node,并放到尾部。如果同步队列为空,则调用enq方法(详解见下文代码块2)添加节点。

代码块2:enq方法


// 将节点插入队列,如果队列为空则先进行初始化,再插入队列。

private Node enq(final Node node) {

for (;😉 {

Node t = tail; // 将t赋值为尾节点

// 如果尾节点为空,则初始化head和tail节点

if (t == null) {

// 使用CAS将头节点赋值为一个新创建的无状态的节点

if (compareAndSetHead(new Node()))

tail = head; // 初始化尾节点

} else { // 如果尾节点不为空,使用CAS将当前node添加到尾节点

node.prev = t; // 将node的前驱节点设置为t

if (compareAndSetTail(t, node)) { // 使用CAS将尾节点设置为node

// 成功将尾节点修改为node后,将t的后驱节点设置为node,与node.prev=t对应

t.next = node;

return t;

}

}

}

}

  1. 如果队列为空,则先初始化head和tail节点(介绍属性时说过了,head和tail采用懒汉模式初始化),再使用CAS将node添加到队列尾部。

  2. 如果队列不为空,直接使用CAS将node添加到队列尾部。

该方法和上面的addWaiter方法其实很相似,只是多了一个队列为空时的初始化head和tail操作。

代码块3:acquireQueued方法


// 添加完节点后,立即尝试该节点是否能够成功acquire

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false; // 用于判断是否被中断过

for (;😉 { // 自旋过程

final Node p = node.predecessor(); // 将p赋值为node的前驱节点

// 如果p为头节点,则node节点尝试以独占模式acquire(acquire一般为获取锁)

if (p == head && tryAcquire(arg)) {

// node节点成功以独占模式acquire,调用setHead方法将node设置为头节点

setHead(node);

p.next = null; // 断开原头节点与node节点的关联

failed = false;

return interrupted; // 返回node是否被中断过

}

// shouldParkAfterFailedAcquire: 校验node是否需要park(park:会将node的线程阻塞)

// 只有当前驱节点等待状态为SIGNAL,才能将node进行park,因为当前驱节点为SIGNAL

// 时,会保证来唤醒自己,因此可以安心park

if (shouldParkAfterFailedAcquire(p, node) &&

// node进入park状态,直到被前驱节点唤醒,被唤醒后返回线程是否为中断状态

parkAndCheckInterrupt())

interrupted = true; // 在等待过程中被中断

}

} finally {

if (failed)

cancelAcquire(node); // 取消正在进行的acquire尝试,走到这边代表出现异常

}

}

  1. 该方法用于添加完节点后调用,首先判断node节点的前驱节点是否为head,如果是,node会尝试acquire,如果node成功acquire,会调用setHead方法,将node设置为head、将node的thread设置为null、将node的prev设置为null,这保证了数据结构中头节点永远是一个不带Thread的空节点。

  2. 如果node节点的前驱节点不是head,或者node尝试acquire失败,则会调用shouldParkAfterFailedAcquire方法(详解见下文代码块4)校验node是否需要park(此处park是将node的线程阻塞,LockSupport.park),如果shouldParkAfterFailedAcquire返回true则调用parkAndCheckInterrupt方法(详解见下文代码块5)将node的线程阻塞。

  3. 如果走到finally方法时,failed为true,则代表出现了异常,调用cancelAcquire方法(详解见下文代码块6)取消正在进行的acquire尝试。

代码块4:shouldParkAfterFailedAcquire方法


// 判断节点是否应该park

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus; // 前驱节点的等待状态

if (ws == Node.SIGNAL) // 如果前驱节点节点等待状态为SIGNAL,

return true; // 返回true,表示node节点应该park,等待它的前驱节点来唤醒

// 如果前驱节点的等待状态>0,代表该前驱节点为CANCELLED(取消)状态,需要跳过该节点

if (ws > 0) {

// 从pred节点开始向前寻找,直到找到等待状态不为CANCELLED的,

do {

node.prev = pred = pred.prev; // 将其设置为node的前驱节点

} while (pred.waitStatus > 0);

pred.next = node; // 与上面对应

} else {

// pred节点使用CAS尝试将等待状态修改为SIGNAL(ws必须为PROPAGATE或0),

// 然后返回false(即再尝试一次能否不park直接acquire成功),

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false; // 返回false,代表node还不能park

}

  1. 如果前驱节点pred的waitStatus为SIGNAL,返回true,表示node节点应该park,等待它的前驱节点来唤醒。

  2. 如果前驱节点pred的waitStatus>0,代表该节点为CANCELLED(取消)状态,需要跳过该节点。从pred节点开始向前寻找,直到找到等待状态不为CANCELLED的,将其设置为node的前驱节点。

  3. 否则,使用CAS尝试将pred节点的waitStatus修改为SIGNAL,然后返回false,这里直接返回false是为了再执行一次acquireQueued方法for循环的“if (p == head && tryAcquire(arg))”代码,因为如果能tryAcquire成功,则避免了当前线程阻塞,也就减少了上下文切换的开销(关于上下文切换等内容可以参考我的另一篇文章:Java并发:性能与可伸缩性)。

代码块5:parkAndCheckInterrupt方法


private final boolean parkAndCheckInterrupt() { // 等待,然后检查是否被中断

LockSupport.park(this); // 阻塞当前节点的线程

return Thread.interrupted(); // 返回当前线程是否为中断状态

}

调用LockSupport.park方法将当前线程阻塞,并在被唤醒之后,返回当前线程是否为中断状态。

代码块6:cancelAcquire方法


private void cancelAcquire(Node node) { // 取消正在进行的acquire尝试

if (node == null)

return;

node.thread = null;

Node pred = node.prev;

// node的前驱节点pred的waitStatus如果为CANCELLED,

// 则向前寻找waitStatus不为CANCELLED的前驱节点pred

while (pred.waitStatus > 0)

node.prev = pred = pred.prev;

// 拿到pred的后继节点(不一定为node,请注意)

Node predNext = pred.next;

node.waitStatus = Node.CANCELLED; // 将node的waitStatus设置为CANCELLED

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

image.png

  • RabbitMQ实战指南

image.png

  • 手写RocketMQ笔记

image.png

  • 手写“Kafka笔记”

image

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦
ANCELLED; // 将node的waitStatus设置为CANCELLED

最后总结

ActiveMQ+Kafka+RabbitMQ学习笔记PDF

[外链图片转存中…(img-XYV6t8Cu-1719270965208)]

  • RabbitMQ实战指南

[外链图片转存中…(img-p1SgNE4X-1719270965209)]

  • 手写RocketMQ笔记

[外链图片转存中…(img-nrYMHEfZ-1719270965209)]

  • 手写“Kafka笔记”

[外链图片转存中…(img-oaAUD3X6-1719270965210)]

关于分布式,限流+缓存+缓存,这三大技术(包含:ZooKeeper+Nginx+MongoDB+memcached+Redis+ActiveMQ+Kafka+RabbitMQ)等等。这些相关的面试也好,还有手写以及学习的笔记PDF,都是啃透分布式技术必不可少的宝藏。以上的每一个专题每一个小分类都有相关的介绍,并且小编也已经将其整理成PDF啦

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值