1、引言
在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问。synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现。
而从JDK1.5以后java界的一位大神—— Doug Lea 开发了AbstractQueuedSynchronizer(AQS)组件,使用原生java代码实现了synchronized语义。换句话说,Doug Lea没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,仅用一个普普通通的类就完成了代码块的并发访问控制,比那些费力不讨好的实现不知高到哪里去了。
java.util.concurrent包有多重要无需多言,一言以蔽之,是Doug Lea大爷对天下所有Java程序员的怜悯。
AQS定义了一套多线程访问共享资源的同步器框架,是整个java.util.concurrent包的基石,Lock、ReadWriteLock、CountDowndLatch、CyclicBarrier、Semaphore、ThreadPoolExecutor等都是在AQS的基础上实现的。
2、细说AQS
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。AQS的主要作用是为Java中的并发同步组件提供统一的底层支持,例如ReentrantLock,CountdowLatch就是基于AQS实现的,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。
在深入分析AQS之前,我想先从AQS的功能上说明下AQS,站在使用者的角度,AQS的功能可以分为两类:独占功能和共享功能,它的所有子类中,要么实现并使用了它独占功能的API,要么使用了共享锁的功能,而不会同时使用两套API,即便是它最有名的子类ReentrantReadWriteLock,也是通过两个内部类:读锁和写锁,分别实现的两套API来实现的,为什么这么做,后面我们再分析,到目前为止,我们只需要明白AQS在功能上有独占控制和共享控制两种功能即可。
3、自定义锁
AQS需实现方法:
AQS模板方法:
void acquire(int arg) | 模板方法,独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则会进入同步队列等待,此方法会调用子类重写的tryAcquire方法 |
void acquireInterruptibly(int arg) | 模板方法,与acquire相同,但是此方法可以响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,此方法会抛出InterruptedException并返回 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 模板方法,在acquireInterruptibly基础上增加了超时限制,如果当前线程在超时时间内没有获取到同步状态,则会返回false,如果获取到了则会返回true |
boolean release(int arg) | 模板方法,独占式的释放同步状态,该方法会在释放同步状态后,将同步队列中的第一个节点包含的线程唤醒 |
void acquireShared(int arg) | 模板方法,共享式的获取同步状态,如果当前系统未获取到同步状态,将会进入同步队列等待,与acquire的主要区别在于同一时刻可以有多个线程获取到同步状态 |
void acquireSharedInterruptibly(int arg) | 模板方法,与acquireShared一致,但是可以响应中断 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 模板方法,在acquireSharedInterruptibly基础上增加了超时限制 |
boolean releaseShared(int arg) | 模板方法,共享式的释放同步状态 |
Collection<Thread> getQueuedThreads() | 模板方法,获取等待在同步队列上的线程集合 |
自定义锁:
package multi;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyLock implements Lock{
private final Sync sync;
public MyLock(){
sync = new Sync();
}
private class Sync extends AbstractQueuedSynchronizer{
private static final long serialVersionUID = 4206028266355265870L;
protected boolean tryAcquire(int arg) {
int state = getState();
Thread currentThread = Thread.currentThread();
if(state == 0){
if(compareAndSetState(state, arg)){
setExclusiveOwnerThread(currentThread);
return true;
}
}
if(currentThread == getExclusiveOwnerThread()){
state = getState();
setState(state + 1);
return true;
}
return false;
}
protected boolean tryRelease(int arg) {
if(Thread.currentThread() != getExclusiveOwnerThread()){
throw new RuntimeException();
}
int state = getState() - arg;
if(state < 0 ){
throw new RuntimeException();
}
boolean flag = false;
if(state == 0){
// 先设定当前线程为空后,再设定state值
setExclusiveOwnerThread(null);
flag = true;
}
setState(state);
return flag;
}
protected Condition newCondition() {
return new ConditionObject();
}
}
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
4、节点和同步队列
AQS有一个静态内部类Node,即CHL队列中的节点对象,对象中包含了前驱节点,后驱节点,等待节点的后续节点,等待获取同步状态的线程引用、等待状态。
int waitStatus:
- 1、CANCELLED : 值为1,当线程等待超时或者被中断,则取消等待,设等待状态为-1,进入取消状态则不再变化。
- 2、SIGNAL : 值为-1。后继节点处于等待状态,当前节点(为-1)被取消或者中断时会通知后继节点,使后继节点的线程得以运行
- 3、CONDITION : 值为-2.当前节点处于等待队列,节点线程等待在Condition上,当其他线程对condition执行signall方法时,等待队列转移到同步队列,加入到对同步状态的获取。
- 4、PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
- 5、0状态:值为0,代表初始化状态。
Node prev:前驱节点,当节点加入同步队列的时候被设置(尾部添加)
Node next:后继节点
Node nextWaiter:Node既可以作为同步队列节点使用,也可以作为Condition的等待队列节点使用(将会在后面讲Condition时讲到)。在作为同步队列节点时,nextWaiter可能有两个值:EXCLUSIVE、SHARED标识当前节点是独占模式还是共享模式;在作为等待队列节点使用时,nextWaiter保存后继节点。
Thread thread:获取同步状态的线程。
同步队列是AQS很重要的组成部分,它是一个双端队列(其实就是数据结构双向链表),遵循FIFO原则,主要作用是用来存放在锁上阻塞的线程,当一个线程尝试获取锁时,如果已经被占用,那么当前线程就会被构造成一个Node节点加入到同步队列的尾部,队列的头节点是成功获取锁的节点,当头节点线程获得锁时,会唤醒后面的节点并释放当前头节点的引用。
- 同步队列设置尾节点(未获取到锁的线程加入同步队列): 同步器AQS中包含两个节点类型的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail),当一个线程成功的获取到锁(同步状态),其他线程无法获取到锁,而是被构造成节点(包含当前线程,等待状态)加入到同步队列中等待获取到锁的线程释放锁。这个加入队列的过程,必须要保证线程安全。否则如果多个线程的环境下,可能造成添加到队列等待的节点顺序错误,或者数量不对。因此同步器提供了CAS原子的设置尾节点的方法(保证一个未获取到同步状态的线程加入到同步队列后,下一个未获取的线程才能够加入)。 如下图,设置尾节点:
- 同步队列设置首节点(原头节点释放锁,唤醒后继节点):同步队列遵循FIFO,头节点是获取锁(同步状态)成功的节点,头节点在释放同步状态的时候,会唤醒后继节点,而后继节点将会在获取锁(同步状态)成功时候将自己设置为头节点。设置头节点是由获取锁(同步状态)成功的线程来完成的,由于只有一个线程能够获取同步状态,则设置头节点的方法不需要CAS保证,只需要将头节点设置成为原首节点的后继节点 ,并断开原头结点的next引用。如下图,设置首节点:
5、独占式的锁的获取
调用同步器的acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,即线程获取同步状态失败后进入同步队列,后续对线程进行中断操作时,线程不会从同步队列中移除。
(1) 当前线程实现通过tryAcquire()方法尝试获取锁,获取成功的话直接返回,如果尝试失败的话,进入同步队列排队,可以保证线程安全(CAS)的获取同步状态。
(2) 如果尝试获取锁失败的话,构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法,将节点加入到同步队列的队列尾部。
(3) 最后调用acquireQueued(final Node node, int args)方法,使该节点以死循环的方式获取同步状态,如果获取不到,则阻塞节点中的线程。acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点的时候才能尝试获取锁(同步状态)( p == head && tryAcquire(arg))。
原因是:
- 头结点是成功获取同步状态的节点,而头结点的线程释放锁以后,将唤醒后继节点,后继节点线程被唤醒后要检查自己的前驱节点是否为头结点。
- 维护同步队列的FIFO原则,节点进入同步队列以后,就进入了一个自旋的过程,每个节点(后者说每个线程)都在自省的观察。
下图为节点自旋检查自己的前驱节点是否为头结点:
独占式的锁的获取源码:
/**
* 独占式的获取同步状态
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
尝试获取锁:tryAcquire方法:如果获取到了锁,tryAcquire返回true,反之,返回false。
//方法2:
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取“独占锁”的状态,获取父类AQS的标志位
int c = getState();
//c == 0 意思是锁(同步状态)没有被任何线程所获取
//1.当前线程是否是同步队列中头结点Node,如果是的话,则获取该锁,设置锁的状态,并设置锁的拥有者为当前线程
if (c == 0) {
if (!hasQueuedPredecessors() &&
// 修改下状态为,这里的acquires的值是1,是写死的调用子类的lock的方法的时候
// 传进来的,如果c == 0,compareAndSetState操作会更新成功为1.
compareAndSetState(0, acquires)) {
// 上面CAS操作更新成功为1,表示当前线程获取到了锁,因为将当前线程设置为
// AQS的一个变量中,代表这个线程拿走了锁。
setExclusiveOwnerThread(current);
return true;
}
}
//2.如果c不为0,即状态不为0,表示锁已经被拿走。
//因为ReetrantLock是可重入锁,是可以重复lock和unlock的,所以这里还要判断一次,
//获取锁的线程是否为当前请求锁的线程。
else if (current == getExclusiveOwnerThread()) {
//如果是,state继续加1,这里nextc的结果就会 > 1,这个判断表示获取到的锁的线程,
//还可以再获取锁,这里就是说的可重入的意思
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
addWaiter方法的源码:回到aquire方法,如果尝试获取同步状态(锁)失败的话,则构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法将该节点加入到同步队列的队尾。
/**
* Creates and enqueues node for current thread and given mode.
* 如果尝试获取同步状态失败的话,则构造同步节点(独占式的Node.EXCLUSIVE),
* 通过addWaiter(Node node,int args)方法将该节点加入到同步队列的队尾。
*
*/
private Node addWaiter(Node mode) {
// 用当前线程构造一个Node对象,mode是一个表示Node类型的字段,
// 或者说是这个节点是独占的还是共享的,或者说AQS的这个队列中,
// 哪些节点是独占的,哪些节点是共享的。
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;
// 确保节点能够被线程安全的添加,使用CAS方法
// 尝试修改为节点为最新的节点,如果修改失败,意味着有并发,
// 这个时候进入enq中的死循环,进行“自旋”的方式修改
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进入自旋
enq(node);
return node;
}
enq方法的源码:同步器通过死循环的方式来保证节点的正确添加,在“死循环” 中通过CAS将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则当前线程不断的尝试设置。enq方法将并发添加节点的请求通过CAS变得“串行化”了。
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;
}
}
}
}
acquireQueued方法:在队列中的线程获取锁的过程:
/**
* acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点才能尝试获取
同步状态(锁)( p == head && tryAcquire(arg))
*原因是:1.头结点是成功获取同步状态(锁)的节点,而头节点的线程释放了同步状态以后,
将会唤醒其后继节点,后继节点的线程被唤醒后要检查自己的前驱节点是否为头结点。
* 2.维护同步队列的FIFO原则,节点进入同步队列之后,就进入了一个自旋的过程,
每个节点(或者说是每个线程)都在自省的观察。
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环检查(自旋检查)当前节点的前驱节点是否为头结点,才能获取锁
for (;;) {
// 获取节点的前驱节点
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) &&
//如果需要挂起,借助JUC包下面的LockSupport类的静态方法park挂起当前线程,直到被唤醒
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果有异常
if (failed)
//取消请求,将当前节点从队列中移除
cancelAcquire(node);
}
}
独占锁的释放:下面直接看源码:
/*
1. unlock():unlock()是解锁函数,它是通过AQS的release()函数来实现的。
* 在这里,“1”的含义和“获取锁的函数acquire(1)的含义”一样,它是设置“释放锁的状态”的参数。
* 由于“公平锁”是可重入的,所以对于同一个线程,每释放锁一次,锁的状态-1。
unlock()在ReentrantLock.java中实现的,源码如下:
*/
public void unlock() {
sync.release(1);
}
release()会调用tryRelease方法尝试释放当前线程持有的锁(同步状态),成功的话唤醒后继线程,并返回true,否则直接返回false。
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() 尝试释放当前线程的同步状态(锁)
protected final boolean tryRelease(int releases) {
//c为释放后的同步状态
int c = getState() - releases;
//判断当前释放锁的线程是否为获取到锁(同步状态)的线程,不是抛出异常(非法监视器状态异常)
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果锁(同步状态)已经被当前线程彻底释放,则设置锁的持有者为null,同步状态(锁)变的可获取
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁成功后,找到AQS的头结点,并唤醒它即可:
// 4. 唤醒头结点的后继节点
private void unparkSuccessor(Node node) {
//获取头结点(线程)的状态
int ws = node.waitStatus;
//如果状态<0,设置当前线程对应的锁的状态为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//解释:Thread to unpark is held in successor, which is normally just the next node.
//But if cancelled or apparently(显然) null, traverse backwards(向后遍历) from tail to find the actual(实际的) non-cancelled successor(前继节点).
//从队列尾部开始往前去找最前面的一个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);
}
上面说的是ReentrantLock的公平锁获取和释放的AQS的源码,唯独还剩下一个非公平锁NonfairSync没说,其实,它和公平锁的唯一区别就是获取锁的方式不同,公平锁是按前后顺序一次获取锁,非公平锁是抢占式的获取锁,那ReentrantLock中的非公平锁NonfairSync是怎么实现的呢?
/**
* Sync object for non-fair locks
*/
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);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁会先进行!hasQueuedPredecessors()判断,为true时在进行CAS操作。而非公平锁的lock先直接用cas判断state变量是否为0(尝试获取锁),成功的话更新成1,表示当前线程获取到了锁,不需要在排队,从而直接抢占的目的。而对于公平锁的lock方法是一开始就走AQS的双向队列排队获取锁。
非公平锁的效率比公平锁的效率高。因为公平锁的线程切换次数大于非公平锁(释放后在获取的几率高),会造成额外的开销。但是非公平会造成线程饥饿的问题。
总结:在获取同步状态的时候,同步器维护一个同步队列,获取失败的线程会被加入到队列中并在队列中自旋;移除队列(或停止自旋)的条件是前驱节点为头结点并且获取到了同步状态。在释放同步状态时,同步器调用tryRelease(int args)方法释放同步状态,然后唤醒头结点的后继节点。AQS的实现思路其实并不复杂,用一句话准确的描述的话,其实就是使用标志状态位status(volatile int state)和 一个双向队列的入队和出队来实现。AQS维护一个线程何时访问的状态,它只是对状态负责,而这个状态的含义,子类可以自己去定义。
参考:
《Java并发编程的艺术》