建议先阅读 【线程、锁】什么是AQS(锁分类:自旋锁、共享锁、独占锁、读写锁)
介绍ReentrantLock之前,你知道ReentrantLock有什么优点吗?为何已经有了synchronized还要ReentrantLock?参见 《synchronized 和reentrantlock的优缺点》
ReentrantLock 默认采用非公平锁
,除非你在构造方法中传入参数 true :
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队(即先进先出队列),永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
可以简单的理解一定会乖乖的去排队,先排队的先执行,后排队的后执行。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
非公平锁不是简单的插队到第一个位置就行了。实际情况是这样的:非公平锁模式下,如果某个对象没有锁(有2种情况无锁,一是当前线程恰好是第一个工作线程,此时不存在插队概念;二是某个持有锁的线程刚刚释放锁,等待队列的线程还未被唤醒,我们这里的非公平锁当然特指第二种情况),恰好线程A尝试加锁,线程A会和即将被从等待队列唤醒的那些线程们一起竞争锁,也就是说线程A可能成功,也可能失败,失败了就乖乖去队列中睡眠,等待被唤醒。
Sync类,是ReentrantLock他本身的一个内部类,他继承了AbstractQueuedSynchronizer,我们在操作锁的大部分操作,都是Sync本身去实现的。
Sync又分别有两个子类:FairSync和NofairSync,也是ReentrantLock的内部类,具体的实现,其实就是这两个内部类。
真正的加锁逻辑的入口是FairSync
和NofairSync
的lock()
方法
AQS提供了一种实现阻塞锁
和一系列依赖FIFO等待队列
的同步器的框架,如下图所示。
AQS具备的特性:
state是一个计数器,默认无锁时,值是0,初次获取锁,会改变为!0的值,支持可重入锁机制,计数器会累加,释放锁累减,直至恢复为0,表明完全释放锁。
如上图所示:exclusiveOwnerThread指向持有锁的线程
AQS为一系列同步器依赖于一个单独的原子变量(state
,本身不是原子态,采用CAS修改保证原子态)的同步器提供了一个非常有用的基础。子类们必须定义改变state变量的protected方法,这些方法定义了state是如何被获取或释放的。鉴于此,本类中的其他方法执行所有的排队和阻塞机制。子类也可以维护其他的state变量,但是为了保证同步,必须原子地操作这些变量。
getState()
setState()
compareAndSetState()
这三种叫做均是原子操作,其中compareAndSetState的实现依赖于Unsafe的compareAndSwapInt()方法。代码实现如下:
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);
}
AQS定义两种资源共享方式:
共享锁调用acquireShared()
独占锁调用acquire
acquire(int arg)是获取锁的模板方法,接收一个参数,表示独占锁或共享锁,对于ReentrantLock来说,是以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
该方法是线程获取共享资源的顶层入口。获取到资源后,线程就可以去执行其临界区代码了。下面是acquire()的源码:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
通过代码的注释我们知道,acquire方法是一种互斥模式,且忽略中断
。该方法至少执行一次tryAcquire(int)方法,如果tryAcquire(int)方法返回true,则acquire直接结束,否则当前线程需要进入队列进行排队,然后阻塞、唤醒,不断循环,直至最后获取到锁。
函数流程如下:
自旋
,获取锁,获取失败,可能进入part阻塞态,等待唤醒,然后不断重复,直至成功。如果在整个等待过程中被中断过,则返回true,否则返回false。队列节点的状态,waitStatus(队列节点的一个属性)表示当前线程的等待状态:
①CANCELLED=1
:表示线程因为中断或者等待超时,需要从等待队列中取消等待
;
②SIGNAL=-1
:当前线程thread1占有锁,队列中的head(仅仅代表头结点,里面没有存放线程引用)的后继结点thread2处于等待状态,如果已占有锁的线程thread1释放锁或被CANCEL之后就会通知这个结点thread2去获取锁执行
。
③CONDITION=-2
:表示节点在等待队列中(这里指的是等待在某个lock的condition上,关于Condition的原理下面会写到),当持有锁的线程调用了Condition的signal()方法之后,节点会从该condition的等待队列转移到该lock的同步队列上,去竞争lock。(注意:这里的同步队列就是我们说的AQS维护的FIFO队列,等待队列则是每个condition关联的队列)
④PROPAGTE=-3
:表示下一次共享状态获取将会传递给后继结点获取这个共享同步状态。
waitStatus默认值为0,新建一个node节点值为0
tryAcquire()尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false。
该方法可以用于实现Lock中的tryLock()方法。该方法的默认实现是抛出UnsupportedOperationException
,具体实现由自定义的扩展了AQS的同步类来实现,比如ReentrantLock
中的FairSync
和NonfairSync
。
AQS在这里只负责定义了一个公共的方法框架。这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法用于将当前线程根据不同的模式(Node.EXCLUSIVE互斥模式、Node.SHARED共享模式
)加入到等待队列的队尾
,并返回当前线程所在的节点。
如果队列不为空,则以通过compareAndSetTail方法以CAS的方式将当前线程节点加入到等待队列的末尾。否则,通过enq(node)方法初始化一个等待队列,并返回当前节点。源码如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private Node addWaiter(Node mode) {
//(1)将当前线程以及阻塞原因(是因为SHARED模式获取state失败还是EXCLUSIVE获取失败)构造为Node结点
Node node = new Node(Thread.currentThread(), mode);
//(2)这一步是快速将当前线程插入队列尾部
Node pred = tail;
//tail节点不为空,说明队列已初始化,可以尝试添加至队列尾,否则就要进入enq先进行初始化操作
if (pred != null) {
//(2-1)将构造后的node结点的前驱结点设置为tail
node.prev = pred;
//(2-2)以CAS的方式设置当前的node结点为tail结点
if (compareAndSetTail(pred, node)) {
//(2-3)CAS设置成功,就将原来的tail的next结点设置为当前的node结点。这样这个双向队
//列就更新完成了
pred.next = node;
return node;
}
}
//自旋,直到成功加入队列
//(3)执行到这里,说明要么当前队列为null,要么存在多个线程竞争失败都去将自己设置为tail结点,
//那么就会有线程在上面(2-2)的CAS设置中失败,就会到这里调用enq方法
enq(node);
return node;
}
enq(node)用于将当前节点插入等待队列,如果队列为空,则初始化当前队列。整个过程以CAS自旋
的方式进行,直到成功加入队尾为止。
源码如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private Node enq(final Node node) {
for (;;) { //自旋
//(1)还是先获取当前队列的tail结点
Node t = tail;
//(2)如果tail为null,表示当前同步队列为null,就必须初始化这个同步队列的head和tail
if (t == null) {
//(2-1)初始情况下,可能多个线程竞争失败,在检查的时候都发现没有head结点,所以需要CAS的设置head结点
if (compareAndSetHead(new Node()))
tail = head;
} else { //(3) tail不为null,队列已初始化
//(3-1)直接将当前结点的前驱结点设置为tail结点
node.prev = t;
//(3-2)前驱结点设置完毕之后,还需要以CAS的方式将自己设置为tail结点,如果设置失败,就会重新进入循环判断一遍
if (compareAndSetTail(t, node)) { //CAS往队列尾添加节点
t.next = node;
return t;
}
}
}
}
acquireQueued()用于队列中的线程自旋
地以独占(ReentrantLock的实现,其他的不一定是独占)且不可中断的方式获取同步状态(acquire),直到拿到锁之后再返回。
该方法的实现分成两部分:
如果当前节点符合成为新的头结点的条件,尝试获取锁(tryAcquire),如果成功,返回;
一般来说,当前持有锁的线程
thread1
(也就是头节点)释放锁后, 不会立即删除自己,而是由被唤醒的下一个节点thread2
来负责删除,假设thread2
醒来后,发现自己的前区节点thread1是头节点,刚好符合FIFO规则按次序唤醒,说明自己是符合获取锁条件的。那么是什么醒来后不符合呢?线程中断,假设thread3是阻塞状态,中途被中断提前醒来了,必须让其重新阻塞,不能持有锁,否则不符合FIFO,详细步骤参见下文截图中的步骤1和步骤2
获取锁失败则检查当前节点是否应该被park阻塞,然后将该线程park并且检查当前线程是否被可以被中断。
简单来说,就是自旋判断当前线程对应的节点,如果是头节点,符合唤醒状态,调用tryAcquire()方法去获取锁,失败了就par,,直至成功
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//在这样一个循环中尝试tryAcquire同步状态
for (;;) {
//获取前驱结点
final Node p = node.predecessor();
//(1)如果前驱结点是头节点,就尝试取获取同步状态,这里的tryAcquire方法相当于还是调
//用NofairSync的tryAcquire方法,在上面已经说过
if (p == head && tryAcquire(arg)) {
//如果前驱结点是头节点并且tryAcquire返回true,那么就重新设置头节点为node
setHead(node);
p.next = null; //将原来的头节点的next设置为null,交由GC去回收它
failed = false;
return interrupted; //返回,这是循环的最终目标
}
//(2)如果不是头节点,或者虽然前驱结点是头节点但是尝试获取同步状态失败就会将node结点
//的waitStatus设置为-1(SIGNAL),并且park阻塞自己,等待前驱结点的唤醒。至于唤醒的细节
//下面会说到
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
当线程由阻塞态醒来后,会尝试加锁,如果加锁成功,就把自己设置为头结点
,也就是说头结点就是指当前持有锁的节点。
setHead:
private void setHead(Node node) {
head = node; //把当前节点设置为head节点
node.thread = null; //新的头节点不再持有线程
node.prev = null; //新的头结点不在有pre指针
}
保证了下次头结点被删除时,不存在引用,符合gc:
演示一下按次序被唤醒的下一个线程,当前持有锁的线程就是头节点,也就是thread1:
当thread1释放锁后,会唤醒thread2 (假设是正常的按次序唤醒),那么thread2 醒来后,就会尝试去加锁,并且把自己设置为新的头节点(对应步骤1),并且解除旧的头节点引用关系,保证其可以被垃圾回收(对应步骤2):
本链表没有采用哨兵节点,避免某些文章的误导,判断依据是哨兵节点需要在初始化时,就给哨兵节点赋值一个空节点(new 一个node节点,但是pre和next均为空),本链表并没有这个空节点,多处地方存在head==null的类似判断,哨兵节点参见《关于链表中哨兵结点(sentinel)问题的深入剖析》
shouldParkAfterFailedAcquire方法通过对当前节点的前一个节点的状态进行判断,对当前节点做出不同的操作,注意功能是让当前节点对应的线程进入part阻塞中,等待后续的唤醒。
如果线程需要阻塞的话,返回true;
该方法会将同步队列中node结点的前驱结点的waitStatus为CANCELLED的线程移除,并将当前调用该方法的线程所属结点自己和他的前驱结点的waitStatus设置为-1(SIGNAL),然后返回。具体方法实现如下所示:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//(1)获取前驱结点的waitStatus
int ws = pred.waitStatus;
//(2)如果前驱结点的waitStatus为SINGNAL,就直接返回true
if (ws == Node.SIGNAL)
//前驱结点的状态为SIGNAL,那么该结点就能够安全的调用park方法阻塞自己了。
return true;
if (ws > 0) {
//(3)这里就是将所有的前驱结点状态为CANCELLED的都移除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//CAS操作将这个前驱节点设置成SIGHNAL。
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
为什么检测到0或PROPAGATE后,一定要设置成SIGNAL,然后继续下一次循环(因为返回的false)。
也就是说会尝试2次加锁,第一次加锁失败,设置前驱的SIGNAL为-1,第二次加锁失败就进入阻塞
为什么要设置-1?其实作用正如前文说的,这样,最多加2次锁,就让线程进入阻塞态。第一次失败时就加锁其实也行,只是减少线程阻塞次数,万一再多试一次就成功了呢?当然,也可以多试几次,但是多了就会让资源产生消耗。或者2次是作者的认为比较好的吧。
什么时候node的前驱pred的状态为0?
node刚成为新队尾,值就是0,只有后继节点即将进入阻塞就会设置为-1。
这种情况是最常见的,比如现在AQS的等待队列中有很多node同时正在等待,先有一个node1刚加入队列,等待状态是0(新节点的waitStatus默认值是0),紧跟着node2也加入队列,此时node2的前驱是node1,也就是旧队尾node1的waitStatus肯定还是0,然后node2死循环执行两次,第一次执行shouldParkAfterFailedAcquire自然会检测到前驱状态为0,然后将node2设置为SIGNAL;第二次执行shouldParkAfterFailedAcquire,直接返回true,进入阻塞。
API DOC对-1的解释:假设waitStatus=-1的节点node1,那么它的后继节点node2是(或即将成为阻塞状态,2次循环后才会真正结束)阻塞的,因此当前节点node1在释放锁后必须唤醒后继者节点node2。为了避免竞争
,node2必须在调用获取锁方法时,首先表明他们需要一个信号,然后才能重试原子获取锁,如果失败再次阻塞。
其实就是控制尝试次数的。
该方法让线程去阻塞,真正进入等待状态。park()会让当前线程进入waiting
状态。
在此状态下,有两种途径可以唤醒该线程:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //调用part进入休眠态
return Thread.interrupted();
}
release(int)
方法是独占模式下线程释放共享资源的顶层入口。
它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程
来获取资源。这也正是unlock()的语义,当然不仅仅只限于unlock()。
一般来说,当前持有锁的线程
thread1
(也就是头节点)释放锁后, 不会立即删除自己,而是由被唤醒的下一个节点thread2
来负责删除,假设thread2
醒来后,发现自己的前区节点thread1是头节点,刚好符合FIFO规则按次序唤醒,说明自己是符合获取锁条件的。那么是什么醒来后不符合呢?线程中断,假设thread3是阻塞状态,中途被中断提前醒来了,必须让其重新阻塞,不能持有锁,否则不符合FIFO,详细步骤参见《2.2.3 acquireQueued(Node, int)》截图中的步骤1和步骤
thread获取锁,执行完释放锁的流程是怎样的呢。首先肯定是在finally中调用ReentrantLock.unlock()方法,所以我们就从这个方法开始看起:
public void unlock() {
sync.release(1); //这里ReentrantLock的unlock方法调用了AQS的release方法
}
下面是release()的源码:
/**
* 释放独占模式持有的锁
*/
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()作用是释放锁。
与acquire()方法中的tryAcquire()类似,tryRelease()方法也是需要独占模式的自定义同步器去实现的。
正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可(state-1),也不需要考虑线程安全的问题。
但要注意它的返回值,上面已经提到了,release()是根据tryRelease()的返回值来判断该线程是否已经完成释放掉资源了!所以自义定同步器在实现时,如果已经彻底释放资源(state=0),要返回true,否则返回false。
可重入锁,state是累加的,释放时,当state=0说明累加的锁都释放了。
释放锁的过程:
①获取当前AQS的state,并减去1;
②判断当前线程是否等于AQS的exclusiveOwnerThread,如果不是,就抛IllegalMonitorStateException异常,这就保证了加锁和释放锁必须是同一个线程;
③如果(state-1)的结果不为0,说明锁被重入了,需要多次unlock,这也是lock和unlock成对的原因;
④如果(state-1)等于0,我们就将AQS的ExclusiveOwnerThread设置为null;
⑤如果上述操作成功了,也就是tryRelase方法返回了true;返回false表示需要多次unlock。
protected final boolean tryRelease(int releases) {
//(1)获取当前的state,然后减1,得到要更新的state
int c = getState() - releases;
//(2)判断当前调用的线程是不是持有锁的线程,如果不是抛出IllegalMonitorStateException
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//(3)判断更新后的state是不是0
if (c == 0) {
free = true;
//(3-1)将当前锁持者设为null
setExclusiveOwnerThread(null);
}
//(4)设置当前state=c=getState()-releases
setState(c);
//(5)只有state==0,才会返回true
return free;
unparkSuccessor(Node)方法用于唤醒等待队列中下一个线程。
一般情况下,待唤醒的节点是当前持有锁节点的下一个节点,即node.next。
这里要注意的是,下一个可唤醒的线程并不一定是当前节点的next节点(当前线程是头节点,下一个节点即头结点的下一个节点):
因为node.next(当前节点的下一个节点)waitStatus 可能不符合条件,此时需要从尾部往前找,最新的符合条件的那个节点,如果这个节点存在,调用unpark()方法唤醒。
当前节点的下一个节点,可能被取消了,等价于被删除了;必须是以下状态的一种:0默认值,一般表示最后一个节点;-1表示在阻塞;-2条件阻塞;-3 广播
当前节点的下一个节点不存在
可能当前头节点已经是最后一个节点了,没有后续节点;
可能是这样的,当前持有锁的线程可能没有进入过队列,以非公平方式插队抢到的,没有next指针。因此每次尝试唤醒的是队列中的第一个节点,如果其不符合条件,就在链表上一直迭代下去,直到找到符合条件的为止
。为什么不从前面找,非要从后面找?
private void unparkSuccessor(Node node) {
//(1)获得node的waitStatus
int ws = node.waitStatus;
//(2)判断waitStatus是否小于0
if (ws < 0)
//(2-1)如果waitStatus小于0需要将其以CAS的方式设置为0
compareAndSetWaitStatus(node, ws, 0);
//(2)获得s的后继结点,这里即head的后继结点
Node s = node.next;
//(3)判断后继结点是否已经被移除,或者其waitStatus==CANCELLED
if (s == null || s.waitStatus > 0) {
//(3-1)如果s!=null,但是其waitStatus=CANCELLED需要将其设置为null
s = null;
//(3-2)会从尾部结点开始寻找,找到离head最近的不为null并且node.waitStatus的结点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//(4)node.next!=null或者找到的一个离head最近的结点不为null
if (s != null)
//(4-1)唤醒这个结点中的线程
LockSupport.unpark(s.thread);
}
步骤总结:
遍历下去
,找到第一个waitStatus<=0
的节点,并唤醒。有个细节:
在当前节点释放锁的时候,把头节点的waitStatus设置为0,与前面的加锁失败逻辑设置 当前节点的头驱节点的waitStatus为-1相呼应:
通过前面章节 《2.2 AbstractQueuedSynchronizer类的acquire()方法》,我们得知加锁的逻辑,那么回到本文的主题,非公平锁和公平锁是怎么实现的呢?
答案很简单,就是触发加锁的时机不一样,真正的加锁逻辑的入口是FairSync
和NofairSync
的lock()
方法:
static final class NonfairSync extends Sync {
//入口
final void lock() {
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); //抢锁失败,再次尝试获取锁
}
//重写
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
lock()会先抢占锁,失败后,会调用重写的tryAcquire()方法再次去获取锁,间接调用nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {
//(1)获取当前线程
final Thread current = Thread.currentThread();
//(2)获得当前同步状态state
int c = getState();
//(3)如果state==0,表示没有线程获取
if (c == 0) {
//(3-1)那么就尝试以CAS的方式更新state的值
if (compareAndSetState(0, acquires)) {
//(3-2)如果更新成功,就设置当前独占模式下同步状态的持有者为当前线程
setExclusiveOwnerThread(current);
//(3-3)获得成功之后,返回true
return true;
}
}
//(4)这里是重入锁的逻辑
else if (current == getExclusiveOwnerThread()) {
//(4-1)判断当前占有state的线程就是当前来再次获取state的线程之后,就计算重入后的state
int nextc = c + acquires;
//(4-2)这里是风险处理
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//(4-3)通过setState无条件的设置state的值,(因为这里也只有一个线程操作state的值,即
//已经获取到的线程,所以没有进行CAS操作)
setState(nextc);
return true;
}
//(5)没有获得state,也不是重入,就返回false
return false;
}
总结来说就是:
重入锁
的实现方式。static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
if (!hasQueuedPredecessors() &&
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;
}
公平锁与非公平锁相比,不会抢占,必须先去排队。
参考:《深入理解Java中的AQS》
《Java并发指南8:AQS中的公平锁与非公平锁,Condtion》重要性高
《深入理解Java中的AQS》 重要性高
《AQS同步队列器之一:使用和原理》 java并发编程的艺术
《AQS深入理解 shouldParkAfterFailedAcquire源码分析 状态为0或PROPAGATE的情况分析》 有完整的锁的文章目录