ReentrantLock是什么?
ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似。不过,ReentrantLock更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
ReentrantLock里面有一个内部抽象类Sync,Sync继承了AQS(AbstractQueuedSynchronizer)类,添加锁和释放锁的大部分操作实际上都是在Sync中实现的。
Sync有公平锁和非公平锁两个子类,分别是公平锁FairSync类和非公平锁NonfairSync类
ReentrantLock构造函数如下,如果不传参数那么就初始化那么ReentrantLock默认使用非公平锁。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁的区别
公平锁:锁被释放之后,先申请的线程先得到锁,性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。但减少了线程饥饿的可能性。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
ReentrantLock做了什么?
ReentrantLock比synchronized增加了一些高级功能,主要来说主要三点:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁(等待可中断)
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知
两者都是可重入锁;
synchronized是JVM级别的,而ReentrantLock是API级别;
synchronized内置在JVM里面,而ReentrantLock是由代码实现的
ReentrantLock是怎么上锁的
线程加锁的流程是:.lock() -> .acquire() -> tryAcquire()
ReentrantLock类继承了 lock 接口,所以需要实现 lock() 方法
而ReetrantLock的lock()
函数的实现,是调用了内部类Sync的lock()
抽象方法,该方法在FairSync和NonfairSync中有不同的实现
FairSync中的lock直接调用了 acquire
方法
而NonfairSync会先尝试进行锁的获取,先调用 compareAndSetState()
争取一次,失败的话再调用 acquire
方法
并且两者的 tryAcquire
方法也不同
NonfairSync 类中执行 tryAcquire
, 当 getState == 0 时会直接去获取锁
而FairSync类中 tryAcquire
,即是 getState == 0 ,也需要先判断是否有线程在队列中排队,如果有的话就也要返回false
tryAcquire方法获取锁失败的话就会调用acquireQueued把当前线程放到等待队列里面去
AQS中的acquire方法运行流程
如果tryAcquire返回false,没有获取锁,那么就会使用addWait将当前节点添加到等待队列,然后调用acquireQueued
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued
在acquireQueued中,如果 p == head && tryAcquire(arg)
成功就 return interrupted(false),也就不会在acquire中自我中断
再次尝试加锁,非公平锁在这里就会再次抢一次锁
如果失败那么会通过 shouldParkAfterFailedAcquire
自旋一次,下一次依旧获取锁失败,那么就调用shouldParkAfterFailedAcquire判断是否应该调用park方法进入睡眠。
shouldParkAfterFailedAcquire(应该在获取锁失败后沉睡吗)
为什么在park前需要调用shouldParkAfterFailedAcquire判断是否应该调用park方法进入睡眠呢?
因为当前节点的线程进入park后只能被前一个节点唤醒,那前一个节点怎么知道有没有后继节点需要唤醒呢?因此当前节点在park前需要给前一个节点设置一个标识,即将waitStatus设置为Node.SIGNAL(-1),然后自旋一次再走一遍刚刚的流程,若还是没有获取到锁,则调用parkAndCheckInterrupt进入睡眠状态。
parkAndCheckInterrupt(睡眠且检查是否中断过)
parkAndCheckInterrupt通过 LockSupport.park(this)
进入睡眠后,有两种情况会被唤醒
一种是前一个节点持有锁,且锁被释放,那么前一个节点在执行 un.lock的时候会执行 LockSupport.unpark(s.thread)
,这里的被唤醒的s.thread就是当前线程,这种情况唤醒后parkAndCheckInterrupt
会返回false,表示没有被中断过,那么acquireQueued中的interrupted也就不会被设置为True
还有一种那就是被中断了,那么此种情况 parkAndCheckInterrupt会
返回 True,那么interrupted被设置为interrupted,
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//直到p节点是node节点的上一个节点
if (p == head && tryAcquire(arg)) {//如果p节点是head,那么再次使用tryAcquire尝试获取锁。如果获取成功就将头结点设置为当前节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
那么Thread.interrupted这个方法是做什么用的呢
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
这个是用来判断当前线程是否被打断过,并清除打断标记(若是被打断过则会返回true,并将打断标记设置为false),所以调用lock方法时,通过interrupt也是会打断睡眠的线程的,只是Doug Lea做了一个假象,让用户无感知;但有些场景又需要知道该线程是否被打断过,所以acquireQueued最终会返回interrupted打断标记,如果是被打断过,则返回的true,并在acquire方法中调用selfInterrupt再次打断当前线程(将打断标记设置为true)。
ReetrantLock可重入锁的具体实现步骤
NonfairSync的tryAcquire()
实现直接调用了Sync抽象类中已经实现的nonfairTryAcquire()方法:
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;
}
首先获取当前线程
getState()
方法是AQS类里面定义的,返回当前状态值,0表示未上锁状态,非0表示上锁状态.注意:这里的State可以设置为1,2,3,4…表示有多少重锁
如果c = 0,表示当前 lock对象
未上锁,那么调用compareAndSetState()
方法,和CAS类似,比较然后设置State值
该方法AQS类中的方法,最后的实现是调用的 Unsafe
类中的一个native方法,表示由外部实现,非JAVA代码实现(Unsafe
类经常用于执行一些原子操作)
compareAndSetState
如果成功上锁,那么会返回True。将ReentrantLock在 AbstractOwnableSynchronizer
类中的 exclusiveOwnerThread
变量设置为当前的线程。
先把State设置为1,才能 setExclusiveOwnerThread
exclusiveOwnerThread
作用是记录当前持有锁的线程
如果c != 0,表示当前 lock对象
已经被上锁,那么这里就是 ReentrantLock实现可重入锁的核心代码
判断当前线程是否在AbstractOwnableSynchronizer
类中记录的 exclusiveOwnerThread
,也即是持有锁的线程
如果当前线程就是持有锁的线程,那么将 State
的值设置为 State + 1 (acquire的值一般都是1)
如果c != 0,且当前线程不是锁的持有者,那么返回false,代表获取锁失败
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;
}
与NonFairSync中不同的就是当 c == 0时,也不能直接进行上锁操作,需要先执行 hasQueuedPredecessors()
方法判断是否存在前驱节点(线程):
如果 hasQueuedPredecessors()
返回 true,这意味着当前线程前面有其他线程在等待,因此当前线程不能立即尝试获取锁,它应该等待直到前面的线程释放锁。
如果 hasQueuedPredecessors()
返回 false,这意味着当前线程是队列中的第一个等待线程,它可以尝试通过 compareAndSetState(0, acquires) 来获取锁。
如果 !hasQueuedPredecessors()
返回false,那么就会把当前的线程放到队列当中去
可中断锁和不可中断锁有什么区别
可中断锁:获取锁的过程可以被中断,不需要一直等到获取锁之后,才能进行其他逻辑处理。ReentrantLock就属于是可中断锁
不可中断锁:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。synchronized就属于不可中断锁
相关的技术
ReadWriteLock
首先明确一下,不是说ReentrantLock不好,只是ReentrantLock某些时候有局限。如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
因为这个,才诞生了读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现
实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。