1 AQS 锁的底层支持
AQS的全称是AbstractQueuedSynchronizer,抽象同步队列。ReentrantLock底层使用了AQS(内部静态类Sync实现了AQS),ThreadPoolExecutor底层也是用了AQS(内部类Worker实现了AQS)。
AQS是一个FIFO的双向队列,其内部通过结点head和tail记录队首和队尾元素。
1.1 内部静态类Node
- Thread 用来存放进入AQS队列里面的线程
- SHARED 用来标记该线程是获取共享资源是阻塞挂起后放入AQS队列
- EXCLUSIVE 用来标记线程是获取独占资源时被挂起后放入AQS队列
- waitStatus 记录当前线程等待状态
* CANCELLED 线程取消了
* SIGNAL 线程需要被唤醒
* CONDITION 线程在条件队列里等待
* PROPAGATE 释放共享资源时需要通知其他结点 - pre 记录当前结点的前驱结点
- next 记录当前结点的后继结点
1.2 内部类ConditionObject
ConditionObject用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量。每个条件变量对应一个条件队列(单向链表),用来存放调用条件变量的await方法后被阻塞的线程。
条件队列的头部元素是firstWaiter,尾部元素是lastWaiter。
1.3 AQS中成语变量state的作用
/**
* The synchronization state.
*/
private volatile int state; // 使用volatile,保证内存的可见性
对于AQS来说,线程同步的关键是对状态值state进行操作。根据state是否是属于一个线程,操作state的方式分为独占方式和共享方式。以下6个方法的方法参数的值都是state的值。
1.3.1 使用独占方式获取和释放资源的方法
- public final void acquire(int arg)
- public final void acquireInterruptibly(int arg)
- public final boolean release(int arg)
使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前资源不是自己持有的,就会在获取失败后被阻塞。
1.3.2 使用共享方式获取和释放资源的方法
- public final void acquireShared(int arg)
- public final void acquireSharedInterruptibly(int arg)
- public final boolean releaseShared(int arg)
共享方式是与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到资源后,另外一个线程再次获取资源时,如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。
2 独占方式下的获取资源与释放资源
2.1 获取资源的方法
当一个线程调用acquire(int arg)获取独占资源时,会首先使用tryAcquire尝试获取资源,具体是设置state的状态,成功则则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node结点插入到AQS阻塞队列的尾部,并调用 LockSupport.park()挂起自己。
// 头部结点
private transient volatile Node head;
// 尾部结点
private transient volatile Node tail;
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 自我中断
selfInterrupt();
}
// 需要子类去实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 添加结点到AQS阻塞队列尾部
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 快速添加到尾部
Node pred = tail;
if (pred != null) {
// 设置node的前驱结点
node.prev = pred;
// 通过CAS设置node为tail结点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 上一步失败,则通过enq入队
enq(node);
return node;
}
// 入队操作,双向链表的插入,初始化waitStatus为0
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
// 初始化,设置哨兵结点,设置成功,tail也指向哨兵结点
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 设置node的前驱结点
node.prev = t;
// 通过CAS设置node为tail结点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
// 比较是否成功拿到资源
boolean failed = true;
try {
// 标记等待过程是否被中断
boolean interrupted = false;
// 自旋
for (;;) {
// 获取当前结点的上一个结点
final Node p = node.predecessor();
// 如果p是头结点,并且当前结点尝试获取资源成功
if (p == head && tryAcquire(arg)) {
// 获取资源后,设置该结点为head结点
setHead(node);
// 此时head的prev和next均为null,方便GC回收之前的head结点,意味获取资源的结点已经出队列了
p.next = null; // help GC
// 获取资源成功
failed = false;
// 返回等待过程中是否被中断
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果等待过程中被中断过,就将interrupted标记为true
interrupted = true;
}
} finally {
if (failed)
// 如果等待过程中没有成功获取资源,就取消结点在队列里的等待
cancelAcquire(node);
}
}
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);
// 把找到的这个结点的后继结点设为当前结点node
pred.next = node;
} else {
/*
* 通过CAS,把找到的前驱结点的状态设置为SIGNAL
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
// 调用park()使线程进入waiting状态
LockSupport.park(this);
// 如果被唤醒,查看自己是不是被中断的
return Thread.interrupted();
}
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:
1)被unpark();2)被interrupt()
Thread.interrupted()会清除当前线程的中断标记位
具体流程如下:
1.调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
2.没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
3.acquireQueued()使线程在阻塞队列中等待,有机会时(轮到自己,会被unpark())会去尝试获取资源。直至获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
2.2 释放资源的方法
当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态量state是否能满足自己的需要,满足则该线程被激活,然后继续向下执行,否则还是会被放入AQS队列并被挂起。
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 找到头结点
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒AQS阻塞队列里的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
// 需要子类实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 注意,理解的时候node是当前线程所在的结点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果当前线程的waitStatus小于0,就把当前线程的waitStatus通过AQS置为0,允许失败
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//找到下一个需要唤醒的结点s
Node s = node.next;
// 如果没空或者已经取消
if (s == null || s.waitStatus > 0) {
s = null;
// 从后面往前找
for (Node t = tail; t != null && t != node; t = t.prev)
// 如果找到的结点的waitStatus <= 0,就是有效结点,就把该结点赋给s
// 找到距离node(当前结点)最近的有效结点,赋值给s,可以看做是FIFO
if (t.waitStatus <= 0)
s = t;
}
// 如果结点s不为空,唤醒s结点
if (s != null)
LockSupport.unpark(s.thread);
}
2.3 acquire和release的小结
- AQS并没有提供可用的tryAcquire和tryRelease方法,tryAcquire和tryRelease需要由具体的子类去实现,比如ReentrantLock的内部静态类Sync,ThreadPoolExecutor的内部类Worker
- unparkSuccessor()和acquireQueued()联系起来一起看
- 用unpark()唤醒AQS阻塞队列中最前边的,waitStatus<=0的线程s
- 线程s被唤醒后,进入if (p == head && tryAcquire(arg))的判断,即便即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个合适的位置。这里既然s已经是AQS等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立
- s把自己设置成head结点,表示自己已经获取到资源了,acquire()也返回
3 共享方式下的获取资源与释放资源
3.1 获取资源的方法
当线程调用accquireShared(int arg)获取共享资源的时候,会首先使用tryAcquireShared尝试获取资源,具体是设置状态量state的值,成功则会直接返回,失败则将当前线程封装为类型Node.SHARED的Node结点插入到AQS阻塞队列的尾部,LockSupport.park(this)挂起自己。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* a negative value on failure; zero if acquisition in shared
* mode succeeded but no subsequent shared-mode acquire can
* succeed; and a positive value if acquisition in shared
* mode succeeded and subsequent shared-mode acquires might
* also succeed, in which case a subsequent waiting thread
* must check availability.
*/
// 需要子类去实现
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
private void doAcquireShared(int arg) {
// 加入到队列尾部
final Node node = addWaiter(Node.SHARED);
// 是否成功标志
boolean failed = true;
try {
// 等待过程中是否被中断的标志
boolean interrupted = false;
// 自旋
for (;;) {
// 获取前驱结点
final Node p = node.predecessor();
// 如果p结点是头结点,head结点是拿到资源的结点
// 此时node被唤醒,很可能head用完资源来唤醒自己
if (p == head) {
// 尝试获取资源
int r = tryAcquireShared(arg);
// 获取资源成功
if (r >= 0) {
// 将head指向自己,还有剩余资源可以再唤醒之后的线程
setHeadAndPropagate(node, r);
// 将之前的头结点出队列,方便GC回收
p.next = null; // help GC
// 如果等待过程中被中断过,将中断补上
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 判断状态,寻找合适的位置,进入waiting状态
// 等着被unpark()或interrupt()
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 将head结点指向自己
setHead(node);
// 如果还有剩余量,继续唤醒下一个邻居线程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
总结一下上面的具体流程:
- tryAcquireShared()尝试获取资源,成功则直接返回
- 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。多个线程拿到资源后,还会去唤醒后继结点的操作
强调一点:AQS已经定义好了tryAcquireShared()的返回值
- 负值代表获取失败
- 0代表获取成功,但没有剩余资源
- 正数表示获取成功,还有剩余资源,其他线程还可以去获取
留一个思考题:
AQS保证严格按照入队顺序唤醒,保证了公平,但是降低了并发,什么情况下会出现这样的场景呢?
3.2 释放资源的方法
当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS阻塞队列里面的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,继续向下运行,否则还是会被放入AQS阻塞队列并被挂起。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 需要子类去实现
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
private void doReleaseShared() {
// 自旋
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// CAS更改状态失败,继续更改,直至成功
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 唤醒后继结点
unparkSuccessor(h);
}
// 如果waitStatus为0,CAS更改waitStatus为PROPAGATE,失败则继续进行
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// head发生改变,继续循环
if (h == head) // loop if head changed
break;
}
}
2.3 acquireShared和releaseShared的小结
AQS并没有提供可用的tryAcquireShared和tryReleaseShared方法,tryAcquireShared和tryReleaseShared需要有具体的子类去实现。子类在实现tryAcquireShared和tryReleaseShared要根据具体场景使用CAS算法尝试修改state状态值,成功返回true,失败返回false。
4 关于方法中带Interruptibly和不带Interruptibly的区别
笔者看源码的时候,独占方式有两个方法,acquire(int arg)和acquireInterruptibly(int arg),共享模式下有两个方法,acquireShared(int arg)和acquireSharedInterruptibly(int arg),那么这里会有什么区别呢?
不带Interruptibly的就是不对中断进行响应,也就是线程在调用了不带Interruptibly关键字的方法获取资源时或者获取资源失败被挂起时,其他线程中断了该线程,那么该线程不会因为被中断而抛出异常,它还是继续获取资源或者被挂起,直白一点,就是不对中断进行响应,忽略中断。
带Interruptibly的方法要对中断进行响应,也就是线程在调用Interruptibly方法获取资源时或者获取资源失败挂起时,其他线程中断了该线程,那么该线程会抛出InterruptedExeception异常而返回。
5 AQS 条件变量
想必读者都知道notify和wait,是配合synchronized内置锁实现线程间同步的基础设施。条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。
二者的不同之处
synchronized同时只能与一个共享变量的notify或await方法实现同步
AQS实现的锁,可以对应多个条件变量,多个条件变量就可以有多个条件队列。
研究一下什么是条件队列
// 创建独占锁
public ReentrantLock lock = new ReentrantLock();
// 创建条件变量
public Condition condition = lock.newCondition();
// 线程1
lock.lock();
try {
System.out.println("begin wait");
condition.await();
System.out.println("end wait");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
// 线程2
lock.lock();
try {
System.out.println("begin signal");
condition.signal();
System.out.println("end signal");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
线程1首先获取独占锁,调用条件变量的await()方法阻塞挂起当前的线程。当其他线程调用条件变量的signal方法时,被阻塞的线程才会从await出返回。最后线程1释放锁。线程2获取获取锁,调用条件变量的signal()方法,线程1从await出返回,线程2释放锁之后,重新获取锁。
不难分析出,这里的Lock对象等价于synchronized加上共享变量,调用lock.lock()就相当于进入了synchronized块(获取了共享变量的内置锁),调用lock.unlock()就相当于退出了synchronized块。调用条件变量的await()方法就相当于调用共享变量wait()方法,调用条件变量的signal()方法就相当于调用共享变量notify()方法,调用条件变量的signalAll()方法就相当于调用共享变量notifyAll()方法。
其实lock.newCondition()的作用就是new了一个在AQS内部声明的ConditionObject对象,ConditionObject是AQS的内部类,可以访问AQS内部的变量(例如状态变量state)和方法。每个条件变量内部维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。注意下,条件队列和AQS阻塞队列不是一回事。
在如下代码中,当线程调用条件变量await()方法时(必须要先调用lock()获取锁),在内部会构造一个类型为Node.CONDITION的node结点,然后将该节点插入到条件队列尾部,之后当前线程会是释放获取的锁(操作锁对应的state变量的值),并被阻塞挂起。这时候如果有其他线程调用了lock.lock()尝试获取锁,就会有一个线程获取到锁,如果获取到锁的线程调用了条件变量的await()方法,则该线程也会被放入到条件变量的阻塞队列里面,然后释放获取到的锁,并在await()方法处阻塞。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 创建新的node结点,并插入到条件队列末尾
Node node = addConditionWaiter();
// 释放当前线程获取的锁
int savedState = fullyRelease(node);
// 设置中断标志
int interruptMode = 0;
// 如果不在AQS阻塞队列
while (!isOnSyncQueue(node)) {
// 挂起当前线程
LockSupport.park(this);
// 如果线程等待的时候被中断,中断标志的值发生改变,跳出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 从AQS阻塞队列中获取资源,留个疑点,同步队列的结点怎么到了阻塞队列?
// 通过这里也能看出,条件队列只适合独占锁的方式
// THROW_IE看下面的解释
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 进行到这里,说明已经成功获取到独占锁
// 删除条件队列中被取消的节点
if (node.nextWaiter != null) // clean up if cancelled
// 遍历清除status不为Node.CONDITION的结点
unlinkCancelledWaiters();
// 根据不同模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
/** 等待退出时抛出InterruptedException异常*/
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1;
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
// 如果最后一个节点被取消,则删除队列中被取消的节点
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 创建一个类型为CONDITION的节点并加入队列
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
// 入参就是当前结点
final int fullyRelease(Node node) {
// 设置释放锁成功的标志
boolean failed = true;
try {
// 获取当前的state,这里也说明必须是独占方式
int savedState = getState();
// 就是独占方式的释放锁
if (release(savedState)) {
failed = false;
// 释放锁成功,返回释放之后的state
return savedState;
} else {
// 释放失败,则抛出IllegalMonitorStateException异常
throw new IllegalMonitorStateException();
}
} finally {
// 最后如果释放锁失败
if (failed)
// 则把当前结点的状态改为Node.CANCELLED
// 从这里也可以看出,为什么检测最后一个结点的waitStatus
node.waitStatus = Node.CANCELLED;
}
}
// 判断是否在同步队列里面
final boolean isOnSyncQueue(Node node) {
// 快速判断:结点状态或者结点的前驱是不是为null,注:条件队列是单向链表
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 快速判断:next字段只有同步队列才会使用,条件队列中使用的是nextWaiter字段
if (node.next != null) // If has successor, it must be on queue
return true;
// 上述无法判断,则进入findNodeFromTail进行判断
return findNodeFromTail(node);
}
// 注意这里用的是tail,这是因为条件队列中的节点是被加入到同步队列尾部,这样查找更快
// 从同步队列尾节点开始向前查找当前节点,如果找到则说明在,否则不在
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
/** 等待退出时重新中断*/
/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT = 1;
/** 等待退出时抛出InterruptedException异常*/
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1;
// 在条件队列等待的过程中,检查是否被中断
private int checkInterruptWhileWaiting(Node node) {
// 如果不是中断,即正常被signal唤醒则返回0
// 如果结点由中断加入同步队列则返回THROW_IE,由signal加入同步队列则返回REINTERRUPT
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
// 被中断后
// 返回true表示结点由中断加入同步队列,返回false表示由signal加入同步队列
final boolean transferAfterCancelledWait(Node node) {
// CAS设置结点状态为0,如果成功则加入同步队列
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 添加结点到AQS阻塞队列尾部
enq(node);
return true;
}
// 如果上面设置失败,说明节点已经被signal唤醒
// 由于signal操作会将结点加入AQS阻塞队列,只需自旋等待即可
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
// 根据中断时机选择抛出异常或者设置线程中断状态
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
// 实际上,就是Thread.currentThread().interrupt();
selfInterrupt();
}
如下代码,当另外一个线程调用条件变量的signal方法时(必须先调用锁的lock()方法获取锁),在内部会把条件队列里面的头结点从条件队列里面移除并放入AQS阻塞队列里面,然后激活这个线程。
public final void signal() {
// 如果不是独占锁,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
// 将条件队列的头元素移动到AQS阻塞队列
doSignal(first);
}
// 把一个有效结点从条件队列删除,并把它添加到AQS阻塞队列里面
// 如果失败,则会查找条件队列上等待的下一个结点,直到队列为空
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 将结点加到AQS阻塞队列
final boolean transferForSignal(Node node) {
// 修改结点状态
// 如果修改失败只,有一种可能就是该结点被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 添加到AQS阻塞队列,并获得当前结点的前置结点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前置结点被取消或者修改状态失败则直接唤醒当前节点
// 此时当前已经在AQS阻塞队列里面,唤醒会进行锁获取或者正确的挂起操作
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
最后总结一下,一个锁对应一个AQS阻塞队列,多个多个条件变量,每个条件变量有自己的一个条件队列。
6 感悟与寄语
学习AQS是一个枯燥漫长的过程,笔者先看了ThreadPoolExecutor和ScheduledThreadPoolExecutor,线程池里的内部类Worker继承了AQS,当时有很多方法不明白,比如tryAcquire(),tryRelease(),为什么通过state变量来控制资源的获取和释放,无法理解Doug Lea写线程池的精髓,经过学习AQS的过程,此有了一点小小的感悟,希望可以给读者带来一点作用。
在这里特别感谢《Java并发编程之美》这本的书的作者,给我理清了思路。还有深入浅出AQS之条件队列这篇文章,把条件队列讲述的明明白白,感谢两位作者的精彩分享。