深入并发原理和大厂面试(一):AQS解析

  1. Node相关属性

// 节点所代表的线程

volatile Thread thread;

// 双向链表,每个节点需要保存自己的前驱节点和后继节点的引用

volatile Node prev;

volatile Node next;

// 线程所处的等待锁的状态,初始化时,该值为0

volatile int waitStatus;

static final int CANCELLED = 1;

static final int SIGNAL = -1;

2. 源码解析


ReentrantLock有公平锁和非公平锁两种实现,默认实现非公平锁。但是可配置为公平锁:

ReentrantLock lock=new ReentrantLock(true);

调用公平锁加锁逻辑:

final void lock() {

//开始加锁,将state修改为1

acquire(1);

}

真正的加锁方法:

public final void acquire(int arg) {

if (!tryAcquire(arg) &&

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

selfInterrupt();

}

2.1 加锁的逻辑方法

只执行上述方法便可完成整个的加锁逻辑。而该方法中又包含下列四个方法的调用:

1. tryAcquire(arg)

该方法由继承AQS的子类实现,为获取锁的具体逻辑;

2. addWaiter(Node.EXCLUSIVE)

该方法由AQS实现,负责在获取锁失败后调用,将当前请求锁的线程包装成Node并且放到等待队列中,并返回该Node。

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

该方法由AQS实现。针对上面加入到队列的Node不断尝试两种操作之一:

  • 若前驱节点是head节点的时候,尝试获取锁;

  • 调用park将当前线程挂起,线程阻塞。

4. selfInterrupt

该方法由AQS实现。恢复用户行为。

  1. 用户在外界调用t1.interrupt()进行中断。

  2. 线程在parkAndCheckInterrupt方法被唤醒之后。会调用Thread.interrupted();判断线程的中断标识,而该方法调用完毕会清除中断标识位。

  3. 而AQS为了不改变用户标识。再次调用selfInterrupt恢复用户行为。

2.2 如何构建等待队列——addWaiter

我们使用ReentrantLock独占锁时,等待队列是延迟加载的。也就是说若是线程交替执行,那么借助信号量(状态)来保证。若是线程并发执行,就需要将阻塞线程放入到队列中。

//注意这个方法可能存在并发问题,mode为null(独占锁)。

private Node addWaiter(Node mode) {

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

Node pred = tail;

//队列已经存在

if (pred != null) {

//新节点的前驱指针指向尾节点(可能造成尾分叉)

node.prev = pred;

//保证原子性,只有一个才能成功

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

//队列不存在&&上面CAS失败的线程会进入enq方法自旋

enq(node);

return node;

}

队列不存在的情况

image

注意,该方法处理CAS操作是原子性的,其他操作都存在并发冲突问题。

private Node enq(final Node node) {

for (;😉 {

Node t = tail;

//初始化阻塞队列

if (t == null) { // Must initialize

if (compareAndSetHead(new Node()))

tail = head;

} else {

//自旋处理addWaiter中CAS加锁失败的线程

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

该方法采用自旋+CAS。CAS是保证同一时刻只有一个线程能成功改变引用的指向。

image

根据上面的流程图,sync queue的创建过程。head节点是new Node()产生的,即其中的属性为默认值。也就是thread属性为null。也就是说正在执行的线程也会在sync queue中占据头节点,但是节点中不会保存线程信息。

image

尾分叉问题:

上面已经说了,该方法是线程不安全的。

//步骤1:可能多个节点的prev指针都指向尾结点,导致尾分叉

node.prev = t;

//步骤2:但同一时刻,tail引用只会执行一个node。

if (compareAndSetTail(t, node)) {

//步骤3:现在环境是线程安全,旧尾结点的后继指针指向新尾结点。

t.next = node;

return t;

}

image

执行完步骤2,但步骤3还未执行时,恰好有线程从头节点开始往后遍历。**此时(旧)尾结点中的next域还为null。**它是遍历不到新加进来的尾结点的。这显然是不合理的。

但此时步骤1是执行成功的,所以若是tail节点往前遍历,实际上是可以遍历到所有节点的,这也是为什么在AQS源码中,有时候常常会出现从尾结点开始逆向遍历链表的情况

那些“分叉”的节点,肯定会入队失败。那么继续自旋,等待所有的线程节点全部入队成功。

2.3 尝试获取锁——tryAcquire

根据标志位state,来判断锁是否被占用。此时可能锁未被占用,由于是公平锁,于是会去判断sync queue中是否有人在排队。

protected final boolean tryAcquire(int acquires) {

//获取当前线程

final Thread current = Thread.currentThread();

//获取Lock对象的上锁情况,0-表示无线程持有;1-表示被线程持有;大于1-表示锁被重入

int c = getState();

//若此刻无人占有锁

if (c == 0) {

if (!hasQueuedPredecessors() && //判断队列中是否有前辈。若返回false代表没有,开始尝试加锁

compareAndSetState(0, acquires)) { //此刻队列中没有存在前辈,尝试加锁

setExclusiveOwnerThread(current); //将当前线程修改为持有锁的线程(后续判断可重入)

return true;

}

}

//若是当前线程是持有锁的线程

else if (current == getExclusiveOwnerThread()) {

//当前状态+1

int nextc = c + acquires;

if (nextc < 0)

throw new Error(“Maximum lock count exceeded”);

setState(nextc);

return true;

}

//否则,代表加锁失败

return false;

}

下面的方法返回false才会尝试加锁(该方法不具有原子性,可能会放行多个线程)。

//该方法不具有原子性,可能多个线程都觉得自己不需要排队,最终还是依靠外面

//条件上的CAS来保持其原子性。

public final boolean hasQueuedPredecessors() {

Node t = tail; //尾节点

Node h = head; //头节点

Node s;

return h != t &&

((s = h.next) == null || s.thread != Thread.currentThread());

}

上述方法是判断队列中是否存在元素。可能存在以下几种情况:

  • 此时未维护队列【h和t指向null】,h!=t返回false,即无人排队;

  • 此时队列只有头节点(哑结点)【h和t都指向哑结点】,h!=t返回false,即无人排队;

  • 此时队列中存在2个以上的节点。若线程是头结点的后继节点线程(即处理正在办理业务的线程,进来的线程是第一个排队的线程)。那么s.thread != Thread.currentThread()返回false,即可是尝试加锁。

  • 队列存在2个以上节点,且进来的线程不是第一个排队的线程,那么该线程需要乖乖的排队。

当然该方法不是并发安全的方法,即可能存在多个线程觉得自己无需排队,最终还是依靠CAS来争夺锁。

if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {

//线程安全

setExclusiveOwnerThread(current);

return true;

}

同一时刻,只有一个线程可以成功改变state的状态。记录该线程为独占锁线程,一般后续可以重入。

没成功获取锁那么会调用2.2 中的方法,将该线程加入到阻塞队列中

2.3. 阻塞线程——acquireQueued

  • 若执行到该方法,说明addWaiter方法已经成功将该线程包装为Node节点放到了队尾。

  • 在该方法中依旧尝试获取锁;

  • 再次获取锁失败后,会将其阻塞;

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

boolean failed = true;

try {

boolean interrupted = false;

for (;😉 {

//获取node的前驱节点

final Node p = node.predecessor();

//若前驱节点在办理业务,那么它将再次获取一次锁。

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

//获取锁成功,此处便是线程安全。

//将自己设置为头节点,并将自己设置为哑节点

setHead(node);

p.next = null; // help GC

failed = false;

return interrupted;

}

//获取锁失败,将自己挂起。

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

上述方法时自旋方法,而出口就是获取到锁。若线程获取不到锁,便会将自己阻塞。

//该方法时node线程获取锁成功后执行的,故是线程安全的。

private void setHead(Node node) {

head = node;

node.thread = null;

node.prev = null;

}

image

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

//上一个节点的waitStatus

int ws = pred.waitStatus;

// Node.SIGNAL==-1

if (ws == Node.SIGNAL)

return true;

//ws大于0,则说明该节点已经被取消了。

if (ws > 0) {

do {

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

} else {

//CAS变更ws的状态

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

上述方法是加锁失败开始执行的。也就是一个线程决定挂起之前需要执行的操作。这里就用到了节点中的信号量waitStatus

  1. 判断前驱节点waitStatus的值,会做出如下操作:

1.1 前驱节点waitStatus若是-1,直接返回true。

1.2 前驱节点waitStatus若大于0,证明前驱节点已被取消,那么在链表中删除前驱节点,直到node的前驱节点的waitStatus不大于0为止。然后返回false

1.3. 若前驱节点waitStatus等于0,使用CAS尝试改变前驱节点waitStatus状态,由0到-1,然后返回false。

  1. 若是返回true,那么去阻塞该节点,若是返回false,那么继续自旋,继续上述过程,直至该方法返回true为止,方法返回true,便会执行下列方法,阻塞线程。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

题外话

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

欢迎评论区讨论。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

2-1713678551408)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

题外话

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

这里我为大家准备了一些我工作以来以及参与过的大大小小的面试收集总结出来的一套进阶学习的视频及面试专题资料包,主要还是希望大家在如今大环境不好的情况下面试能够顺利一点,希望可以帮助到大家~

[外链图片转存中…(img-x0h6dl0c-1713678551409)]

欢迎评论区讨论。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值