JUC之深入理解ReentrantLock
1. 简介
ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于 synchronized
的使用,但是 ReentrantLock 提供了比 synchronized
更强大、灵活的锁机制,可以减少死锁发生的概率。
一个可重入的互斥锁定 Lock,它具有与使用
synchronized
方法和语句所访问的隐式监视器锁定相同的一些基本行为和语义,但功能更强大。ReentrantLock 将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一个线程所拥有时,调用 lock 的线程将成功获取该锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用
isHeldByCurrentThread()
和getHoldCount()
方法来检查此情况是否发生。
ReentrantLock 还提供了公平锁和非公平锁的选择,通过构造方法接受一个可选的 fair
参数(默认非公平锁):当设置为 true 时,表示公平锁;否则为非公平锁。
公平锁与非公平锁的区别在于,公平锁的锁获取是有顺序的。但是公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
ReentrantLock 整体结构如下图:
-
ReentrantLock 实现 Lock 接口,基于内部的 Sync 实现。
-
Sync 实现 AQS ,提供了 FairSync 和 NonFairSync 两种实现。
2. Sync 抽象类
Sync 是 ReentrantLock 的内部静态类,实现 AbstractQueuedSynchronizer 抽象类,同步器抽象类。它使用 AQS 的 state
字段,来表示当前锁的持有数量,从而实现可重入的特性。
/**
* 该锁同步控制的一个基类.下边有两个子类:非公平机制和公平机制.使用了AbstractQueuedSynchronizer类的
*/
static abstract class Sync extends AbstractQueuedSynchronizer
2.1 lock
基于CAS尝试将state(锁数量)从0设置为1
A、如果设置成功,设置当前线程为独占锁的线程;
B、如果设置失败,还会再获取一次锁数量,
B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
B2、如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
-
执行锁。抽象了该方法的原因是,允许子类实现快速获得非公平锁的逻辑。
2.2 nonfairTryAcquire
nonfairTryAcquire(int acquires)
方法,非公平锁的方式获得锁。代码如下:
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//state == 0,表示没有该锁处于空闲状态
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;
}
-
该方法主要逻辑:首先判断同步状态state == 0?
-
如果是,表示该锁还没有被线程持有,直接通过CAS获取同步状态。
-
如果成功,返回 true 。
-
否则,返回 false 。
-
-
如果不是,则判断当前线程是否为获取锁的线程?
-
如果是,则获取锁,成功返回 true 。成功获取锁的线程,再次获取锁,这是增加了同步状态
state
。通过这里的实现,我们可以看到上面提到的 “它使用 AQS 的state
字段,来表示当前锁的持有数量,从而实现可重入的特性”。 -
否则,返回 false 。
-
-
-
理论来说,这个方法应该在子类 FairSync 中实现,但是为什么会在这里呢?在下文的
ReentrantLock.tryLock()
中,详细解析。
2.3 tryRelease
tryRelease(int releases)
实现方法,释放锁。代码如下:
protected final boolean tryRelease(int releases) {
// 减掉releases
int c = getState() - releases;
// 如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
-
通过判断判断是否为获得到锁的线程,保证该方法线程安全。
-
只有当同步状态彻底释放后,该方法才会返回 true 。当
state == 0
时,则将锁持有线程设置为 null ,free= true
,表示释放成功。
2.4 其他实现方法
// 是否当前线程独占
@Override
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();
}
// 新生成条件
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
}
-
从这些方法中,我们可以看到,ReentrantLock 是独占获取同步状态的模式。
3. Sync 实现类
3.1 NonfairSync
获取锁的步骤
基于CAS尝试将state(锁数量)从0设置为1
A、如果设置成功,设置当前线程为独占锁的线程;
B、如果设置失败,还会再获取一次锁数量,
B1、如果锁数量为0,再基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
B2、如果锁数量不为0或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
NonfairSync 是 ReentrantLock 的内部静态类,实现 Sync 抽象类,非公平锁实现类。
3.1.1 lock
lock()
实现方法,首先基于 AQS state
进行 CAS操作,将 0 设置为1
。若成功,则获取锁成功。若失败,执行 AQS 的正常的同步状态获取逻辑。代码如下:
/**
* 1)首先基于CAS将state(锁数量)从0设置为1,如果设置成功,设置当前线程为独占锁的线程;-->请求成功-->第一次插队
* 2)如果设置失败(即当前的锁数量可能已经为1了,即在尝试的过程中,已经被其他线程先一步占有了锁),这个时候当前线程执行acquire(1)方法
//获取锁的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();//中断自己
}
* 2.1)acquire(1)方法首先调用下边的tryAcquire(1)方法,在该方法中,首先获取锁数量状态,
* 2.1.1)如果为0(证明该独占锁已被释放,当下没有线程在使用),这个时候我们继续使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁;-->请求成功-->第二次插队;当然,如果设置不成功,直接返回false
* 2.2.2)如果不为0,就去判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
*
* 下边的流程一句话:请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
*
* 2.2.3)如果最后在tryAcquire(1)方法中上述的执行都没成功,即请求没有成功,则返回false,继续执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法
* 2.2)在上述方法中,首先会使用addWaiter(Node.EXCLUSIVE)将当前线程封装进Node节点node,然后将该节点加入等待队列(先快速入队,如果快速入队不成功,其使用正常入队方法无限循环(即自旋)一直到Node节点入队为止)
* 2.2.1)快速入队:如果同步等待队列存在尾节点,将使用CAS(compareAndSetTail(pred, node))尝试将尾节点设置为node,并将之前的尾节点插入到node之前(即Node pred = tail;node.prev = pred;)
* 2.2.2)正常入队:如果同步等待队列不存在尾节点或者上述CAS尝试不成功的话,就执行正常入队(enq(node);该方法是一个无限循环的过程,即直到入队为止)-->第一次阻塞
* 2.2.2.1)如果尾节点为空(初始化同步等待队列),创建一个dummy节点,并将该节点通过CAS尝试设置到头节点上去,设置成功的话,将尾节点也指向该dummy节点(即头节点和尾节点都指向该dummy节点)
* 2.2.2.1)如果尾节点不为空,执行与快速入队相同的逻辑,即使用CAS尝试将尾节点设置为node,并将之前的尾节点插入到node之前
* 最后,如果顺利入队的话,就返回入队的节点node,如果不顺利的话,无限循环去执行2.2)下边的流程,直到入队为止
* 2.3)node节点入队之后,就去执行acquireQueued(final Node node, int arg)(这又是一个无限循环的过程,这里需要注意的是,无限循环等于阻塞,多个线程可以同时无限循环--每个线程都可以执行自己的循环,这样才能使在后边排队的节点不断前进)
* 2.3.1)获取node的前驱节点p,如果p是头节点,就继续使用tryAcquire(1)方法去尝试请求成功,-->第三次插队(当然,这次插队不一定不会使其获得执行权,请看下边一条),
* 2.3.1.1)如果第一次请求就成功,不用中断自己的线程,如果是之后的循环中将线程挂起之后又请求成功了,使用selfInterrupt()中断自己
* (注意p==head&&tryAcquire(1)成功是唯一跳出循环的方法,在这之前会一直阻塞在这里,直到其他线程在执行的过程中,不断的将p的前边的节点减少,直到p成为了head且node请求成功了--即node被唤醒了,才退出循环)
* 2.3.1.2)如果p不是头节点,或者tryAcquire(1)请求不成功,就去执行shouldParkAfterFailedAcquire(Node pred, Node node)来检测当前节点是不是可以安全的被挂起,
* 2.3.1.2.1)如果node的前驱节点pred的等待状态是SIGNAL(即可以唤醒下一个节点的线程的状态),则node节点的线程可以安全挂起,返回true,执行2.3.1.3)
* 2.3.1.2.2)如果node的前驱节点pred的等待状态是CANCELLED,则pred的线程被取消了,我们会将pred之前的连续几个被取消的前驱节点从队列中剔除,返回false(即不能挂起),如下:
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
之后继续执行2.3)中上述的代码的cancelAcquire(node);
* 2.3.1.2.3)如果node的前驱节点pred的等待状态是除了上述两种的其他状态,则使用CAS尝试将前驱节点的等待状态设为SIGNAL,并返回false(因为CAS可能会失败,这里不管失败与否,都返回false,下一次执行该方法的之后,pred的等待状态就是SIGNAL了),之后继续执行2.3)中上述的代码的cancelAcquire(node);
* 2.3.1.3)如果可以安全挂起,就执行parkAndCheckInterrupt()挂起当前线程,之后,继续执行2.3)中之前的代码
* 最后,直到该节点的前驱节点p之前的所有节点都执行完毕为止,我们的p成为了头节点,并且tryAcquire(1)请求成功,跳出循环,去执行。
* (在p变为头节点之前的整个过程中,我们发现这个过程是不会被中断的)
* 2.3.2)当然在2.3.1)中产生了异常,我们就会执行cancelAcquire(Node node)取消node的获取锁的意图。
*/
final void lock() {
if (compareAndSetState(0, 1))//如果CAS尝试成功
setExclusiveOwnerThread(Thread.currentThread());//设置当前线程为独占锁的线程
else
acquire(1);
}
-
优先基于 AQS
state
进行 CAS 操作,已经能体现出非公平锁的特点。因为,此时有可能有 N + 1 个线程正在获得锁,其中 1 个线程已经获得到锁,释放的瞬间,恰好被新的线程抢夺到,而不是排队的 N 个线程。
3.1.2 tryAcquire
tryAcquire(int acquires)
实现方法,非公平的方式,获得同步状态。代码如下:
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
-
直接调用
nonfairTryAcquire(int acquires)
方法,非公平锁的方式获得锁。
/**
* 非公平锁中被tryAcquire调用
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取锁数量
if (c == 0) {//如果锁数量为0,证明该独占锁已被释放,当下没有线程在使用
if (compareAndSetState(0, acquires)) {//继续通过CAS将state由0变为1,注意这里传入的acquires为1
setExclusiveOwnerThread(current);//将当前线程设置为独占锁的线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//查看当前线程是不是就是独占锁的线程
int nextc = c + acquires;//如果是,锁状态的数量为当前的锁数量+1
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//设置当前的锁数量
return true;
}
return false;
}
3.2 FairSync
FairSync 是 ReentrantLock 的内部静态类,实现 Sync 抽象类,公平锁实现类。
lock的步骤:获取一次锁数量,
B1、如果锁数量为0,如果当前线程是等待队列中的头节点,基于CAS尝试将state(锁数量)从0设置为1一次,如果设置成功,设置当前线程为独占锁的线程;
B2、如果锁数量不为0或者当前线程不是等待队列中的头节点或者上边的尝试又失败了,查看当前线程是不是已经是独占锁的线程了,如果是,则将当前的锁数量+1;如果不是,则将该线程封装在一个Node内,并加入到等待队列中去。等待被其前一个线程节点唤醒。
3.2.1 lock
lock()
实现方法,代码如下:
final void lock() {
acquire(1);
}
-
直接执行 AQS 的正常的同步状态获取逻辑。
3.2.2 tryAcquire
tryAcquire(int acquires)
实现方法,公平的方式,获得同步状态。代码如下:
/**
* 获取公平锁的方法
* 1)获取锁数量c
* 1.1)如果c==0,如果当前线程是等待队列中的头节点,使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁-->请求成功
* 1.2)如果c!=0,判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)-->请求成功
* 最后,请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // <1>
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;
}
比较非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于,公平锁在获取同步状态时多了一个限制条件 <1>
处的 hasQueuedPredecessors()
方法,是否有前序节点,如果返回true,即表示自己不是首个等待获取同步状态的节点。代码如下:
// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;
//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
-
该方法主要做一件事情:主要是判断当前线程是否位于 CLH 同步队列中的第一个。如果是则返回 true ,否则返回 false 。
总结:公平锁与非公平锁对比
-
FairSync:lock()少了插队部分(即少了CAS尝试将state从0设为1,进而获得锁的过程)
-
FairSync:tryAcquire(int acquires)多了需要判断当前线程是否在等待队列首部的逻辑(实际上就是少了再次插队的过程,但是CAS获取还是有的)。
最后说一句,
-
ReentrantLock是基于AbstractQueuedSynchronizer实现的,AbstractQueuedSynchronizer可以实现独占锁也可以实现共享锁,ReentrantLock只是使用了其中的独占锁模式
4. Lock 接口
java.util.concurrent.locks.Lock
接口,定义方法如下:
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
5. ReentrantLock
java.util.concurrent.locks.ReentrantLock
,实现 Lock 接口,重入锁。
ReentrantLock 的实现方法,基本是对 Sync 的调用。
5.1 构造方法
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
-
基于
fair
参数,创建 FairSync 还是 NonfairSync 对象。
5.2 lock
/**
*获取一个锁
*三种情况:
*1、如果当下这个锁没有被任何线程(包括当前线程)持有,则立即获取锁,锁数量==1,之后再执行相应的业务逻辑
*2、如果当前线程正在持有这个锁,那么锁数量+1,之后再执行相应的业务逻辑
*3、如果当下锁被另一个线程所持有,则当前线程处于休眠状态,直到获得锁之后,当前线程被唤醒,锁数量==1,再执行相应的业务逻辑
*/
public void lock() {
sync.lock(); //调用NonfairSync(非公平锁)或FairSync(公平锁)的lock()方法
}
//获取公平锁
/**
*获取一个锁
*三种情况:
*1、如果当下这个锁没有被任何线程(包括当前线程)持有,则立即获取锁,锁数量==1,之后被唤醒再执行相应的业务逻辑
*2、如果当前线程正在持有这个锁,那么锁数量+1,之后被唤醒再执行相应的业务逻辑
*3、如果当下锁被另一个线程所持有,则当前线程处于休眠状态,直到获得锁之后,当前线程被唤醒,锁数量==1,再执行相应的业务逻辑
*/
5.3 lockInterruptibly
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
6.synchronized 和 ReentrantLock 异同?
-
相同点
-
都实现了多线程同步和内存可见性语义。
-
都是可重入锁。
-
-
不同点
-
同步实现机制不同
-
synchronized
通过 Java 对象头锁标记和 Monitor 对象实现同步。 -
ReentrantLock 通过CAS、AQS(AbstractQueuedSynchronizer)和 LockSupport(用于阻塞和解除阻塞)实现同步。
-
-
可见性实现机制不同
-
synchronized
依赖 JVM 内存模型保证包含共享变量的多线程内存可见性。 -
ReentrantLock 通过 ASQ 的
volatile state
保证包含共享变量的多线程内存可见性。
-
-
使用方式不同
-
synchronized
可以修饰实例方法(锁住实例对象)、静态方法(锁住类对象)、代码块(显示指定锁对象)。 -
ReentrantLock 显示调用 tryLock 和 lock 方法,需要在
finally
块中释放锁。
-
-
功能丰富程度不同
-
synchronized
不可设置等待时间、不可被中断(interrupted)。 -
ReentrantLock 提供有限时间等候锁(设置过期时间)、可中断锁(lockInterruptibly)、condition(提供 await、condition(提供 await、signal 等方法)等丰富功能
-
-
锁类型不同
-
synchronized
只支持非公平锁。 -
ReentrantLock 提供公平锁和非公平锁实现。当然,在大部分情况下,非公平锁是高效的选择。
-
ReentrantLock 支持中断处理,且性能较
synchronized
会好些。
-
-
在
synchronized
优化以前,它的性能是比 ReenTrantLock 差很多的,但是自从synchronized
引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized
。并且,实际代码实战中,可能的优化场景是,通过读写分离,进一步性能的提升,所以使用 ReentrantReadWriteLock 。