本文深入理解一下ReenTrantLock,翻译为重入锁,是lock接口的一个实现类,也是使用频率很高的一个同步组件,同时在并发容器中也有使用。支持重入性,同时支持公平锁和非公平锁。
简介
- Sync是AQS的一个子类
- NonfairSync继承自Sync,为非公平锁
- FairSync 继承自Sync ,公平锁
这三个类通过重写AQS中的方法和调用AQS提供的方法实现锁的获取和释放。并且主要重写的方法为TryAquire和TryRelease方法,也就是ReenTrantLock为独占式锁。
ReenTrantLock的重入
ReenTrantLock是独占式锁,AQS的state属性既可以表示同步状态也可以记录重入次数。因为ReenTrantLock获取锁的过程是互斥的,也就是同一时刻只有一个线程可以获取得到锁,那么可以通过AQS的state>0来表示同步状态,state的数值来表示重入次数。
获取锁
以下代码就是重入逻辑:
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()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
测试: 以debug模式启动,断点打在最后一个lock()处:
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(()->{
try {
lock.lock();
lock.lock();
lock.lock();
}finally {
//不释放锁
}
}).start();
}
state为2,也就是ReenTrantLock支持重入,重入逻辑就是同步状态值加一。
再看一下释放锁的逻辑:
当前同步状态减一,当同步状态为0,返回true,锁释放成功。
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;
}
对比Synchronized
我们知道Synchronized是通过monitorEnter和monitorExit两个指令来隐式的加锁和释放锁。执行monitorEnter指令前需要获取对象监视器monitor。Synchronized支持重入,线程执行monitorEnter指令计数器加一,monitorExit指令计数器减一,当计数器为0时锁就释放成功。
public static void main(String[] args) throws InterruptedException {
//共享资源
Object o = new Object();
try {
synchronized (o){
System.out.println("首次获取锁,进入同步代码");
synchronized (o){
System.out.println("再次获取锁");
}
}
}catch (Exception e){
System.out.println("异常");
}
}
用javap -v查看字节码文件
后面的两个monitorExit指令,是遇到异常需要执行的指令。
公平锁和非公平锁
公平锁表现在'排队获取锁',针对获取锁的过程是公平的,即满足先进先出。非公平锁表现在竞争,谁有能耐谁获取锁,竞争存在于head节点释放锁时。
ReenTrantLock默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁相比非公平锁只是多了一个hasQueuedPredecessors()来判断同步队列是否为存在节点的判断,具体看实现逻辑:
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;
}
}
这个方法判断的是当前同步队列是否还有正在排队节点,存在返回true、不存在返回false。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
//获取头结点和尾结点
Node t = tail; // Read fields in reverse initialization order
Node h = head;
//缓存
Node s;
//头结点尾结点不是一个节点,也就是完成了初始化 并且同步队列存在其他节点
return h != t &&
//头结点的后继节点为null 当前线程不是头结点后驱节点线程
((s = h.next) == null || s.thread != Thread.currentThread());
}
图示:
同步队列原状态:
解释:sync为同步器、head和tail为头尾节点、exclusiveOwner为同步状态拥有者(这是AQS父类一个属性)、Node们构成同步队列。
公平锁情况下排队获取锁:
公平锁情况下,会先判断同步队列列里是否还有节点,如果有则获取同步状态失败,尾插法将节点插入同步队列,并且使得sync的tail指向刚插入的节点。即公平锁在同步队列有节点的情况下不会进行cas竞争。
非公平锁情况下: 可能会出现这种情况,cas竞争
当头结点完全释放锁时,如果此刻刚好启动一个线程去获取锁,同时此刻head节点的后驱节点也在cas自旋尝试获取得到锁,如果head节点的后驱节点cas失败了,也就出现上图所示状态,同步队列没有变化,但是锁却被一个同步队列外的线程获取,形成了插队!!!
公平锁 VS 非公平锁
- 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
- 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。