AQS全称是(Abstract Queued Synchronizer),单从名字能够翻译为抽象队列同步器,它是构建J.U.C(java.util.concurrent)包下并发工具类的基础框架,AQS除了提供了可中断锁(等待中断),超时锁、独占锁、共享锁等等功能以外,又在这些基础的功能上进行扩展,衍生除了其余的一些工具类,这些工具类基本上能够知足咱们实际应用中对于锁的各类需求。java
在我没有看过AQS源码以前,感受它的实现和synchronized原理是同样的,感受都是经过对象锁来实现并发访问控制,但事实上它仅仅是一个普通的工具类,就比如咱们平时开发过程当中写的utils类同样,AQS的实现没有像synchronized关键字同样,利用高级的机器指令和内存模型的规则,它没有利用高级机器指令,也没有利用JDK编译时的特殊处理,仅仅是一个普通的类,就实现了并发的控制。这是令我很是有兴趣想去深刻的探索和学习它的设计思想和实现原理。node
咱们为何要研究AQS的实现呢?由于synchronized和J.U.C包下的工具类是咱们并发编程中常用到的,J.U.C包下的大部分工具类都是基于AQS进行的扩展实现,想要掌握和了解J.U.C包下工具类的实现原理,了解AQS的实现是必不可少的。web
既然JVM已经提供了像synchronized、volatile这样的关键字,已经能够解决并发中的三个问题,也能够解决线程执行顺序的问题,那为何还要去创造一个AQS框架,重复造造轮子的意义又在哪里?编程
其实咱们在使用synchronized的工程中设计模式
一个框架或者技术的出现确定是为了解决某些问题,那功能和性能是否能成为重复造轮子的理由呢?那AQS同步框架的出现是为了解决synchronized没有办法知足的使用场景,咱们来看一下AQS提供的功能特色。并发
上述所说的几个特色,都是synchronized这个关键字不不具有的特色,AQS除了知足synchronized的全部功能以外呢,又基于实现了扩展读写锁(ReadWriteLock)、信号量(Semaphore)、栅栏(CyclicBarrier)等额外的功能锁,极大的提升的使用场景和灵活性。那咱们接下就一块儿看看AQS的详细实现。框架
咱们进入到AQS的源码中能够看到AQS是一个抽象类,可是咱们发现AQS中并无一个抽象方法,这是由于AQS是被设计来支持多种用途的,它是做为不少工具类的基础框架来使用的,若是有抽象方法则子类在继承时必需要重写全部的抽象方法,这显然是不符合AQS的设计初衷;因此,AQS框架采用了模板方法的设计模式,AQS将一些须要子类覆写的方法都设计成protect方法,将其默认实现为抛出UnsupportedOperationException异常,若是子类须要使用到此方法,则重写此方法。编辑器
AQS底层设计并非特别复杂,它底层采用的是状态标志位(state变量)+FIFO队列的方式来记录获取锁、释放锁、竞争锁等一系列锁操做;对于AQS而言,其中的state变量能够看作是锁,队列采用的是先进先出的双向链表,state共享状态变量表示锁状态,内部使用CAS对state进行原子操做修改来完成锁状态变动(锁的持有和释放)。函数
当某个线程请求持有锁时,经过判断state当前状态,判断锁是否被其余线程持有,若是没有被占用,那就让请求线程持有锁;若是锁被占用,那请求线程将进入阻塞状态,将其封装成Node节点,而后经过节点之间进行关联,组成了一个双向链表。当持有锁的线程完成操做之后,会释放锁资源,而后唤醒在队列中的节点(固然这是公平锁的作法,咱们下面会说到),就这样经过队列来实现了线程的阻塞和唤醒。那下面咱们就经过具体的代码来看一下AQS的实现。工具
private volatile int state;
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
复制代码
state状态这里仍是比较简单的,使用volatile修饰,保证state变量的可见性, setState(int newState)方法只是用做给state进行初始化,而compareAndSetState(int expect, int update)用做了在运行期间对state变量的修改。
为何要单独多出来一个compareAndSetState方法对state变量进行修改呢?由于对共享变量的赋值,不是原子操做须要额外的锁同步,咱们可能想到使用synchronized来保证原子性,可是synchronizedh会使线程阻塞,致使线程上下文的切换,影响其性能。这里采用的是CAS无锁操做,可是CAS也是有不足的,它会进行自旋操做,这样也会对CPU的资源形成浪费。
AQS会把没有争抢到锁的线程包装成Node节点,加入到队列中,咱们看一下Node的结构
static final class Node {
//标记节点是共享模式
static final Node SHARED = new Node();
//标记节点是独占的
static final Node EXCLUSIVE = null;
//表明此节点的线程取消了争抢资源
static final int CANCELLED = 1;
//表示当前node的后继节点对应的线程须要被唤醒
static final int SIGNAL = -1;
//这两个状态和condition有关系,这里先不说condition
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 取值为上面的一、-一、-二、-3 或者 0
volatile int waitStatus;
volatile Node prev;
volatile Node next;
//等待线程
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
//线程入队。
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//使用condition用到
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
复制代码
同步队列是AQS的核心,用来实现线程的阻塞和唤醒操做,waitStatus它表示了当前Node所表明的线程的等待锁的状态,在独占锁模式下,咱们只须要关注CANCELLED、SIGNAL两种状态便可。nextWaiter属性,它在独占锁模式下永远为null,仅仅起到一个标记做用。下图是基于独占锁画的。
AQS定义两种资源共享方式:
AQS的设计是基于模板方法模式,队列维护和Node节点的入队出队或者获取资源失败等操做,AQS都已经实现了。资源的实际获取逻辑交由子类去实现。并且它提供了两种资源访问的方式:Exclusive(独占模式)和Share(共享模式);须要实现什么样的资源访问模式,子类只须要重写AQS预留的方法,利用其提供的原子操做方法,来修改state变量实现相应的同步逻辑就能够了。通常状况下,子类只须要根据需求实现其中一种模式,固然也有同时实现两种模式的同步类,如ReadWriteLock。
自定义同步器实现时主要实现如下几种方法:
在独占模式下和synchronized实现的效果是同样的,一次只能有一个线程访问。state 等于0 表明没有线程持有锁,大于0表明有线程持有当前锁。这个值能够大于1,是由于锁能够重入,每次重入都加上 1,也须要对应的屡次释放。
在共享模式下,state的值表明着有多少个许可,可是它在每一个具体的工具类里的应用仍是有一些差异的。经过下面的动画来感觉一下什么是共享锁的用法。
前面咱们说AQS获取锁的逻辑都是交由子类去实现,那咱们就经过具体代码来看一会儿类究竟是如何实现的,以ReentranLock为例,来看一下实现的细节。
ReentrantLock有公平锁 和 非公平锁 两种实现, 默认实现为非公平锁, 这体如今它的构造函数中,咱们接下来就以独占锁开始分析一下ReentranLock,咱们先来看一下ReentranLock的结构。
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
//ReentranLock的内部类,
abstract static class Sync extends AbstractQueuedSynchronizer {
...
}
// 非公平锁
static final class NonfairSync extends Sync{...}
//公平锁
static final class FairSync extends Sync {...}
//构造函数
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 获取锁
public void lock() {
sync.lock();
}
// 释放锁
public void unlock() {
sync.release(1);
}
...
}
复制代码
能够看到FairSync和NonfairSync都是继承自Sync,而Sync又继承自AQS。ReentrantLock获取锁的逻辑是直接调用了FairSync或者NonfairSync的逻辑.咱们就以FairSync为例,来看一下具体实现。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//抢锁
final void lock() {
//这里直接调用的AQS的acquire()方法
acquire(1);
}
//====此方法来自AQS,为了方便阅读,贴过来了====
/**
经过代码咱们能看到,若是tryAcquire(arg)这个方法返回true,直接就退出了,后续也就不会进行了。
因此咱们能够推断出来,大部分状况下,应该返回的是false。咱们一个方法一个方法来看。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
}
//======================================
protected final boolean tryAcquire(int acquires) {
...
}
}
复制代码
tryAcquire 判断当前锁有没有被占用:
获取锁成功返回true
, 失败则返回false
/**
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
尝试获取锁,返回boolean,是否获取锁成功。
true:1.表明没有线程在等待锁。2.自己就持有锁,可是是重入锁。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//若是等于0,那么表明没有线程持有锁。
if (c == 0) {
/**
到这里说明没有线程抢锁,再去判断队列中是否有线程在等待获取锁。
由于是公平锁,老是先来后到的
若是队列中没有线程等待获取锁,那就尝试去获取锁。
若是获取成功了,那就把当前占用锁的线程,更新为当前线程。
*/
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
/**
hasQueuedPredecessors方法,主要来判断队列是否为空,
判断头结点的后节点是否是当前节点。
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
*/
// 将当前线程设置为占用锁的线程
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
/**
若是到这里,说明当前占用锁的线程就是自己。就是咱们所说的重入锁。
*/
int nextc = c + acquires;
if (nextc < 0){
throw new Error("Maximum lock count exceeded");
}
setState(nextc);
return true;
}
//若是到这里都没有返回true,说明没有获取到锁。
return false;
}
复制代码
若是tryAcquire()方法返回false说明抢锁失败了,那就继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法, 这一步主要是将没有抢到锁的线程加入到队列中,咱们先来看一下addWaiter()方法。
/**
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
*/
private Node addWaiter(Node mode) {
/** Node构造方法
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
*/
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// tail !=null 说明队列不为空。当队列为空时tail = head 是为null的,
if (pred != null) {
//将新节点的前驱节点指向旧的尾结点。
node.prev = pred;
//将新的节点变成尾结点。
if (compareAndSetTail(pred, node)) {
//若是设置成功,那就将旧的尾结点的后继节点,指向新的节点。直接返回Node
pred.next = node;
return node;
}
}
//若是执行到这里说明有两种状况 :1.队列为空。2.CAS失败(有线程在竞争入队)
//这时会执行enq()方法
enq(node);
return node;
}
复制代码
此方法主要是将等待的线程包装成 Node节点。可见,每个处于独占锁模式下的节点,nextWaiter 必定是null。此方法会先判断队列是否为空,若是不为空,尝试将Node节点添加到队列的队尾。若是入队失败了或者队列为空,就执行enq方法。
若是执行了enq()方法会有两种可能:
在该方法中使用了死循环, 即以自旋方式将节点插入队列,若是失败则不停的尝试, 直到成功为止, 另外, 该方法也负责在队列为空时, 初始化队列,这也说明,队列是延时初始化的(lazily initialized):
/*咱们再看一下enq()这个方法的代码。
这个方法采用了自旋式入队列的方式。
若是没有抢到锁,那就一直循环,直到入队。
*/
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
/**若是tail==null 说明队列为空,咱们在刚开始的时候会发现,head和tail都为null,
是没有进行初始化的。这里仍是使用的cas设置头结点,跟设置尾结点同样。
*/
if (compareAndSetHead(new Node())){
/**
这里设置了头节点,可是尾结点仍是为null,
将尾结点也设置一下,注意,此时尚未return,继续循环。
*/
tail = head;
}
}else {
//这个其实和addWaiter()方法是相似的,都是将线程添加到队尾。
//只不过是若是不成功一直循环,直到成功为止。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
复制代码
咱们这里能够看到,当队列为空的时候,初始化队列没有使传入的那个Node节点,而是新建了一个Node节点。初始化之后里面没有返回,而是直接进入下一次循环,此时队列已经不为空了,就将传入的Node节点添加到队尾。这也说明了为何在咱们刚开始说FIFO队列的时候头结点是空节点了。
这里咱们能够看到enq()方法是有返回值的,返回的是node结点的前驱节点,只不过在这里没有用到它的返回值,可是在其余的地方用到了它的返回值。
代码能走到这里已经说明,通过addWaiter(Node.EXCLUSIVE),此时节点添加到了队列中。
注意:若是acquireQueued(addWaiter(Node.EXCLUSIVE),arg))返回true的话,意味着上面这段代码将进入selfInterrupt(),因此正常状况下,下面应该返回false。
/**
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
selfInterrupt();
}
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//当前节点的前驱节点。addWaiter方法返回的是通过封装的Node节点。
final Node p = node.predecessor();
/**
p == head 说明当前节点已经进到了阻塞队列中,可是Node节点是阻塞队列的第一个,由于它的前驱是 head。正常状况下,咱们是将Node节点添加到队尾的,若是说Node的前驱节点是head节点,说明Node节点是 阻塞队列中的第一个,能够再去尝试获取锁。
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
/**
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
*/
p.next = null; // help GC
failed = false;
return interrupted;
}
//当前Node不是在CLH队列的第一位或者是当前线程获取锁失败,判断是否须要把当前线程挂起。
if(shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()){
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
复制代码
咱们在分析FIFO队列的结构时,看到节点组成中有 waitStatus这个状态,它的取值有四个
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
复制代码
在独占锁的状况下只会用到 CANCELLED 和 SIGNAL这两个状态,怎么理解这个状态表明的含义呢。
CANCELLED这个比较好理解,它表示当前的节点取消了排队,即取消了抢锁。SIGNAL这个状态它不表示当前节点的状态,它表明当前节点前驱节点的状态,当一个节点的waitStatus被置为SIGNAL
,就说明它的下一个节点已经被挂起了(或者立刻就要被挂起了),所以在当前节点释放了锁或者放弃获取锁时,若是它的waitStatus属性为SIGNAL
,它还要完成一个额外的操做——唤醒它的后继节点。
/**if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){}
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
//我认为这里只是对前驱节点状态进行判断,判断前驱节点时候是正常状态,由于咱们知道若是当前节点被挂
//唤醒时须要前驱节点来进行唤醒的,若是当前节点的前驱节点是正常状态,就能保证当前节点能够被正常唤醒,
//由于在等待队列中的节点有可能退出了所等待,因此须要判断前驱节点状态是否正常。
if (ws == Node.SIGNAL)
//若是前驱节点的状态已是SIGNAL,就直接返回true,接下来就会直接去执行parkAndCheckInterrupt()将线程挂起
//由于前驱节点状态正常,当前节点能够被挂起。
return true;
/*
* 当前驱节点的status大于0说明前驱节点取消了抢锁,退出了队列。
若是前驱节点取消了抢锁,就继续往前找,找到一个节点是正常状态的节点,而后直接跳过那些不排队的节点,添加到 第一个正常等待节点的后面
*/
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
前驱节点的状态既不是SIGNAL,也不是CANCELLED
用CAS设置前驱节点的ws为 Node.SIGNAL。
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
复制代码
这里值得咱们注意的是,只有当前节点的前驱节点状态等于SIGNAL的时候才会返回ture,其余状况只会返回false。
当返回false以后呢,又会回到acquireQueued方法中循环,由于当前节点的前驱节点发生了变化,说不定前驱节点是头结点了呢,直到返回true,也就是前驱节点状态时SIGNAL,就能够安心的将当前线程挂起了,此时将调用parkAndCheckInterrupt将线程挂起。
这个方法很简单,由于前面返回true,因此须要挂起线程,这个方法就是负责挂起线程的,到这里锁获取就已经分析完了。
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 线程被挂起,停在这里再也不往下执行了
return Thread.interrupted();
}
复制代码
非公平锁的实现,其实和公平锁的实现差异不大,具体经过代码来看一下吧。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//非公平锁的lock和公平锁的lock区别在于,非公平锁直接上来就去直接获取锁,无论阻塞队列是有线程等待
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
}
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//这个方法来自于Sync
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)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
复制代码
公平锁和非公平锁的实现有细微的差异,可是差异不是很大。非公平锁和公平锁的不一样在于,非公平锁在lock()的时候,多了一段代码
//非公平锁的lock
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//公平锁的lock
final void lock() {
acquire(1);
}
复制代码
非公平锁在lock的时候,就会直接去尝试拿锁,若是尝试成功了,就直接占有锁。这是第一个不一样。
在tryAcquire()方法中,公平锁会多出!hasQueuedPredecessors()行这个代码,这段代码主要就是判断阻塞队列中是否已经有等待线程。
//公平锁
if (c == 0) {
if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
/**
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
*/
//非公平锁。
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
复制代码
这里公平锁会去判断队列中是否有线程在等待获取锁,只有当阻塞队列为空时,才会尝试去获取锁。
可是非公平锁不会检查阻塞队列中是否已经有线程等待,而是会直接去获取锁。
公平和非公平锁的实现差别就这些不一样,其余的实现逻辑都是差很少的。
前面咱们说到若是没有抢到锁,就会被LockSupport.park(this)挂起线程,那如何解锁,唤醒线程的呢,接下来咱们看一下。
public void unlock() {
sync.release(1);
}
//====此方法来自AQS================
public final boolean release(int arg) {
//尝试去释放锁
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
复制代码
这里会调用tryRelease()方法尝试去释放锁,若是释放锁成功了,判断头结点的状态,去唤醒线程。 这里须要说明的一点,head != null 咱们能理解,可是为何waitStatus != 0 呢。 咱们前面看了线程抢锁,只有一处给waitStatus赋值了。在shouldParkAfterFailedAcquire这个方法中,将前驱节点的 waitStatus设为Node.SIGNAL。能够往前翻一下。
除此之外,还有就是在初始化的时候enq()方法中,对waitStatus初始化的时候默认为0,其余地方没有对 waitStatus赋值。若是waitStatus != 0,那说明head后面没有被挂起等待唤醒的线程,也就不须要去唤醒。
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;
}
复制代码
太简单了,没有什么好说的。
private void unparkSuccessor(Node node) {
//咱们知道阻塞队列是一个先进先出的队列,唤醒的话,也是按照顺序唤醒的,咱们能够看到参数的Node是头结点
//若是头结点的waitStatus < 0 ,说
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//下面的代码就是唤醒后继节点,可是有可能后继节点取消了等待(waitStatus==1)
// 从队尾往前找,找到waitStatus<=0的全部节点中排在最前面的
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);
}
//唤醒线程之后,被唤醒的线程将从如下代码中继续往前走:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 刚刚线程被挂起在这里了
return Thread.interrupted();
}
// 又回到这个方法了:acquireQueued(final Node node, int arg),这个时候,node的前驱是head了
复制代码
到这里基本上吧ReenTranLock的获取锁、释放锁都分析完了,具体的一些细节可能没有说到,你们就本身去跟一下代码就能够了。
本片文章基于ReentranLock独占锁,分析了AQS了解到了一下几点,
本来的计划是一周输出一篇,可是临时遇上有一个紧急需求要作,上上周六加班,周天又和chessy大佬面基约了一个饭,上周也是天天很晚回去,周六加班也在加班赶需求,本周要提测,月底要上线,中间也是抽时间磕磕绊绊的写一点是一点,终于在昨天写完了。最近两周感受本身的精力被耗尽了,状态不是和好好,这几天把状态调整一下。上上周跟chessy大佬聊了不少,让我有不少感想,计划写一篇关于持续学习和我的成长方向的分享,你们到时候也能够互相交流一下心得。
码了这么多字也是不容易,那就点个赞支持一下呗。
参考
https://javadoop.com/2017/06/16/AbstractQueuedSynchronizer/