重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。
阅读这个可重入锁类之前,可以先阅读我的上两篇文章,对lock以及AbstractQueuedSynchronizer这两个类的作用和设计有一个基础的了解。然后再看着个类的时候,会更好的理解。
AQS(AbstractQueuedSynchronizer)队列同步器源码阅读(二)
https://www.jianshu.com/p/e0066f9349cd
AQS(AbstractQueuedSynchronizer)队列同步器源码阅读(一)
https://www.jianshu.com/p/a41088fc1516
然后我们可以知道一般来说,ReentrantLock是一个可重入锁,所以会实现Lock这个类,并重写该类提供的几个方法:
//获取锁,释放锁
void lock();
void unlock();
//可相应线程中断的获取锁,当线程被中断后,锁会被释放。而一般的lock则不会响应
void lockInterruptibly() throws InterruptedException;
//尝试获取锁,获取不到则返回false
boolean tryLock();
//与上一个一样
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//获取对应的condition,后面会专门研究,现在只要知道,这个可以用来配合完成不同线程的等待/通知机制。
Condition newCondition();
然后一般重写以上几个方法时,需要依赖队列同步器来获取同步状态,获取不到需要加入同步队列等等,所以一般还会设计工具类Sync继承AbstractQueuedSynchronizer
继承AQS的子类重写以上AQS提供的方法后,对外ReentrantLock是实现Lock提供的几个方法一般是调用Sync继承的AQS里面的方法。例如:
加锁时:
//调用的是AQS的子类sync的lock
public void lock() {
sync.lock();
}
//而lock方法一般会根据具体的锁的设计去实现我们已经重写的acquire方法。大体的锁的设计不会偏差太多。
然后我们来具体看一下:
ReentrantLock 是一个可重入锁。
关于线程的调度策略分为公平调度和非公平调度。
他的设计同样是内部有一个继承AQS的静态内部类Sync,同时为了区分不同的调度策略,有设计了两个子类继承Sync,
static final class NonfairSync extends Sync{.....}
static final class FairSync extends Sync{.....}
看类名可以看出,一个是针对非公平调度策略设计的锁,一个是公平调度的。两种重写同步队列器的方法的实现不同。
再来看构造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这回应该不用多解释了吧,看类名。所以说可重入锁是默认非公平调度策略的,就是说线程执行的顺序是不是按执行时间顺序执行的,是非公平的,效率比较高。
可重入性
可重入锁顾名思义就是线程获取到锁之后,可以再次获取该锁,而不会被锁阻塞。该锁实现需要解决两个问题:
1.线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
2.锁的最终释放。。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。
以默认的非公平调度的锁实现来查看获取同步状态时,如何处理:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前state,重入数量
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;
}
该方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。
成功获取锁的线程再次获取锁,只是增加了同步状态值,这也就要求ReentrantLock在释放同步状态时减少同步状态值。
protected final boolean tryRelease(int releases) {
//释放时减去同步状态值
int c = getState() - releases;
//如果当前线程与持有线程不一致,则报对象头状态异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果同步状态值为0,代表该锁已全部释放,需要释放锁,使其他线程能够获取该锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
对于非公平锁,只要CAS设置同步状态成功,则表示当前线程获取了锁,而公平锁则不同.
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//主要区别如下: 即加入了同步队列中当前节点是否有前驱节点的判断,如果该方法返回true,
//则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。
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;
非公平调度策略加锁的流程
final void lock() {
//1.如果同步操作state,获取同步状态成功,则设置当前线程为当前独占式获取锁.否则进行获取。
//compareAndSetState(0, 1) 使用的是:
//unsafe.compareAndSwapInt(this, stateOffset, expect, update); CAS偏移地址来直接修改state的值。
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 2.否则获取同步状态。进去里面看实现
acquire(1);
}
//1.产时获取同步状态,(tryAcquire),
失败则加入队尾tail,状态设置为EXCLUSIVE,(addWaiter(Node.EXCLUSIVE), arg))
同时进入自旋去获取同步状态,直到该节点前一个节点为头节点并获取成功,则出队列,并唤醒下一个节点,并且响应中断。(acquireQueued()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire()看上面的分析.主要调用我们NonfairSync重写的nonfairTryAcquire。
如果获取非公平的可重入锁失败,则执行下面方法。该方法不做具体分析了,之前已经有具体分析过了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 1. 获得当前节点的先驱节点
final Node p = node.predecessor();
// 2. 当前节点能否获取独占式锁
// 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁
if (p == head && tryAcquire(arg)) {
//队列头指针用指向当前节点
setHead(node);
//释放前驱节点
p.next = null; // help GC
failed = false;
return interrupted;
}
// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
非公平调度策略释放锁的流程就不再说了,上面有。
然后本来想要再分享下,公平调度获取锁和解锁的过程,其实不用。解锁的过程是一致的,这个前面有说了。然后加锁的区别也是只有一点区别。就是获取同步状态的时候,需要判断前面是否有头节点,如果没有则可以直接获取同步状态,否则继续自旋。因为公平调度是根据时间线程执行的时间顺序获取锁的,所以通过控制这点,来判断是否按时间进行获取同步状态,从而控制获取锁的顺序。
从上面### 标记的那行开始看,以下代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
公平锁 VS 非公平锁
1.公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。
2.公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。
编写不易,给个赞