作者:小傅哥
博客:https://bugstack.cn
积淀、分享、成长,让本人和别人都能有所播种!????
一、前言
如果你置信你做什么都能成,你会自信的多!
千万不要总自我否定,尤其是职场的打工人。如果你常常感觉,这个做不好,那个学不会,别的也不懂,那么长此以往会越来越缺乏自信。
个别说能成事的人都具备 赌徒
精力,在他们眼里只有做这事那就肯定能成,当然也有可能最初就没成,但在整个过程中人的心态是良好的,每天都有一个丰满的精神状态,手不释卷的奋斗着。最初也就是这样的斗志让走在一个终点的小伙伴,有了差距。
二、面试题
谢飞机,小记
,明天打工人呀,今天早上困呀,嘟嘟嘟,喂?谁呀,打农药呢!?
谢飞机:哎呦,面试官大哥,咋了!
面试官:偷偷通知你哈,你一面过了。
谢飞机:嘿嘿,真的呀!太好了!哈哈哈,那我还筹备点什么呢!?
面试官:二面会比拟难喽,嗯,我顺便问你一个哈。AQS 你理解吗,ReentrantLock 获取锁的过程是什么样的?什么是 CAS?…
谢飞机 :我我我, 脑子还在后羿射箭里,我一会就看看!!
面试官:好好筹备下吧,打工人,打工魂!
三、ReentrantLock 和 AQS
1. ReentrantLock 常识链
ReentrantLock 可重入独占锁波及的知识点较多,为了更好的学习这些常识,在上一章节先剖析源码和学习实现了偏心锁的几种计划。包含:CLH、MCS、Ticket,通过这部分内容的学习,再来了解 ReentrantLock 中对于 CLH 的变体实现和相应的利用就比拟容易了。
接下来沿着 ReentrantLock 的常识链,持续剖析 AQS 独占锁的相干知识点,如图 17-1
在这部分常识学习中,会次要围绕 ReentrantLock 中对于 AQS 的应用进行开展,逐渐剖析源码理解原理。
AQS 是 AbstractQueuedSynchronizer 的缩写,简直所有 Lock 都是基于 AQS 来实现了,其底层大量应用 CAS 提供乐观锁服务,在抵触时采纳自旋形式进行重试,以此实现轻量级和高效的获取锁。
另外 AbstractQueuedSynchronizer 是一个抽象类,但并没有定义相应的形象办法,而是提供了能够被字类继承时笼罩的 protected 的办法,这样就能够十分不便的反对继承类的应用。
2. 写一个简略的 AQS 同步类
在学习 ReentrantLock 中利用的 AQS 之前,先实现一个简略的同步类,来领会下 AQS 的作用。
2.1 代码实现
public class SyncLock {
private final Sync sync;
public SyncLock() {sync = new Sync();
}
public void lock() {sync.acquire(1);
}
public void unlock() {sync.release(1);
}
private static class Sync extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {return compareAndSetState(0, 1);
}
@Override
protected boolean tryRelease(int arg) {setState(0);
return true;
}
// 该线程是否正在独占资源,只有用到 Condition 才须要去实现
@Override
protected boolean isHeldExclusively() {return getState() == 1;
}
}
}
这个实现的过程属于 ReentrantLock 简版,次要包含如下内容:
- Sync 类继承 AbstractQueuedSynchronizer,并重写办法:tryAcquire、tryRelease、isHeldExclusively。
- 这三个办法根本是必须重写的,如果不重写在应用的时候就会抛异样
UnsupportedOperationException
。 - 重写的过程也比较简单,次要是应用 AQS 提供的 CAS 办法。以预期值为 0,写入更新值 1,写入胜利则获取锁胜利。其实这个过程就是对 state 应用
unsafe
本地办法,传递偏移量 stateOffset 等参数,进行值替换操作。unsafe.compareAndSwapInt(this, stateOffset, expect, update)
- 最初提供 lock、unlock 两个办法,理论的类中会实现 Lock 接口中的相应办法,这里为了简化间接自定义这样两个办法。
2.2 单元测试
@Test
public void test_SyncLock() throws InterruptedException {final SyncLock lock = new SyncLock();
for (int i = 0; i < 10; i++) {Thread.sleep(200);
new Thread(new TestLock(lock), String.valueOf(i)).start();}
Thread.sleep(100000);
}
static class TestLock implements Runnable {
private SyncLock lock;
public TestLock(SyncLock lock) throws InterruptedException {this.lock = lock;}
@Override
public void run() {
try {lock.lock();
Thread.sleep(1000);
System.out.println(String.format("Thread %s Completed", Thread.currentThread().getName()));
} catch (Exception e) {e.printStackTrace();
} finally {lock.unlock();
}
}
}
- 以上这个单元测试和咱们在上一章节介绍偏心锁时是一样的,验证程序输入。当然你也能够抉择多线程操作一个办法进行加和运算。
- 在测试的过程中能够尝试把加锁代码正文掉,进行比对。如果能够程序输入,那么就是预期后果。
测试后果
Thread 0 Completed
Thread 1 Completed
Thread 2 Completed
Thread 3 Completed
Thread 4 Completed
Thread 5 Completed
Thread 6 Completed
Thread 7 Completed
Thread 8 Completed
Thread 9 Completed
- 从测试后果看,以上 AQS 实现的同步类,满足预期成果。
- 有了这段代码的概念构造,接下来在剖析 ReentrantLock 中的 AQS 应用就有肯定的感觉了!
3. CAS 介绍
CAS 是 compareAndSet 的缩写,它的利用场景就是对一个变量进行值变更,在变更时会传入两个参数:一个是预期值、另外一个是更新值。如果被更新的变量预期值与传入值统一,则能够变更。
CAS 的具体操作应用到了 unsafe
类,底层用到了本地办法 unsafe.compareAndSwapInt
比拟替换办法。
CAS 是一种无锁算法,这种操作是 CPU 指令集操作,只有一步原子操作,速度十分快。而且 CAS 防止了申请操作系统来裁定锁问题,间接由 CPU 搞定,但也不是没有开销,比方 Cache Miss,感兴趣的小伙伴能够自行理解 CPU 硬件相干常识。
4. AQS 外围源码剖析
4.1 获取锁流程图
图 17-2 就是整个 ReentrantLock 中获取锁的外围流程,包含非偏心锁和偏心锁的一些穿插流程。接下来咱们就以此依照此流程来解说相应的源码局部。
4.2 lock
ReentrantLock 实现了非偏心锁和偏心锁,所以在调用 lock.lock();
时,会有不同的实现类:
- 非偏心锁,会间接应用 CAS 进行抢占,批改变量 state 值。如果胜利则间接把本人的线程设置到 exclusiveOwnerThread,也就是取得锁胜利。不胜利后续剖析
- 偏心锁,则不会进行抢占,而是规规矩矩的进行排队。老实人
4.3 compareAndSetState
final void lock() {if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
在非偏心锁的实现类里,获取锁的过程,有这样一段 CAS 操作的代码。compareAndSetState
赋值胜利则获取锁。那么 CAS 这外面做了什么操作?
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
往下翻咱们看到这样一段代码,这里是 unsafe 性能类的应用,两个参数到这里变成四个参数。多了 this、stateOffset。this 是对象自身,那么 stateOffset 是什么?
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
再往下看咱们找到,stateOffset 是偏移量值,偏移量是一个固定的值。接下来咱们就看看这个值到底是多少!
援用 POM jol-cli
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-cli</artifactId>
<version>0.14</version>
</dependency>
单元测试
@Test
public void test_stateOffset() throws Exception {Unsafe unsafe = getUnsafeInstance();
long state = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
System.out.println(state);
}
// 16
- 通过 getUnsafeInstance 办法获取 Unsafe,这是一个固定的办法。
- 在获取 AQS 类中的属性字段 state 的偏移量,16。
- 除了这个属性外你还能够拿到:headOffset、tailOffset、waitStatusOffset、nextOffset,的值,最终自旋来变更这些变量的值。
4.4 (AQS)acquire
public final void acquire(int arg) {if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();}
整个这块代码外面蕴含了四个办法的调用,如下:
- tryAcquire,别离由继承 AQS 的偏心锁(FairSync)、非偏心锁(NonfairSync)实现。
- addWaiter,该办法是 AQS 的公有办法,主要用途是办法 tryAcquire 返回 false 当前,也就是获取锁失败当前,把以后申请锁的线程增加到队列中,并返回 Node 节点。
- acquireQueued,负责把 addWaiter 返回的 Node 节点增加到队列结尾,并会执行获取锁操作以及判断是否把以后线程挂起。
- selfInterrupt,是 AQS 中的
Thread.currentThread().interrupt()
办法调用,它的次要作用是在执行完 acquire 之前本人执行中断操作。
4.5 tryAcquire
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {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;
}
这部分获取锁的逻辑比较简单,次要包含两局部:
- 如果
c == 0
,锁没有被占用,尝试应用 CAS 形式获取锁,并返回 true。 - 如果
current == getExclusiveOwnerThread()
,也就是以后线程持有锁,则须要调用setState
进行锁重入操作。setState 不须要加锁,因为是在本人的以后线程下。 - 最初如果两种都不满足????,则返回 false。
4.6 addWaiter
private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果队列不为空, 应用 CAS 形式将以后节点设为尾节点
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 队列为空、CAS 失败,将节点插入队列
enq(node);
return node;
}
- 当执行办法
addWaiter
,那么就是!tryAcquire = true
,也就是 tryAcquire 获取锁失败了。 - 接下来就是把以后线程封装到 Node 节点中,退出到 FIFO 队列中。因为先进先出,所以起初的队列退出到队尾
compareAndSetTail
不肯定肯定胜利,因为在并发场景下,可能会呈现操作失败。那么失败后,则须要调用 enq 办法,该办法会自旋操作,把节点入队列。
enq
private Node enq(final Node node) {for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 自旋转
for 循环
+ CAS 入队列。 - 当队列为空时,则会新创建一个节点,把尾节点指向头节点,而后持续循环。
- 第二次循环时,则会把以后线程的节点增加到队尾。head 节是一个无用节点,这和咱们做 CLH 实现时相似
留神,从尾节点逆向遍历
- 首先这里的节点连贯操作并不是原子,也就是说在多线程并发的状况下,可能会呈现个别节点并没有设置 next 值,就失败了。
- 但这些节点的 prev 是有值的,所以须要逆向遍历,让 prev 属性从新指向新的尾节点,直至全副自旋入队列。
4.7 acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {final Node p = node.predecessor();
// 以后节点的前驱就是 head 节点时, 再次尝试获取锁
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);
}
}
当获取锁流程走到这,阐明节点曾经退出队列实现。看源码中接下来就是让该办法再次尝试获取锁,如果获取锁失败会判断是否把线程挂起。
setHead
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
在学习 CLH 偏心锁数据结构中讲到 Head 节点是一个虚节点,如果以后节点的前驱节点是 Head 节点,那么阐明此时 Node 节点排在队列最后面,能够尝试获取锁。
获取锁后设置 Head 节点,这个过程就是一个出队列过程,原来节点设置 Null 不便 GC。
shouldParkAfterFailedAcquire
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// SIGNAL 设置了前一个节点完结唤醒,安心干别的去了,这里是睡。return true;
if (ws > 0) {
do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
pred.next = node;
} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
你是否还 CANCELLED、SIGNAL、CONDITION、PROPAGATE,这四种状态,在这个办法中用到了两种如下:
- CANCELLED,勾销排队,放弃获取锁。
- SIGNAL,标识以后节点的下一个节点状态曾经被挂起,意思就是大家一起排队上厕所,队伍太长了,前面的谢飞机说,我去买个油条哈,一会到我了,你微信我哈。其实就是以后线程执行结束后,须要额定执行唤醒后继节点操作。
那么,以上这段代码次要的执行内容包含:
- 如果前一个节点状态是
SIGNAL
,则返回 true。安心睡觉???? 等着被叫醒 - 如果前一个节点状态是
CANCELLED
,就是它放弃了,则持续向前寻找其余节点。 - 最初如果什么都没找到,就给前一个节点设置个闹钟
SIGNAL
,等着被告诉。
4.8 parkAndCheckInterrupt
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
// 线程挂起期待被唤醒
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);
return Thread.interrupted();}
- 当办法
shouldParkAfterFailedAcquire
返回 false 时,则执行 parkAndCheckInterrupt() 办法。 - 那么,这一段代码就是对线程的挂起操作,
LockSupport.park(this);
。 Thread.interrupted()
查看以后线程的中断标识。
四、总结
- ReentrantLock 的常识比拟多,波及的代码逻辑也比较复杂,在学习的过程中须要对照源码和相干并发书籍和材料一起学习,以及最好的是本身实际。
- AQS 的实现局部波及的内容较多,例如:state 属性应用 unsafe 提供的本地办法进行 CAS 操作,把初始值 0 改为 1,则取得了锁。addWaiter、acquireQueued、shouldParkAfterFailedAcquire、parkAndCheckInterrupt 等,能够粗疏总结。
- 所有的 Lock 都是基于 AQS 来实现了。AQS 和 Condition 各自保护了不同的队列,在应用 Lock 和 Condition 的时候,就是两个队列的相互挪动。这句话能够细细领会。可能文中会有一些不精确或者错字,欢送留言,我会一直的更新博客。
五、系列举荐
- volatile 怎么实现的内存可见?没有 volatile 肯定不可见吗?
- synchronized 解毒,分析源码深度剖析!
- ReentrantLock 之偏心锁解说和实现
- 咋嘞?你的 IDEA 过期了吧!加个 Jar 包就破解了,为什么?
- 大学四年到毕业工作 5 年的学习路线资源汇总