引言:接上一篇博文java线程池的阻塞队列详解,讲到线程池复用时用的阻塞队列时用到了Lock和Condition,这一篇对这AQS和Condition进行讲解
AQS是什么
AQS的全称是AbstractQueuedSynchronizer,是抽象队列同步器,是一个用来构建锁和同步器的框架,是java自带的synchronized关键字之外的锁机制,常用的有ReentrantLock、 CountDownLatch(栅栏)、Semaphore(信号量)都基于他实现,它核心思想在于“如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,其他线程进入到aqs队列中进行排队等待”,他的核心数据结构是,int state同步状态,双向队列(Node tail +Node head)、以及cas自旋。
定义了两种资源共享方式:
- 独占:ReentrantLock
- 共享:CountDownLatch、Semaphore
AbstractQueuedSynchronizer维护了一个state状态和一个先进先出的队列:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
...
/**
* The synchronization state.
*/
private volatile int state;
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* Wait queue node class.
**/
static final class Node {
...
}
...
}
因为AbstractQueuedSynchronizer是一个抽象类,他采用模板方法的设计模式,规定了独占和共享模式需要实现的方法,并且将一些通用的功能已经进行了实现,所以不同模式的使用方式,只需要自己定义好实现共享资源的获取与释放即可,至于具体线程在等待队列中的维护(获取资源入队列、唤醒出队列等),AQS已经实现好了。
根据根据共享资源的模式一般实现的方法有如下几个:
isHeldExclusively();// 是否为独占模式;但是只有使用到了Condition的,才需要去实现它。例如:ReentrantLock。
boolean tryAcquire(int arg); // 独占模式;尝试获取资源,成功返回true,失败返回false。
boolean tryRelease(int arg) ; // 独占模式;尝试释放资源,成功返回true,失败返回false。
int tryAcquireShared(int arg); // 共享模式;尝试获取资源,负数表示失败;0表示成功,但是没有剩余可用资源了;正数表示成功,且有剩余可用资源。
boolean tryReleaseShared(int arg) ; // 共享欧式;尝试释放资源,若释放资源后允许唤醒后续等待节点返回true,否则返回false。
上面的这几个方法在AbstractQueuedSynchronizer这个抽象类中,都没有被定义为abstract的,说明这些方法都是可以按需实现的,共享模式下可以只实现共享模式的方法(例如CountDownLatch),独占模式下可以只实现独占模式的方法(例如ReentrantLock),也支持两种都实现,两种模式都使用(例如ReentrantReadWriteLock)。
AQS源码解析-以ReentrantLock为例子
ReentrantLock中实现了Lock接口,他的内部类Sync继承自AbstractQueuedSynchronizer,具体的实现又由FairSync和unFairSync继承自Sync抽象类,当调用reentrantLock.lock方法时,实质上是调用了aqs的acquire方法:
由于RentrantLock默认是非公平锁,就是说,线程在竞争锁的时候并不是按照先来后到的顺序来获取锁的,但是ReentrantLock
也是支持公平锁的,在创建的时候传入一个参数值即可。下面以为非公锁为例
java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
}
compareAndSetState的逻辑很简单,就是通过cas设置到state同步状态字段中,如果A线程设置成功了就直接返回了,那么B线程在A释放前执行本方法进入acquire(1)逻辑中,里面会阻塞当前线程,从而进入阻塞状态
AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire
由上可知,同一个线程多次设置,均可以返回true,从而acquire()方法能够正常返回,而其他线程设置失败返回false,也就是竞争锁失败的线程,则需要执行addWaiter方法和acquireQueued方法
java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
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.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWaiter()方法是将当前线程封装成Node节点,然后进行入队列的操作,并对头尾节点,next节点等进行设置
java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued
acquireQueued会不断尝试从waiter队列中尝试出队(for死循环),只要出队了意味着当前线程可以执行同步语句块中的内容(由lock和unlock包嵌的代码块),底层是通过LockSupport.park unpark()进行线程间的通讯,线程A调用unlock时会调用unpark()
注意:只有前驱节点是head节点的Node,才有机会执行tryAcquire()方法尝试获取锁,其他进入waiter队列的其他节点只能继续等待
以上就是整个AQS的ReentrantLock如何使用同步队列实现的线程阻塞和唤醒操作的流程代码。对于调用lock()方法的线程,阻塞的线程状态是WAITING态,调用带timeout参数的tryLock方法,则是TIMED_WAITING状态,而synchronized同步语句块阻塞的线程则是BLOCKED状态.
简单来说,就是AQS维护了一个同步队列,所有抢占锁失败的线程都在加入到队列尾部,随后线程会阻塞;释放锁时会唤醒头结点的后继结点,使其加入对同步状态的争夺中。其中head节点就是当前抢占到了锁正在执行同步代码的节点(除开第一次lock直接compareAndSetstate成功的)
PS: 调用ReentrantLock的lock()方法和unlock()方法,是不会调用到condition.await()和condition.signal()方法的,调用的是LockSupport.park(thread) 和LockSupport.unpark(thread)方法!!!!!!!!!!!!
什么是Condition?
首先Condition是一个抽象类,他的具体实现是在AbstractQueuedSynchronizer的内部类ConditionObject作了具体实现,通过ReentrantLock的newCondition()方法可以获取到一个condition实例,通过condition,可以实现类似Object中的wait() notify()相同的效果,来达到线程之间通讯的目的。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
我们在上一段中说到的都是aqs的同步队列,存储的是被lock()方法阻塞住的线程,所有被lock阻塞的线程都会进入同步队列中,直到拿到锁的线程释放后才可以执行。但我们知道拿到锁的线程也可以主动调用await方法,来释放锁,这时候,这个主动调用await方法的线程,又会存储到哪里?答案就是condition的条件等待队列。
一开始A拿到了同步锁,B获取锁失败,因此进入到AQS的同步等待队列中,此时同步队列和条件队列如下图所示:
后来A主动调用condition.await方法释放了锁,此时B会尝试获取锁,此时两个队列状态如下:
当B运行时候,会调用signal方法,将Condition队列中的线程加入到AQS队列中
B执行完毕后,A会尝试获取同步锁。最终得以继续执行。