ReentrantLock
概述
ReentrantLock
来自于jdk 1.5
,位于JUC
包的locks
子包,独占(互斥)式可重入锁。Synchronized
的功能他都有,并且具有更加强大的功能- 实现了
Lock
接口,具有通用的操作锁的方法。内部是使用AQS
队列同步器来辅助实现的,重写了AQS
的获取独占式锁的方法,并实现了可重入性 ReentrantLock
还具有公平与非公平两个获取锁模式,这个并非AQS
提供的,而是ReentrantLock
基于AQS
自己实现的
ReentrantLock
源码阅读可需参考 AQS
框架的详解:https://blog.csdn.net/weixin_38192427/article/details/117028828
可重入性
ReentrantLock
是一个可重入的互斥锁。顾名思义,互斥锁表示锁在某一时间点只能被同一线程所拥有。可重入表示锁可被某一线程多次获取。当然 synchronized
也是可重入的互斥锁
synchronized
关键字隐式的支持重进入,比如一个synchronized
修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁ReentrantLock
虽然没能像synchronized
关键字一样支持隐式的重进入,但是在调用lock()
方法时,已经获取到锁的线程,能够再次调用lock()
方法获取锁而不被阻塞。主要的实现是在实现tryAcquire(int acquires)
方法时考虑占有锁的线程再次获取锁的场景
在 ReentrantLock
中, AQS
的 state
同步状态值表示线程获取该锁的可重入次数
- 在默认情况下,
state
的值为0
表示当前锁没有被任何线程持有 - 当一个线程第一次获取该锁时会尝试使用
CAS
设置state
的值为1
,如果CAS
成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程 - 在该线程没有释放锁的情况下,第二次获取该锁后,状态值被设置为
2
, 这就是可重入次数 - 在该线程释放该锁时,会尝试使用
CAS
让状态值减1
,如果减1
后状态值为0
,则当前线程成功释放该锁
公平与非公平模式
如果在绝对时间上,等待时间最长的线程最优先获取锁。那么这个锁是公平的,反之,是不公平的
- 公平模式是严格的以
FIFO
先进先出的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统资源的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK
将非公平锁作为默认的实现 - 与默认情况非公平锁相比,使用公平锁的程序在多线程环境下效率比较低。而且即使是公平锁也不一定能保证线程调度的公平性,后来的线程调用
tryLock()
方法同样可以不经过排队而获得该锁
ReentrantLock
源码
源码概览
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
// 静态内部类 Sync 继承了 AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
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() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0);
}
}
// 静态内部类 NonfairSync 继承了 Sync,表示非公平模式
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// 静态内部类 FairSync 继承了 Sync,表示公平模式
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
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;
}
}
}
ReentrantLock
构造器
非公平模式(默认)
private final Sync sync;
// 创建一个不公平的 ReentrantLock 的实例,这是默认的模式
public ReentrantLock() {
sync = new NonfairSync();
}
公平模式
// 创建一个具有给定公平策略的 ReentrantLock,false表示不公平,true表示公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
非公平模式加锁
lock()
不可中断获取锁
JUC
中只要是实现 Lock
的锁,那么加锁的方法,一般都统一调用开放给外部的 lock()
方法
public void lock() {
sync.lock();
}
lock()
方法的具体实现是在 sync
的子类 NonfairSync
中
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 首先假设当前锁没有被重入过,那么尝试获取锁
// CAS 尝试将 state 的值从 0 更新为 1
if (compareAndSetState(0, 1))
// 如果 CAS 成功,说明获取到了锁,那么记录获取到的锁的线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 如果 CAS 失败,说明当前锁被重入过,或者锁被其他线程获取了,
// 那么执行 AQS 的 acquire() 方法继续尝试获取
acquire(1);
}
}
- 首先尝试获取锁,使用
CAS
将state
的值从0
更新为1
,如果CAS
成功,那么说明该锁没有被任何线程获取,此时获取锁成功,将当前线程标记为持有锁的线程,加锁结束 - 若获取锁失败,表示该锁此前已经被某条线程获取到了,或者被别的线程抢先
CAS
成功,那么执行AQS
的acquire()
方法继续尝试获取锁
关于 AQS
中的获取独占式锁 acquire()
方法详情:https://blog.csdn.net/weixin_38192427/article/details/117028828
tryAcquire(arg)
方法的重写(重点)
在 AQS
中的获取独占式锁 acquire()
方法中,有一个 tryAcquire(arg)
方法,它的具体实现是由 AQS
的子类去完成的。在非公平模式下,它的实现在 NonfairSync
类中
protected final boolean tryAcquire(int acquires) {
// 调用类 Sync 中的 nonfairTryAcquire()
return nonfairTryAcquire(acquires);
}
/**
* NonfairSync 中的方法,非公平的获取锁
*
* @param acquires 参数,ReentrantLock 中传递 1
* @return true 成功 false 失败
*/
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state同步状态值
int c = getState();
// 如果为 0,表示还没有线程获取该锁
if (c == 0) {
// 使用 CAS 尝试获取锁
if (compareAndSetState(0, acquires)) {
// 获取成功就设置线程变量
setExclusiveOwnerThread(current);
// 返回true,方法结束
return true;
}
}
// 如果不为 0,那么表示有线程获取到了锁,此时判断是否就是当前线程获取到了锁
// 如果是,那么表示可重入,执行重入的逻辑
else if (current == getExclusiveOwnerThread()) {
// 新的 state 应该是此前获取的 state + 1
int nextc = c + acquires;
/**
* 如果小于 0,那么表示重入次数过多(超过了 Integer.MAX_VALUE ),直接抛出异常
* 新的 state 小于 0 的情况,只有 c 为 Integer.MAX_VALUE 时才会发生
* 由于计算机二进制的计算原理,此时加上 1 反而会变成 int 类型的最小值,从而小于 0
*/
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 到这一步说明重入次数正常,那么设置新状态值,此时重入次数 +1
// 这里不需要 CAS,因为在 else if 条件中,当前线程就是已经获取到锁的线程了
setState(nextc);
//返回true,方法结束
return true;
}
// 到这一步,说明尝试获取锁没有成功,或者重入锁的线程不是当前线程,那么返回 false
// 之后执行 AQS 的 acquire() 后续方法,会被加入到同步队列,执行后面的逻辑
return false;
}
- 这该实现中,使用
state
来统计锁被某个线程重入的次数,每重入一次state + 1
,同时注意到内部还会判断加1
之后的state
值是否小于0
,这说明ReentrantLock
并不是无限重入的,最大重入次数就是Integer.MAX_VALUE
,这个值再加上1
就会变成负数,进而抛出异常 nonfairTryAcquire(arg)
方法如果返回true
,则说明当前线程成功获取了锁(第一次获取或者重入获取)
重写的 tryAcquire(arg)
方法实现非公平模式理解
非公平的意思是说:先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁
- 这里假设线程
A
调用lock()
方法时执行到nonfairTryAcquire()
,然后发现当前状态值不为0
,又发现当前线程不是线程持有者,则返回false
,然后当前线程被放入AQS
同步队列 - 这时候某个线程
C
释放了锁,state
变成0
,此时线程B
也调用了lock()
方法执行到nonfairTryAcquire()
,发现当前状态值为0
了,所以通过CAS
设置获取到了该锁,甚至在外面的lock()
方法中就有可能抢先获取到了锁 - 明明是线程
A
先请求获取该锁,但是却由后请求的B
线程获取了锁,这就是非公平的体现。这里线程B
在获取锁前并没有查看当前AQS
队列里面是否有比自己更早请求该锁的线程,而是使用了抢夺策略,从这里也能想像出公平模式的实现
公平模式加锁
公平性与否是针对获取锁顺序而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是 FIFO
先进先出
// 创建一个具有给定公平策略的 ReentrantLock,false表示不公平,true表示公平
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// FairSync 类中的 lock()
final void lock() {
// 调用 AQS 中的 acquire()
acquire(1);
}
关于 AQS
中的获取独占式锁 acquire()
方法详情:https://blog.csdn.net/weixin_38192427/article/details/117028828
tryAcquire(arg)
方法的重写(重点)
公平模式 FairSync
对于获取锁的 tryAcquire()
方法的实现
/**
* FairSync 中的方法,公平的获取锁
*
* @param acquires 参数,ReentrantLock 中传递 1
* @return true 成功 false 失败
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 方法就是公平性的保证
if (!hasQueuedPredecessors() &&
// CAS 尝试将 state 的值从 0 更新为 acquires
compareAndSetState(0, acquires)) {
// 如果 CAS 成功,说明获取到了锁,那么记录获取到的锁的线程
setExclusiveOwnerThread(current);
return true;
}
// 如果不为 0,那么表示有线程获取到了锁,此时判断是否就是当前线程获取到了锁
// 如果是,那么表示可重入,执行重入的逻辑
} else if (current == getExclusiveOwnerThread()) {
// 新的 state 应该是此前获取的 state + 1
int nextc = c + acquires;
/**
* 如果小于 0,那么表示重入次数过多(超过了 Integer.MAX_VALUE ),直接抛出异常
* 新的 state 小于 0 的情况,只有 c 为 Integer.MAX_VALUE 时才会发生
* 由于计算机二进制的计算原理,此时加上 1 反而会变成 int 类型的最小值,从而小于 0
*/
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 到这一步说明重入次数正常,那么设置新状态值,此时重入次数 +1
// 这里不需要 CAS,因为在 else if 条件中,当前线程就是已经获取到锁的线程了
setState(nextc);
return true;
}
return false;
}
/**
* 是 AQS 中的方法,查询是否有任何线程等待获取锁的时间超过当前线程
* * @return 如果有前驱 返回true 否则 返回false
*/
public final boolean hasQueuedPredecessors() {
// 同步队列尾节点
Node t = tail;
// 同步队列头节点
Node h = head;
Node s;
// 如果头结点等于尾节点,则返回false,表示没有线程等待
// 否则,如果头结点的后继s不为null并且s的线程和当前线程相等,则返回false,表示表示当前线程就是等待时间最长的线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
该方法与 NonfairSync
类非公平模式的 nonfairTryAcquire(int acquires)
方法比较:唯一不同的位置为判断条件多了 hasQueuedPredecessors()
方法,是否有任何线程等待获取锁的时间超过当前线程的判断
hasQueuedPredecessors()
方法
- 由于同步队列中头节点是当前获取锁的线程,而新加入的节点是加入到尾部,那么队列中的第二个节点代表的线程就是请求优先级最高的,即等待时间最长的线程
- 如果头节点等于尾节点,表示此时同步队列中没有线程等待;否则,如果头节点的后继
s
不为null
并且s
的线程和当前线程相等,表示当前线程就是等待时间最长的线程。这两种情况都返回false
,表示没有线程比当前线程更早地请求获取锁,那么当前线程可以去获得锁 - 如果该方法返回
true
,则表示有线程比当前线程更早地请求获取锁。那么当前线程将不会执行CAS
操作去获取锁,而是返回false
,保证了线程获取锁的顺序与加入同步队列的顺序一致,很好的保证了公平性,但同时也明显增加了获取锁的成本。为了性能,ReentrantLock
的实现就是默认非公平的
lockInterruptibly()
可中断获取非公平或公平锁
就是当前线程在调用该方法,因为没有获取到锁而被挂起时,如果其他线程调用了当前线程的 interrupt()
方法,则当前线程会被唤醒并抛出 InterruptedException
异常,然后返回
这里的中断与不可中断模式,是 AQS
已经为我们实现好了的,我们只需要调用相应的方法就行了,不需要自己实现
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 如果当前线程被中断,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 尝试获取锁
if (!tryAcquire(arg))
// 如果没获取到,那么调用 AQS 可被中断的方法
doAcquireInterruptibly(arg);
}
tryLock()
尝试获取非公平或公平锁
public boolean tryLock() {
// 调用类 Sync 中的 nonfairTryAcquire(),上面一分析过了
return sync.nonfairTryAcquire(1);
}
- 尝试获取锁,如果当前线程获取该锁(第一次获取或者重入获取)那么返回
true
,否则返回false
。该方法不会引起当前线程阻塞
unlock()
释放锁
JUC
中只要是实现 Lock
的锁,那么解锁的方法,一般都统一调用开放给外部的 unlock()
方法
public void unlock() {
// 调用 AQS 的 release 方法
sync.release(1);
}
// AQS 的 release 方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
// 获取头结点
Node h = head;
// 果头结点不为 null 并且状态不等于 0
if (h != null && h.waitStatus != 0)
// 那么唤醒头结点的一个出于等待锁状态的后继结点
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease(int arg)
方法的重写(重点)
tryRelease()
方法是 AQS
中定义的方法,在 AQS
中的实现是抛出异常,需要子类自己重写
/**
* Sync 的 tryRelease 实现,尝试释放锁
* * @param releases 参数,ReentrantLock 中传递 1
* @return true 成功 false 失败
*/
protected final boolean tryRelease(int releases) {
// 获取释放锁之后的 state 值 c,ReentrantLock 中是减去 1
int c = getState() - releases;
// 如果当前线程不是获取锁的线程,那么抛出 IllegalMonitorStateException 异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// free 用于记录释放成功与否的标志位,默认为 false
boolean free = false;
// 如果 c 为 0,那表示完全释放了锁,此时清除记录线程 free 为 true
// 否则,说明该锁被重入了,那么 lock 了几次就需要 unlock 几次,因为每一次只是 state 减去 1,并且此次并没有完全释放
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
// 设置新的 state
setState(c);
// 返回 free
return free;
}
ReentrantLock
中tryRelease()
的实现,其实就是将线程持有锁的次数state - 1
,若减少后state
值为0
,那么表示线程完全释放锁了,设置获取锁的线程为null
,更新state
值,返回true
- 如果返回
false
,说明此次释放锁并没有完全释放。由于执行该方法的线程必然持有锁,故该方法对state
的更新不需要任何CAS
操作
ReentrantLock
总结
ReentrantLock
是基于 AQS
实现的独占式可重入锁,同时还有公平模式和非公平模式
非公平模式
从源码中可以看出来,非公平锁的非公平之处在于:在头节点释放锁唤醒后继节点线程的时候,如果有新来的线程在尝试获取锁,那么新来的线程在和后继节点中的线程的竞争中可能获取锁成功,有两个地方都有支持这样的可能性
- 在
NonfairSync
类实现的lock()
方法中,使用CAS
方式获取到锁(只有在当前锁未被任何线程占有时才能成功,通过state
是否等于0
来判断) - 在
NonfairSync
类实现的tryAcquire(int acquires)
方法中,使用CAS
方式获取到锁(只有在当前锁未被任何线程占有时才能成功,通过state
是否等于0
来判断)
如果被新来的线程获取到了锁,那么此时获取到锁的线程,不需要加入队列。而队列中被唤醒的后继节点,由于获取不到锁只能继续等待获得锁的节点线程释放了锁之后继续尝试
只要是进入同步队列排了队的节点线程,就只能按照队列顺序去获取锁,而没有排队的线程,则可能直接插队获取锁。如果在上面的两处获取锁都失败后,线程会被构造节点并加入同步队列等待,再也没有先一步获取锁的可能性
可以看出来,非公平模式下,同步队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待唤醒,并且还要检查前驱节点是否为头节点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁,这也就是非公平模式,在高并发情况下会出现的饥饿问题
但是非公平模式会有良好的性能,直接获取锁线程不必加入等待队列就可以获得锁,免去了构造节点并加入同步队列的繁琐操作,因为加入到队列尾部时也是需要 CAS
竞争的,可能会造成 CPU
的浪费,并且还可能会因为后续的线程等待、唤醒,造成线程上下文切换,非常耗时
公平模式
即使是公平锁也不一定能保证线程调度的公平性,公平模式下,后来的线程调用 tryLock()
方法同样可以不经过排队而获得该锁。因为 tryLock()
方法内部是直接调用的 nonfairTryAcquire()
方法,这个方法我们说了它不具备公平性
参考:https://blog.csdn.net/weixin_43767015/article/details/107131636