在深入了解ReentrantLock锁之前我们先来了解一些基本概念。
1. 锁的一些基本分类
公平锁:多个线程申请锁时是相对公平的,在申请锁时如果有其它线程已经占用了锁,则进行排队等待处理(排队先进来的先获取到锁,能够避免线程饥饿)。
非公平锁:多个线程申请锁时相对不公平,与公平锁相同,都会存在排队的情况,但对于一个新线程需要获取锁时不是先排队等待,而是先尝试获取锁,不成功时再进入队列排队等待(谁抢到就是谁的,抢不到就排队),ReentrantLock锁默认是非公平锁。
悲观锁:每次更新和读取都加锁,对于读多写少这种场景不利,适用于于写多读少的场景。
乐观锁:更新失败则重试,适用一个标记位来控制,适用于多读的场景,例如CAS机制。
可重入锁:同一线程多次进入无需重新获取锁,例如synchronized和ReentrantLock。
共享锁(读锁):一个锁可以同时被多个线程拥有,ReentrantReadWriteLock的读锁为共享锁,写锁为排他锁
排他锁(写锁):独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁,synchronized和ReentrantLock就是排他锁。
可中断锁:所谓的中断锁指的是锁在执行时可被中断,也就是在执行时可以接收 interrupt 的通知,从而中断锁执行。
2. ReentrantLock锁的基本原理
2.1 ReentrantLock框架
这里以JDK1.9为基准,解释ReentrantLock锁的基本框架和原理,ReentrantLock应用框架如下:
//1.首先创建一个ReentrantLock对象
private Lock lock = new ReentrantLock();
//2.线程加锁
lock.lock();
try {
...
} finally {
//3.线程释放锁
lock.unlock();
}
我们再深入ReentrantLock源码看看ReentrantLock加锁解锁做了什么事情?
先看下ReentrantLock代码框架:
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
/**
* 默认创建非公平锁
*/
public ReentrantLock() {
sync = new NonfairSync();
}/**
* fair为true表示是公平锁,fair为false表示是非公平锁
*/public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// ...省略其它代码
}
ReentrantLock继承了Lock和Serializable两个接口,同时有两个构造方法,分别可初始化创建非公平锁和公平锁。
2.2 ReentrantLock加锁逻辑
以ReentrantLock默认非公平锁为例来看源代码是如何实现加锁的,这里先梳理下ReentrantLock非公平锁框架来说明加锁流程。
//1.加锁调用入口
lock.lock();
⬇️
⬇️
//2.调用ReentrantLock中的lock方法
public void lock() {
sync.lock();
}
⬇️
⬇️
//3.调用ReentrantLock内部抽象类Sync中的lock方法拿锁
//@ReservedStackAccess注释表示该方法对堆栈溢出特别敏感
@ReservedStackAccess
final void lock() {
if (!initialTryLock())
acquire(1);
}
⬇️
⬇️
//4.调用initialTryLock()去拿锁,如果拿到则返回true,并讲锁状态置为1,拿不到则返回false进入下一步
//initialTryLock()有两个实现类,分别对应非公平锁和公平锁,这里以非公平锁为例进行说明
final boolean initialTryLock() {
...
//通过cas(compareAndSetState)的方式拿锁,拿到锁则将锁状态置为1,并将当前线程设置为独占锁访问权限
if (compareAndSetState(0, 1)) { // first attempt is unguarded
setExclusiveOwnerThread(current);
return true;
}
//省略中间代码
...
}
⬇️
⬇️
//5.如果拿不到锁则调用acquire(1)尝试加入队列,acquire是AQS抽象类中的方法
//acquire(1)这里会调用tryAcquire尝试再拿一次锁,如果还是拿不到则调用acquire加入队列
public final void acquire(int arg) {
if (!tryAcquire(arg))
acquire(null, arg, false, false, false, 0L);
}
⬇️
⬇️
//6.调用AQS中的acquire方法执行入队操作
final int acquire(Node node, int arg, boolean shared,
boolean interruptible, boolean timed, long time) {
//省略中间代码
...
}
ReentrantLock非公平锁加锁流程的调用关系上面已经标出,用流程图表示可能更直观。
大家可能注意到lock()方法有个注解,这里提一下@ReservedStackAccess注解,有的小伙伴可能对它比较陌生,它主要的作用是会保护被注解的方法,通过添加一些额外的空间,防止在多线程运行的时候出现栈溢出。
下面重点讲一下ReentrantLock中的compareAndSetState(CAS)拿锁机制和拿锁失败后的AQS(AbstractQueuedSynchronizer)入队机制。
2.3 ReentrantLock的compareAndSetState(CAS)机制
根据compareAndSetState原文代码注释可知,如果当前线程锁的state等于预期值,则原子性(如果有不清楚原子操作的小伙伴可以自行查找)地将锁的state设置为给定的更新值(默认为1)。
参数:
expect–期望值
update–更新值
返回值:
如果CAS成功,则返回true,否则返回False。
/*
Atomically sets synchronization state to the given updated value if the current state value equals the expected value. This operation has memory semantics of a volatile read and write.
Params:
expect – the expected value update – the new value
Returns:
true if successful. False return indicates that the actual value was not equal to the expected value.
*/
protected final boolean compareAndSetState(int expect, int update) {
return U.compareAndSetInt(this, STATE, expect, update);
}
ReentrantLock非公平锁中的CAS调用了来自Unsafe
类中的compareAndSetInt
方法来保证其原子性,其中U是一个 Unsafe类(可以提供硬件级别的原子操作,直接访问内存,可以获取某个属性在内存中的位置,也可以修改对象的字段值)对象,而STATE字段表示锁状态state在AbstractQueuedSynchronizer(AQS)类中的偏移量(可以理解为state的内存地址),通过这个偏移量可以直接访问state。
// Unsafe
private static final Unsafe U = Unsafe.getUnsafe();
private static final long STATE
= U.objectFieldOffset(AbstractQueuedSynchronizer.class, "state");
private static final long HEAD
= U.objectFieldOffset(AbstractQueuedSynchronizer.class, "head");
private static final long TAIL
= U.objectFieldOffset(AbstractQueuedSynchronizer.class, "tail");
我们再来看下compareAndSetInt
干了什么?
/**
* Atomically updates Java variable to {@code x} if it is currently
* holding {@code expected}.
*
* <p>This operation has memory semantics of a {@code volatile} read
* and write. Corresponds to C11 atomic_compare_exchange_strong.
*
* @return {@code true} if successful
*/
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
compareAndSetInt方法主要就是原子性地将该目标对象(0)内存地址(offset)中的值改为目标值(x),这个方法是一个本地(native)的方法,底层是通过C++实现的,其中o为对象,offset为该对象的内存地址。
state本身定义为volatile类型,当把变量声明为volatile类型后,编译器与程序运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序,保证了其修饰变量的可见性和有序性,因此多线程操作时state时保证了新值能立即同步到主内存,以及线程每次使用前都会从从主内存刷新。
/**
* The synchronization state.
*/
private volatile int state;
用一个简单的图来表示ReentrantLock锁state的CAS机制。
总结下state的CAS过程:
1)线程A首先调用getState()方法获取state在主内存中的状态,state默认值为0,并将该值保存在自己的工作内存中;
2)将工作内存中获取到的state值与传入的expect中的值比较;
3)如果相等则根据state的内存地址执行CAS原子操作;
4)将工作内存中的update值交换到主内存中state中,从而将state的值改为1,修改成功返回true,否则返回false。
CAS机制本质上是一种乐观锁机制,与synchronized不同的是CAS在更新共享数据的时候不会对共享数据阻塞式加锁,但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就自旋重试(非阻塞式),直到成功为止。它的优点如下:
1)可以保证变量操作的原子性;
2)并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;
3)在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。
但是CAS机制也有以下缺点:
1) ABA问题。就是一个共享值本来是A,被改成了B,又变回了A,这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。举个例子,以ReentrantLock锁state为例,例如线程A获取到的state的值本身为0,但是在cmpare阶段另一个线程B将state值由0改为1,释放后又改为了0,这个时候线程A是检查不出来state已经被改过的。解决这个问题的一种常用思路就是加入版本号,这样线程在compare阶段就可以感知到是否有其它线程更改了这个共享值。
2)多线程场景可能存在循环时间长开销大的问题。CAS从阻塞机制变成了非阻塞机制,虽然减少了线程之间等待的时间。但如果多线程之间竞争程度大的时候,自旋CAS长时间不成功,会占用大量的CPU资源。一种解决思路是通过JVM支持处理器提供的「pause指令」, 能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,另一种思路是如果线程之间竞争非常大,建议使用锁的方式而非CAS。
3)只能保证一个共享变量的原子操作。Java中的CAS机制只能保证共享变量操作的原子性,而不能保证代码块的原子性。如果想要实现多个变量的原子操作可以使用java.util.
concurrent.atomic 包下的AtomicReference,或者直接使用同步锁机制来保证。
2.4 ReentrantLock的AQS核心acquire入队流程
线程在拿锁失败之后会调用AQS中的acquire方法进行入队操作,入队之前会调用tryAcquire尝试再次拿锁,如果还是失败则调用acquire加入队列。
public final void acquire(int arg) {
//在第一次拿锁失败之后会调用tryAcquire再次拿锁
if (!tryAcquire(arg))
//如果还是失败则调用acquire加入队列
acquire(null, arg, false, false, false, 0L);
}
在理解acquire入队方法前,我们有必要先了解它的参数,参数解析如下
/**
* Main acquire method, invoked by all exported acquire methods.
*
* @param node null unless a reacquiring Condition --node节点
* @param arg the acquire argument --获取锁次数,为1表示已经有线程拿到了锁
* @param shared true if shared mode else exclusive --true为共享模式,false为独占模式
* @param interruptible if abort and return negative on interrupt --是否可中断线程
* @param timed if true use timed waits --线程阻塞时间
* @param time if timed, the System.nanoTime value to timeout --中断超时时间
* @return positive if acquired, 0 if timed out, negative if interrupted --入队则返回positive,中断则返回negative,超时返回0
*/
final int acquire(Node node, int arg, boolean shared,
boolean interruptible, boolean timed, long time) {
... }
AQS会将线程封装成一个CLH队列中的虚拟节点,即Node,我们先看下Node的数据结构,发现它其实就是一个双向链表结构。这种结构的特点是每个数据节点都有两个指针,分别指向该节点的后继节点和前驱节点,该队列是先进先出队列(FIFO队列),其最主要的作用是存储等待的线程,AQS最主要的功能也就是维护这个队列。我们看下Node的源码,其含义我用注释详细进行标注,相信大家很容易看懂~
/** CLH Nodes */
abstract static class Node {
volatile Node prev; //前置指针,指向前驱节点
volatile Node next; //后置指针,指向后置节点
Thread waiter; //队列中的等待线程数量
volatile int status; //节点(线程)的状态,有3个值分别为WAITING、CANCELLED、COND,注意区别于CAS中锁的状态值state
// methods for atomic operations
//casPrev的作用是原子性地将this对象的prev成员变量从c修改为v(可清理prev节点)
//weakCompareAndSetReference相比CAS更加轻量级,但不保证有序性
final boolean casPrev(Node c, Node v) { // for cleanQueue
return U.weakCompareAndSetReference(this, PREV, c, v);
}
//作用类似于casPrev
final boolean casNext(Node c, Node v) { // for cleanQueue
return U.weakCompareAndSetReference(this, NEXT, c, v);
}
//原子操作,用于信号量的实现,当一个线程释放了一个信号量时,它需要将信号量的状态设置为未被占用,这个方法的作用就是将this对象的status成员变量按位与~v 的结果返回
final int getAndUnsetStatus(int v) { // for signalling
return U.getAndBitwiseAndInt(this, STATUS, ~v);
}
//用于在队列之外分配节点时将当前节点的prev指向指定的节点p
final void setPrevRelaxed(Node p) { // for off-queue assignment
U.putReference(this, PREV, p);
}
//用于在队列之外分配节点时修改节点状态,将节点状态设置为s
final void setStatusRelaxed(int s) { // for off-queue assignment
U.putInt(this, STATUS, s);
}
//将当前节点状态清0,上面提到STATUS有3种状态
final void clearStatus() { // for reducing unneeded signals
U.putIntOpaque(this, STATUS, 0);
}
//以下三个静态变量分别代表status、next和prev的偏移量,通过偏移量
//可直接访问其具体的值
private static final long STATUS
= U.objectFieldOffset(Node.class, "status");
private static final long NEXT
= U.objectFieldOffset(Node.class, "next");
private static final long PREV
= U.objectFieldOffset(Node.class, "prev");
}
我们用图直观的表示下内存中CLH队列的数据结构,其中pre,status,next分别通过其偏移量PREV,STATUS,NEXT访问。
这里留一个问题大家思考下,有小伙伴可能注意到代码中使用weakCompareAndSetReference,看它的源码最终也是返回compareAndSetReference来实现原子操作,那这里为啥用它而不直接用CAS?知道的小伙伴欢迎在评论区留言~
@IntrinsicCandidate public final boolean weakCompareAndSetReference(Object o, long offset, Object expected, Object x) { return compareAndSetReference(o, offset, expected, x); }
梳理完CLH队列数据结构后,我们再来看下整个AQS中的数据结构是什么样的?除了前面提到的Node外,还有2个volatile修饰的Node类型节点head和tail,这2个节点跟等待队列的入队出队相关,后面再细讲,我们可以把AQS队列看作CLH队列的扩展。
/**
* Head of the wait queue, lazily initialized.
*/
private transient volatile Node head;
/**
* Tail of the wait queue. After initialization, modified only via casTail.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
我们再来看下AQS的核心acquire方法做了什么?下面的代码我会用注释进行标识
/*
* @param node node节点
* @param arg 加锁次数
* @param shared 是否是共享线程队列,true为共享模式,false为独占模式
* @param interruptible 是否是可中断线程
* @param timed 线程阻塞时间
* @param time 中断超时时间
*/
final int acquire(Node node, int arg, boolean shared,
boolean interruptible, boolean timed, long time) {
Thread current = Thread.currentThread(); //获取当前线程
byte spins = 0, postSpins = 0; //自旋变量及之前的自旋变量,自旋重试直到唤醒线程
boolean interrupted = false, first = false; //中断变量值interrupted,first表示第一次进入acquire方法
Node pred = null; //存储该线程节点的前置节点
/*
* 这里有一段注释解释AQS队列逻辑:
* 首先检查节点是否为first
* if so, ensure head stable, else ensure valid predecessor
* if node is first or not yet enqueued, try acquiring
* else if node not yet created, create it
* else if not yet enqueued, try once to enqueue
* else if woken from park, retry (up to postSpins times)
* else if WAITING status not set, set and retry
* else park and clear WAITING status, and check cancellation
*/
//循环自旋拿锁
for (;;) {
if (!first && (pred = (node == null) ? null : node.prev) != null &&
!(first = (head == pred))) {
if (pred.status < 0) {
cleanQueue(); // predecessor cancelled
continue;
} else if (pred.prev == null) {
Thread.onSpinWait(); // ensure serialization
continue;
}
}
if (first || pred == null) {
boolean acquired;
try {
if (shared)
acquired = (tryAcquireShared(arg) >= 0);
else
acquired = tryAcquire(arg);
} catch (Throwable ex) {
cancelAcquire(node, interrupted, false);
throw ex;
}
if (acquired) {
if (first) {
node.prev = null;
head = node;
pred.next = null;
node.waiter = null;
if (shared)
signalNextIfShared(node);
if (interrupted)
current.interrupt();
}
return 1;
}
}
if (node == null) { // allocate; retry before enqueue
if (shared)
node = new SharedNode();
else
node = new ExclusiveNode();
} else if (pred == null) { // try to enqueue
node.waiter = current;
Node t = tail;
node.setPrevRelaxed(t); // avoid unnecessary fence
if (t == null)
tryInitializeHead();
else if (!casTail(t, node))
node.setPrevRelaxed(null); // back out
else
t.next = node;
} else if (first && spins != 0) {
--spins; // reduce unfairness on rewaits
Thread.onSpinWait();
} else if (node.status == 0) {
node.status = WAITING; // enable signal and recheck
} else {
long nanos;
spins = postSpins = (byte)((postSpins << 1) | 1);
if (!timed)
LockSupport.park(this);
else if ((nanos = time - System.nanoTime()) > 0L)
LockSupport.parkNanos(this, nanos);
else
break;
node.clearStatus();
if ((interrupted |= Thread.interrupted()) && interruptible)
break;
}
}
return cancelAcquire(node, interrupted, interruptible);
}
为什么需要可中断锁?
不可中断锁的问题是,当出现“异常”时,只能一直阻塞等待,别无其他办法,比如下面这个程序。下面的这个程序中有两个线程,其中线程 1 先获取到锁资源执行相应代码,而线程 2 在 0.5s 之后开始尝试获取锁资源,但线程 1 执行时忘记释放锁了,这就造成线程 2 一直阻塞等待的情况,实现代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class InterruptiblyExample {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
// 创建线程 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
System.out.println("线程 1:获取到锁.");
// 线程 1 未释放锁
}
});
t1.start();
// 创建线程 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// 先休眠 0.5s,让线程 1 先执行
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取锁
System.out.println("线程 2:等待获取锁.");
lock.lock();
try {
System.out.println("线程 2:获取锁成功.");
} finally {
lock.unlock();
}
}
});
t2.start();
}
}
以上代码执行的结果如下: