前言
由ReentrantLock揭开AQS的大幕
先来段代码让我们更好的进入到ReentrantLock的世界
class A {
ReentrantLock lock = new ReentrantLock();
public void run(){
try{
lock.lock();
//执行业务
}finally {
lock.unlock();
}
}
}
上面这段代码,当运行到lock.lock()的时候,ReentrantLock是如何保证线程安全的呢?接下来就一起去揭秘ReentrantLock底层是怎么保证线程安全的吧!
概述
要谈ReentrantLock,那就不得不谈下AbstractQueuedSynchronizer(AQS)!我们从ReentrantLock的Sync开始谈起,慢慢深入AQS!
先解释下AQS,它定义了一套多线程访问共享资源的同步器框架。很多同步类的实现都依赖于它,就比如接下来我们说的ReentrantLock。
ReentrantLock底层实现了一套自定义的同步队列器-Sync,我们来看看它的源码
//它是ReentrantLock实现同步控制的根本,它的子类有fair和non-fair两个,一个是公平锁,一个是非公平,各有各的实现方式。这个基类借助AQS的state来展示持有锁的数量
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
//非公平锁尝试获取资源的方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
//尝试释放资源
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//判断当前线程是否是持有资源的排他线程
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
//TODO:回头补一下
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
//获取拥有资源的线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
//持有锁的数量
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
//判断是否当前资源被占有
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
//防止破坏单例
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
里面定义了多个方法,我们重点需要掌握它的lock和tryAcquire方法,接下来我们看看它的两个子类
FairSync
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//加锁,这里的acquire是调用的AQS的方法,它有自己的一套流程
final void lock() {
acquire(1);
}
//公平锁尝试获取资源的方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//看看当前state是否是0,如果是0说明现在没有线程去获取
if (c == 0) {
//它很老实,需要先看下同步队列中是否也有想要获取相同资源的节点,如果有的话,就需要AQS的一套流程,入队列等
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;
}
}
NonFairSync
//非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//非公平锁,上来先去尝试一下获取资源,如果不成功,再走AQS那一套
final void lock() {
//如果抢占成功,就可以直接设置占有了。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则乖乖听话,走我AQS的那一套
acquire(1);
}
//尝试获取的方式,它是直接调用Sync的nonfairTryAcquire方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
其实ReentrantLock加锁机制就是:我有两种实现方式,一是公平锁机制,而是非公平锁机制,对于队列同步器各有一套方案,ReentrantLock只需要定义获取state的方式和释放方式即可,至于那些具体线程的队列的维护(获取失败入队和唤醒队列)都已经再AQS顶层封装好了。接下来我们来看下AQS框架吧!
AQS框架
AQS内部维护了一个Volatile int state的变量和一个FIFO的等待队列(线程获取资源失败后进入阻塞所加入的队列),访问state的方式有三种:
- getState():获取当前state的大小,就是当前持有多少个锁
- setState():设置state的大小
- compareAndSetState():通过CAS操作设置state的大小,它是线程安全的。
接下来就需要了解下AQS里几个比较重要的方法了:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
AQS定义两种资源共享方式:一种是独占资源,一种是共享资源,在当我们去自定义同步队列器的时候,我们并不是将上面的方法全部重写,而是相对应的进行重写:当我们定义的是独占资源的话,仅需要去实现tryAcquire(int)和tryRelease(int)方法即可;当我们定义的时候共享资源的话,仅需要器实现tryAcquireShared(int)和tryReleaseShared(int)方法即可。
这篇文章主要讲的是ReentrantLock,我们就拿ReentrantLock的资源方式来讲解,因为它是独占资源的方式,接下来我们就只需要分析独占资源重写的两种方式即可。
源码分析
在这一段,我们从acquire–>release的次序来!
在这里先把队列的节点的几种状态先来列举一下,方便后来的分析:
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
不知道大家还记不记得在前面我们看源码的东西:acquire(1),这个方法,它是AQS已经写好的方法,接下来我们去看看它内部是怎样的。
//获取排他锁,忽略掉中断的线程
public final void acquire(int arg) {
//这里的几个函数是关键
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
函数流程是这样的:
- tryAcquire:这个就是我们之前看到的ReentrantLock下Sync的子类实现的,公平锁和非公平锁各有一套。获取失败返回false
- addWaiter(Node.EXCLUSIVE):这是获取资源失败以后的操作,让当前阻塞线程入队列,设置独占模式的节点
- acquireQueued():这个函数的作用就是,让当前节点不断的自旋尝试获取资源,直到成功!
- selfInterrupt():能进入这个函数说明,当前这个线程获取到资源了,不过这个线程在获取到资源之前被中断过,正常来讲,它是不被响应的,所以出来之后,再进行自我中断,把中断补上。
之前我们已经看过tryAcquire()了,现在我们直接从addWaiter开始看起
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入队
enq(node);
return node;
}
再来看enq的源码:
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
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;
}
}
}
}
这段代码是非常经典的CAS操作,确保节点能够安全的加入到队尾当中。
再回来看下acquireQueued:从刚开始的获取资源失败,然后将节点加入到队尾,接下来线程该做什么了?那肯定是进入到休息状态,等待被唤醒然后去获取资源,干自己想干的事情了,这个函数非常关键,我们通过源码一行一行的分析:
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
//正如上面注释说的那样,我对这个方法的理解就是将这个入队的节点通过自旋获取资源
final boolean acquireQueued(final Node node, int arg) {
//这个boolean变量用于表示当前节点是否获取资源失败的状态
boolean failed = true;
try {
//标记当前节点对应的线程是否被中断过,默认没有被中断
boolean interrupted = false;
//接下来就是进入到一个死循环了,直到获取资源才返回
for (;;) {
//找到当前节点的前去节点
final Node p = node.predecessor();
//这个前驱节点是head节点吗?如果是的话,当前线程就可以尝试着再去获取一遍锁了
//是头节点并且获取资源成功
if (p == head && tryAcquire(arg)) {
//将获取资源成功,那就将当前这个节点设置为head节点
setHead(node);
//将它原先的前驱结点与它断开联系,方便回收
p.next = null; // help GC
//更改标志位,表示当前节点获取资源成功了
failed = false;
//返回当前节点在被唤醒前是否被中断过
return interrupted;
}
//第一个函数用来判断当前节点是否可以安心去休息了,如果可以安心秀习,调用第二个函数进入线程进入阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
//醒来之后发现当前线程被中断过,那就更改下标志,方便后续进行的自我中断
interrupted = true;
}
} finally {
// 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了),那么取消结点在队列中的等待。
if (failed)
cancelAcquire(node);
}
}
讲完这个函数,我们来看看他里面的另外几个函数:
shouldParkAfterFailedAcquire: 它是用来判断当前节点是否可以安心的去休息了,假如我前面的节点已经放弃了,只是瞎站着,那么我还是有机会去获取锁的。我们来看看源码:
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev.
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前驱节点的状态
int ws = pred.waitStatus;
//如果是SIGNAL的话,我就可以安心的去休息了
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
//如果我前面的节点是取消状态,那就一直往前找,直到找到不是取消状态的节点,然后排在它后面
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
//设置前驱结点的状态为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false是不确定前驱节点的状态是否是SIGNAL,需要再通过一遍循环来判断,最终确认一下才可以去休息
return false;
}
再来看下parkAndCheckInterrupt: 它是在前面函数确保当前节点可以去休息了,那么这个就是真正的去操作节点休息的函数。
/**
* Convenience method to park and then check if interrupted
* @return {@code true} if interrupted
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);//阻塞当前线程
return Thread.interrupted();//返回该线程被唤醒之前是否被中断过
}
小结一下:
- 节点在加入到队尾后,检查状态,找到休息的安全点
- 调用park进入到wait状态,等待unpark()或interrupt()唤醒自己
- 被唤醒后,看看自己是否有资格获取资源,如果自己之前被中断过,就没有资格去获取资源,没有的话,就继续第一步流程
最后总结一下整个acquire的流程:
- 先来调用一下自定义同步器的tryAcquire(),尝试获取一遍资源,如果失败进入流程2;
- 将当前线程封装成一个节点加入到AQS维护的FIFO队列的尾部,并标记为独占模式;
- acquireQueued()是线程在等待队列中休息,如果有机会(轮到自己会被unpark())会去获取资源。获取到资源后才返回,如果在此期间当前线程被中断过就返回true,否则返回false;
最后再来张流程图加深一遍印象:
至此,整个acquire(1)的流程就结束了!
上面是独占锁的抢占过程以及抢占不成功入队列的过程,下面就来看看独占锁的释放资源的过程。
先来看看顶层入口:release方法
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
//尝试着去释放资源,如果成功就往下走,否则直接返回
if (tryRelease(arg)) {
Node h = head;//找到头结点
if (h != null && h.waitStatus != 0)//这是下一个节点需要去唤醒的条件:头结点不为空或者当前节点状态为0
//去唤醒下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
大家可能会想,去唤醒下一个节点为什么是那样的状态呢?我就说下我在分析的时候是怎么理解的吧:首先,当前节点的状态默认是0,这是大家都知道的。然后,这个节点的状态是由下一个节点是否阻塞来设置的,在获取资源的时候我们了解到,当要获取资源的状态获取失败的时候,它会去找一个安全点然后去阻塞,这个安全点就是让它的前一个节点状态设为SIGNAL。回到我们现在想要知道的缘由,所以,我想要去唤醒我的下一个节点的前提就是,我当前节点的状态不能为0和不能为空,为空说明我没有下一个节点了,自然就不用去唤醒了,为0说明我下一个节点现在没有阻塞,自然也不用去唤醒了。
说完上面的,接下来我们再去看下tryRelease方法
//这个方法是AQS交给自定义同步器去实现的
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
//来看看这个ReentrantLock实现的。
protected final boolean tryRelease(int releases) {
//释放资源
int c = getState() - releases;
//如果当前线程不是持有资源的线程,就抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//释放的标记
boolean free = false;
//如果资源为0了,说明当前线程完全释放这个资源了
if (c == 0) {
//改变标记位
free = true;
//将独占线程设为空
setExclusiveOwnerThread(null);
}
//设置资源的状态
setState(c);
//返回是否完全释放资源
return free;
}
再接着Release往下看unparkSuccessor这个方法:
如果资源被完全释放的话,当前节点就可以去唤醒下一个节点的状态了,我们来看看它的源码
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)//如果当前节点的状态是小于0,先去设置状态为0,允许失败
compareAndSetWaitStatus(node, ws, 0);
/*
* 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.
*/
Node s = node.next;//得到这个节点的后继节点
if (s == null || s.waitStatus > 0) {//如果该节点为null或者状态>0就不唤醒
s = null;
//从后往前找,找到最后一个可以被唤醒的节点,唤醒它。
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
//唤醒该节点,这里就跟前面的parkAndCheckInterrupt方法对上了。
LockSupport.unpark(s.thread);
}
至此,我们的ReentrantLock的加锁解锁的机制就完全结束了。接下来回去分析下AQS下的共享锁机制,后期补上笔记
鸣谢
文章灵感来自这篇博客:https://www.cnblogs.com/waterystone/p/4920797.html,文章写的很好。