ReentrantLock简介
ReentrantLock表示重入锁,它是唯一一个实现了 Lock 接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数。如果当前线程 t1 通过调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞去获取锁的,直接增加重试次数 就行了。synchronized 和 ReentrantLock 都是可重入锁。
重入锁的目的
比如调用 demo 方法获得了当前的对象锁,然后在这个方法中再去调用 demo2,demo2 中的存在同一个实例锁,这个时候当前线程会因为无法获得 demo2 的对象锁而阻塞,就会产生死锁。重入锁的设计目的是避免线程的死锁。
ReentrantLock的源码分析
ReentrantLock.lock(),这个是 reentrantLock 获取锁的入口。
/**
* Acquires the lock.
*
* <p>Acquires the lock if it is not held by another thread and returns
* immediately, setting the lock hold count to one.
*
* <p>If the current thread already holds the lock then the hold
* count is incremented by one and the method returns immediately.
*
* <p>If the lock is held by another thread then the
* current thread becomes disabled for thread scheduling
* purposes and lies dormant until the lock has been acquired,
* at which time the lock hold count is set to one.
*/
public void lock() {
sync.lock();
}
注释翻译:
如果没有线程持有这把锁锁,则当前线程获取到锁,并设置线程的重入次数记为1。
如果是当前线程已经持有这把锁,则重入次数加1并返回。
如果是其他线程持有这把锁,则当前线程将因线程调度而被禁用,并处于休眠状态,直到获得锁,此时锁保持计数设置为1。
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。
sync 有两个具体的实现类:
-
NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁。
-
FailSync:表示所有线程严格按照 FIFO 来获取锁。
非公平锁和公平锁的区别在于,在非公平锁中,在获取锁的时候,不管有没有线程排队,先用 CAS 抢占一下。若CAS 成功,就表示成功获得了锁;CAS 失败,调用 acquire(1)走锁竞争逻辑。
以非公平锁为例,来看看 lock 中的实现:
final void lock() {
if (compareAndSetState(0, 1)) // 不管有没有线程排队,先用 CAS 抢占一下
setExclusiveOwnerThread(Thread.currentThread()); // CAS 成功,就表示成功获得了锁
else
acquire(1); // CAS 失败,调用 acquire(1)走锁竞争逻辑
}
此处的CAS:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
compareAndSwapInt:如果当前内存中的 state 的值和预期值 expect 相等,则替换为 update。更新成功返回 true,否则返 回 false。
state 是 AQS 中的一个属性,它在不同的实现中所表达的含义不一样,对于重入 锁的实现来说,表示一个同步状态。它有两个含义的表示
- 当 state=0 时,表示无锁状态
- 当 state>0 时,表示已经有线程获得了锁,也就是 state=1,但是因为 ReentrantLock 允许重入,所以同一个线程多次获得同步锁的时候,state 会递增, 比如重入 5 次,那么 state=5。而在释放锁的时候,同样需要释放 5 次直到 state=0 其他线程才有资格获得锁
unsafe类:Unsafe 类是在 sun.misc 包下,不属于 Java 标准。但是很多 Java 的基础类库,包 括一些被广泛使用的高性能开发库都是基于 Unsafe 类开发的,比如 Netty、 Hadoop、Kafka 等;Unsafe 可认为是 Java 中留下的后门,提供了一些低层次操作,如直接内存访问、 线程的挂起和恢复、CAS、线程同步、内存屏障。而 CAS 就是 Unsafe 类中提供的一个原子操作,第一个参数为需要改变的对象, 第二个为偏移量(即之前求出来的 headOffset 的值),第三个参数为期待的值,第 四个为更新后的值整个方法的作用是如果当前时刻的值等于预期值 var4 相等,则更新为新的期望值 var5,如果更新成功,则返回 true,否则返回 false;
再来看acquire(1)方法:
acquire 是 AQS 中的方法,如果 CAS 操作未能成功,说明 state 已经不为 0,此 时继续 acquire(1)操作。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在tryAcquire方法中就是重入性的体现了。
重入性的实现原理
ReentrantLock的核心其实是AQS的独占锁,而其重入性体现在:
- 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
- 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
获取锁
NonfairSync.tryAcquire方法:
这个方法重写了AQS的tryAcquire,作用是尝试获取锁,如果成功返回 true,不成功返回 false。
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
直接看nonfairTryAcquire(acquires)
:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. 如果该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.若被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error(""Maximum lock count exceeded"");
setState(nextc);
return true;
}
return false;
}
释放锁
Sync.tryAcquire方法:
这个方法在sync中而不是NonfairSync:
其源码:
protected final boolean tryRelease(int releases) {
// 同步状态减1,getState()获取的是线程的重入次数
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 只有当同步状态为0时,锁成功被释放,返回true
free = true;
setExclusiveOwnerThread(null);
}
// 锁未被完全释放,设置state为减去后的数量,返回false
setState(c);
return free;
}
公平锁和非公平锁
锁的公平性是相对于获取锁的顺序而言的,如果是一个公平锁,那么锁的获取顺序 就应该符合请求的绝对时间顺序,也就是 FIFO。 在上面分析的例子来说,只要 CAS 设置同步状态成功,则表示当前线程获取了锁,而公平锁则不一样,差异点有两个:
FairSync.lock:
final void lock() {
acquire(1);
}
直接acquire(1),并没有先CAS试一下。
FairSync.tryAcquire:
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;
}
和非公平锁的区别就是加上了hasQueuedPredecessors()方法:用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
参考:
《java并发编程的艺术》