AQS?ReentrantLock?
我们知道java并发编程的核心在于JUC(java.util.concurrent)包,而在JUC中的大多数同步器都是围绕一个共同的基础行为,例如等待队列、条件队列、独占获取、共享获取等。而这些行为的抽象就是基于AbstractQueuedSynchronizer(AQS)。简单来说AQS就是一个抽象了同步器公共行为的框架类。
ReentrantLock就是基于AQS实现的一种互斥锁,与synchronized类似,但是功能要比synchronized强大,例如ReentrantLock支持公平与非公平锁等等。
代码层面AQS与ReentrantLock之间的关系
上面说了AQS的大致概念,但是AQS在代码中是如何体现的呢?
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
}
可以发现AQS在代码层面就是一个抽象类。因此我在最开始说AQS就是一个抽象了同步器公共行为的类。
而ReentrantLock与AQS之间又有什么关系呢?我们查看以下ReentrantLock的源码
public class ReentrantLock implements Lock, java.io.Serializable {
}
public interface Lock {
}
可以发现ReentrantLock实现了Lock接口,而Lock接口上面却什么都没有。但是我们说ReentrantLock是基于AQS实现的,又是怎么回事呢?接下来,我们看ReentrantLock的一个内部类:
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
}
static final class FairSync extends Sync {
}
static final class NonfairSync extends Sync {
}
可以发现,在ReentrantLock的内部有一个Sync的抽象内部类,这个类就继承了AQS。
接下来我们看一下这几个类的类关系图就会更加清晰的了解到AQS与ReentrantLock之间的关系
源码解析前置知识
1、AQS的父类
在上面的类结构图中我们可以看到AQS这个抽象类上面是还有一个父类的,叫做AbstractOwnableSynchronizer,这个类很简单,他只有一个属性叫做exclusiveOwnerThread,以及这个属性的 set/get 方法,这个属性表示的就是当前锁是被哪一个线程所获取的。
其源码如下:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
/** Use serial ID even though all fields transient. */
private static final long serialVersionUID = 3737899427754241961L;
protected AbstractOwnableSynchronizer() { }
/**
* 表明当前锁是被这个线程所获取
*/
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
2、CLH队列
CLH队列就是一个双向链表,每一个CLH队列的节点就是一个线程。这个节点在AQS中叫做Node,其源码中比较重要的几个属性值如下所示:
static final class Node {
// 表示该节点是共享属性,多个线程可以同时执行,如Semaphore/CountDownLatch
static final Node SHARED = new Node();
// 表示该节点是独占属性,只有一个线程能执行,如ReentrantLock
static final Node EXCLUSIVE = null;
// 由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。需要注意的是,取消节点的线程不会再阻塞。
static final int CANCELLED = 1;
// 表示此节点的后续节点已经(或即将)被阻塞,因此当前节点在释放锁或者被取消后必须唤醒(unpark)后续节点
// 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
static final int SIGNAL = -1;
// 节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
static final int CONDITION = -2;
// 应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。
static final int PROPAGATE = -3;
// 0:以上数值均未按数字排列以简化使用。
// 非负值表示节点不需要发出信号。所以,大多数代码不需要检查特定的值,只需要检查符号。
// 该节点的信号量状态,即上面四种和0(Init,初始化状态)
volatile int waitStatus;
// 指向前一节点的指针
volatile Node prev;
// 指向下一节点的指针
volatile Node next;
// 当前节点表示的线程
volatile Thread thread;
// 表示该节点是共享还是独占,共享就是 SHARED 独占就是 EXCLUSIVE
Node nextWaiter;
/**
* 获取当前节点的前置节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
}
// 指向CLH双向链表队列的头节点
private transient volatile Node head;
// 指向CLH双向链表队列的尾部
private transient volatile Node tail;
使用一张图来表示CLH队列,如下:
注意:
由于CLH队列中存在头节点,因此为了区分这个头节点和CLH中第一个实际存储信息的节点,下文中提到的第一个节点都是指第一个实际存储信息的节点。
3、 LockSupport.park()/unpark()
这两个方法类似于synchronized同步代码块中的wait()和notify(),都是用于阻塞和唤醒线程。但是不同的是park()/unpark()可以在程序的任意位置使用,并且需要注意的是park()对于interrupt 中断是有感应的。
4、 临界资源
我们知道synchronized关键字的使用如下:
Object lock = new Object();
synchronized(lock){
}
因此对于synchronized来说,各个线程所争夺的临界资源就是这个lock对象。但是对于ReentrantLock来说各个线程所争夺的临界资源又是什么呢?在AQS中有一个属性:
/**
* The synchronization state.
*/
private volatile int state;
这个属性就是各个线程在ReentrantLock中所争夺的资源。
5、 CAS
这个东西不知道的同学去百度一下吧,没啥说的,其实就是偷懒,hhhhh。。。。。。。
有了这些前置知识之后,我们开始进入ReentrantLock的源码解析,
希望读者在阅读文章的同时可以打开源码,看文章的同时也阅读一次源码。
首先看一段代码:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock(true); // 公平锁
try {
lock.lock(); // 上锁
/* 业务逻辑 */
} finally {
lock.unlock(); // 解锁
}
}
接下来的公平锁分析就以这段代码为例
ReentrantLock之lock方法(公平锁)
在讲解源码之前,还是要提醒读者,要记住lock() 方法的作用:就是放行拿到锁的线程,阻塞没有拿到锁的线程!!!要时刻记住这个方法的作用,然后再去阅读源码。
进入ReentrantLock的lock方法
public void lock() {
sync.lock();
}
进入sync.lock
可以发现这个是一个抽象类,我们进入公平锁的具体实现中:
final void lock() {
acquire(1);
}
可以发现实际是调用了这个acquire(1),方法,并传入了一个1!!!
进入acquire(1) 方法
这个方法就是AQS实现的方法了,其源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在这个方法中有四个方法调用,我们直接进入这四个方法的源码实现:
1、tryAcquire(arg)
tryAcquire(arg):获取锁,获取到锁返回true,获取不到返回false
这个方法的具体实现是由子类实现的,我们进入他的公平锁实现:
源码如下(代码解释,我直接写在源码中):
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread(); // 获取当前线程
int c = getState(); // 得到临界资源state的状态
if (c == 0) { // 若是state为0,那就表示还没有线程获取锁
// hasQueuedPredecessors(): 如果当前线程前面有排队线程,则为true;如果当前线程位于队列的头部或队列为空,则为false。即判断当前线程是否可以获取锁
// compareAndSetState(0, acquires): cas替换state的值为1,cas成功表示获取锁成功
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 获取锁成功,那么设置获取锁的线程为当前线程
setExclusiveOwnerThread(current);
// 返回true,表示获取锁成功
return true;
}
}
// 如果当前线程就是获取了锁的线程
else if (current == getExclusiveOwnerThread()) {
// 那么state += 1。这就是ReentrantLock可重入特性的体现!!!
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 重新设置state的值
setState(nextc);
// 返回true,表示获取了锁
return true;
}
// 返回false,表示获取锁失败
return false;
}
}
以上就是ReentrantLock获取锁的逻辑。总结如下:
1、state = 0,表示没有线程获取锁。
1.1、若是CLH队列中没有节点 或者 该线程中所表示节点就是CLH的第一个节点。就表示该线程可以尝试获取锁。那么cas替换state标记为1。cas成功就表示获取了锁,返回true
1.2、因为这里是多线程的情况,因此当前线程进行判断的时候,可能已经线程获取到了锁,并且有线程在排队了,因此判断state=0之后,再进一步判断CLH队列的状态。若是有线程在排队了,或者cas失败,那么返回false,表示获取锁失败。2、 state != 0 && 当前线程就是获取了锁的线程,那么直接将state += 1。即表示ReentrantLock是可重入的。
如果获取锁成功,那么整个lock方法也就结束了,该去执行同步代码块中的逻辑代码了。怎么样,是不是很简单!!!!但是如果获取锁不成功,那么就需要进行线程的阻塞了
2、addWaiter(Node.EXCLUSIVE)
addWaiter(Node.EXCLUSIVE):将线程节点node以独占方式加入到CLH这个双向链表的尾部,并返回该node
源码如下(代码解释,我直接写在源码中):
private Node addWaiter(Node mode) {
// 以独占方式(EXCLUSIVE)创建一个节点
Node node = new Node(Thread.currentThread(), mode);
// 获取CLH队列的尾节点
Node pred = tail;
// 情况1:若是第一次执行的时候,head和tail指针都是指向null的,表示CLH双向链表还未初始化
// 因此不会进入这个if中,而是直接进入enq方法,进行CLH的初始化操作,然后自旋插入节点
// 情况2:CLH中已经有Node了,但是当前Node进行CAS入队操作失败
// 此时也会进入enq(node)方法中进行自旋入队
// 若尾节点不为空,表示CLH队列中已经有节点了
if (pred != null) {
// cas操作将该节点插入到CLH队列尾部,然后返回该节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 该方法进行CLH双向链表的初始化与自旋式的插入操作
enq(node);
return node;
}
2.1、enq(node)
该方法进行CLH双向链表的初始化与自旋式的插入操作
private Node enq(final Node node) {
// 死循环
for (;;) {
Node t = tail;
// 如果尾节点为空,那么表示CLH队列还没有进行初始化操作
if (t == null) { // Must initialize
// 直接new 一个新节点,cas将其作为头节点。
// 这里就说明了上文说的CLH队列是一个带头结点的双向链表。
// cas失败表示有其他线程抢先进行了cas,那么直接进入下一次循环(自旋)。
if (compareAndSetHead(new Node()))
tail = head;
} else {
// CLH已经进行了初始化
// cas将该节点插入到CLH的尾部。
// 同样若是cas失败就表示该位置被其他节点占用,那么自旋,直到插入成功。
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
以上就是节点插入到CLH中的逻辑。总结如下:
ReentrantLock会将线程封装为一个Node节点,然后利用cas将其插入到CLH队列的尾部。失败则自旋重试,直到成功。
3、acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
acquireQueued(node, arg):传入刚才的添加到CLH的节点,修改node的前置节点的waitStatus状态,并阻塞该线程。若是外部interrupt中断唤醒线程,那么返回true,否则unpark正常唤醒返回false
源码如下(代码解释,我直接写在源码中):
final boolean acquireQueued(final Node node, int arg) {
// 默认faile为true,即出错
boolean failed = true;
try {
// 默认中断表示为false。即未被中断
boolean interrupted = false;
for (;;) {
// 获取当前节点的前置节点
final Node p = node.predecessor();
// 若是前置节点就是头节点(即当前节点就是CLH中的第一个节点)
// 并且 获取锁成功(这里进行获取锁的操作是因为在解阻塞之后需要进行一次获取锁,让解阻塞的线程获取锁)
if (p == head && tryAcquire(arg)) {
// 拿到锁成功,那么重新设置CLH的头节点,即当前节点需要从CLH中移除
setHead(node);
p.next = null; // help GC
// 没有失败
failed = false;
// 返回中断标记。若是外部有调用中断,那么这里就是返回true
return interrupted;
}
// 若当前节点不是CLH中的第一个节点,或者说是第一个节点但是获取锁失败
// shouldParkAfterFailedAcquire(p, node): 传入当前节点和前置节点,设置前置节点的waitState状态为SIGNAL来表示当前节点,若是出现问题,返回fasle,成功则返回true
// parkAndCheckInterrupt(): 阻塞当前线程。若外部unpark唤醒,那么返回false。若是外部中断唤醒,那么返回true。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 进入这里说明外部有调用中断方法,那么这里设置中断标记为true。
// 随后再进行一次循环,再次阻塞在 parkAndCheckInterrupt() 方法中
interrupted = true;
}
} finally {
if (failed)
// 有失败,执行取消逻辑
cancelAcquire(node);
}
}
3.1、shouldParkAfterFailedAcquire(p, node)
该方法的作用是为了设置pred节点的waitStatus状态为SIGNAL,利用pred节点的waitStatus值表示当前节点是否为可唤醒,若当前node为第一个节点,那么会设置head节点的waitStatus状态为SIGNAL。
因此可知,CLH队列是利用pred节点的状态来标识当前node的信号量状态
第一次执行的得到的pred节点的waitStatus会为0,cas替换该状态为SIGNAL,会返回false
第二次执行该方法得到pred节点的waitStatus才会为SIGNAL,返回true
源码如下(代码解释,我直接写在源码中):
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前置节点的waitStaus状态
int ws = pred.waitStatus;
// 如果前置节点为SIGNAL,表示可唤醒,那么返回true
if (ws == Node.SIGNAL)
return true;
// 如果前一个结点是表示出现异常,需要取消的
if (ws > 0) {
// 从CLH中依次移除那些表示出现异常需要取消的节点
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果当前节点的前一个结点的信号量状态既不是SIGNAL 也不是 >0 的
// 那么就只可能有 -2和-3还有0
// 此时将当前节点的前一个结点的信号量状态 利用cas替换成SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false,表示出现异常
return false;
}
3.2、shouldParkAfterFailedAcquire(p, node)
使用LockSupport.park(this),阻塞当前线程
源码如下(代码解释,我直接写在源码中):
private final boolean parkAndCheckInterrupt() {
// 阻塞当前线程
LockSupport.park(this);
// 阻塞唤醒有两种方式
// 1、外部调用unpark()方法,正常唤醒,那么此方法直接返回fasle,表示不是中断唤醒
// 2、外部调用interrupt()方法,那么就会解阻塞,此方法返回true。随后进入外部的循环,
// 由于调用的是interrupted()这个静态方法,会重置中断标记,因此再次进入这里会再次阻塞。
// 这是那个静态方法。返回中断标记位,并清空中断标记
return Thread.interrupted();
}
以上就是阻塞线程的逻辑。总结如下:
ReentrantLock的线程阻塞是利用LockSupport的park方法。进行线程的阻塞。由于LockSupport的park方法对于中断是有感应的,因此ReentrantLcok也实现了对中断的感应。
到这里就是lock方法实现阻塞的全部逻辑了。
4、selfInterrupt()
acquireQueued(node, arg):使用当前线程调用一次interrupt方法
源码如下(代码解释,我直接写在源码中):
static void selfInterrupt() {
// 使用当前线程调用一次interrupt方法
Thread.currentThread().interrupt();
}
此方法很简单,就是使用当前线程调用一次interrupt方法。要这么做的原因其实很简单。首先需要知道什么情况下会进入这里。当 acquireQueued() 方法返回true的时候,也就是外部调用了中断的时候。因为在acquireQueued中使用了Thread.interrupted() 清除了中断标记,但是外部是调用了中断的,因此这里需要回显中断标记,因此需要调用一次interrupt方法。
以上就是在公平锁情况下的lock方法的源码说明,下面来说说解锁的源码,如果你已经懂了上面加锁的逻辑,那么其实解锁的源码逻辑也就不难了。
ReentrantLock之unlock方法(公平锁)
进入ReentrantLock的unlock方法
public void unlock() {
sync.release(1);
}
进入release方法
release方法不分公平与非公平,因为解锁逻辑都是一样的。
public final boolean release(int arg) {
// 设置state的值,尝试释放锁。
// 返回true:表示可以唤醒下一个处于阻塞的线程
// 返回false:表示可重入的情况,表示不需要唤醒下一个阻塞的线程
if (tryRelease(arg)) {
// 如果需要唤醒下一个处于阻塞的线程
Node h = head;
// 如果CLH队列的头节点不为空,并且waitState不是0
if (h != null && h.waitStatus != 0)
// 对第一个节点进行唤醒操作,让CLH的第一个节点进行锁的获取
// 传入头节点
unparkSuccessor(h);
// 返回释放成功
return true;
}
// state的值设置失败,返回fasle,表示释放失败
return false;
}
1.1、tryRelease(arg)
此方法进行state的值-1操作,表示释放锁资源,并返回是否唤醒下一个处于阻塞的线程
protected final boolean tryRelease(int releases) {
// 当前state值-1
int c = getState() - releases;
// 如果锁的释放不是由锁的持有者释放的,那么抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 初始化free为false
boolean free = false;
// 如果state为0了,那么表示需要唤醒下一个线程了。
if (c == 0) {
// 重置free为true
free = true;
// 将持有锁的线程置为null
setExclusiveOwnerThread(null);
}
// 重新设置state的值
setState(c);
return free;
}
1.2、tryRelease(arg)
此方法进行线程的解阻塞,注意传入的是头节点(因为CLH队列中是使用上一个节点的waitStatus表示下一个节点的状态)。
private void unparkSuccessor(Node node) {
// 获取头节点的状态
int ws = node.waitStatus;
// 如果状态为负数,那么反正不是取消状态,cas将其替换为0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取头结点的下一节点,即CLH中排在第一个的线程节点
Node s = node.next;
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)
// 调用unpark,解阻塞。
LockSupport.unpark(s.thread);
}
到此公平锁的加锁和解锁源码的解析完毕
总结:
1、加锁:
外部调用lock方法,首先判断state状态,然后进行加锁逻辑。加锁成功的话,state+1。加锁失败,那么就插入到CLH队列的尾部,然后阻塞它。
2、解锁:
外部调用unlock方法,首先state-1,若-1后state为0,那么唤醒CLH队列的第一个节点。
上文中了解了公平锁的lock和unlock逻辑之后,现在我们开始分析非公平锁的lock和unlock方法。其实非公平锁的逻辑和公平锁差不多,只是有些许不同。
ReentrantLock lock = new ReentrantLock(false); // 非公平锁
ReentrantLock之lock方法(非公平锁)
final void lock() {
// 非公平锁在进入lock之后,立马执行一次cas修改state状态,抢资源
if (compareAndSetState(0, 1))
// cas成功,那么修改拥有锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 这个方法和公平锁调用的是一样的
}
1、 acquire(1)
这个方法和公平锁调用的是一样的,只是tryAcquire 方法的实现是调用非公平锁,剩下几个方法的实现和公平锁是一样的
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2、tryAcquire(arg)
进入tryAcquire的非公平锁实现。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
2、nonfairTryAcquire(acquires)
其实这个方法的逻辑和公平锁的tryAcquire 方法差不多。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 非公平锁获取锁的条件,不需要进行CLH队列元素的判断,而是直接尝试获取
// 这就是非公平锁和公平锁获取锁的区别
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
这里给大家展示该方法与公平锁的tryAcquire 方法区别
总结:
非公平锁获取锁的条件,不需要进行CLH队列元素的判断,而是直接尝试获取,如果在这里没有获取到锁,那么后面还是需要乖乖的进入CLH队列,等待被唤醒。
因此不公平锁的不公平性在于进入lock方法,那么不管你CLH队列中是否有排队线程,我先进行锁的获取,因此就可能插队成功。但是如果没有插队成功,那么还是需要进入CLH队列排队。
但是我觉得吧,这样的话ReentrantLock的非公平性只是一种半非公平,因为我觉得既然是非公平,那么就应该让所有的线程去抢锁(个人观点,可能这样实现过于复杂吧,小小吐槽)。
ReentrantLock之unlock方法(非公平锁)
非公平锁的释放锁逻辑和公平锁是一样的,调用的是一个方法,这里不再赘述。