本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。
ReentrantLock简介
ReentrantLock是Java并发包中提供的一个可重入的互斥锁。实现于Lock接口,作用与synchronized相同,不过对比于synchronized更加灵活,但是使用时需要我们手动获取/释放锁。ReentrantLock和synchronized在基本用法,行为语义上是类似的,都具有可重入性。
ReentrantLock实现
ReentrantLock的所有锁相关操作都是通过Sync类实现,Sync继承于AbstractQueuedSynchronizer同步队列,并实现一些通用的接口实现。NonfairSync继承于Sync,实现了非公平的方式获取锁;FairSync继承于Sync,实现了公平的方式获取锁。
ReentrantLock所提供的一些方法如下:
// 查询当前线程调用lock()的次数
int getHoldCount()
// 返回目前持有此锁的线程,如果此锁不被任何线程持有,返回null
protected Thread getOwner();
// 返回一个集合,它包含可能正等待获取此锁的线程,其内部维持一个队列(后续分析)
protected Collection<Thread> getQueuedThreads();
// 返回正等待获取此锁资源的线程估计数
int getQueueLength();
// 返回一个集合,它包含可能正在等待与此锁相关的Condition条件的线程(估计值)
protected Collection<Thread> getWaitingThreads(Condition condition);
// 返回调用当前锁资源Condition对象await方法后未执行signal()方法的线程估计数
int getWaitQueueLength(Condition condition);
// 查询指定的线程是否正在等待获取当前锁资源
boolean hasQueuedThread(Thread thread);
// 查询是否有线程正在等待获取当前锁资源
boolean hasQueuedThreads();
// 查询是否有线程正在等待与此锁相关的Condition条件
boolean hasWaiters(Condition condition);
// 返回当前锁类型,如果是公平锁返回true,反之则返回flase
boolean isFair()
// 查询当前线程是持有当前锁资源
boolean isHeldByCurrentThread()
// 查询当前锁资源是否被线程持有
boolean isLocked()
非公平锁源码中的加锁流程如下:
//非公平锁NonfairSync的实现,其继承于Sync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//lock接口实现;
//首先通过CAS试图获取锁,获取成功则设置锁的Owner;
//否则调用acquire获取锁,acquire又或调用tryAcquire获取锁,
//而tryAcquire是通过非公平的方式获取锁。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//非公平方式获取锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
这块代码的含义为:
若通过CAS设置变量State(同步状态)成功,也就是获取锁成功,则将当前线程设置为独占线程。
若通过CAS设置变量State(同步状态)失败,也就是获取锁失败,则进入Acquire方法进行后续处理。
再看下公平锁源码中获锁的方式:
//公平锁FairSync 的实现,其继承于Sync
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//lock接口实现,自己调用acquire获取锁;
//acquire又会调用tryAcquire获取锁,而tryAcquire是通过公平(FIFO)
//的方式获取锁。
final void lock() {
acquire(1);
}
//公平的方式获取锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取当前锁状态,此状态c>0表示有线程获取到锁,重入的次数为c
int c = getState();
//无线程获取锁?
if (c == 0) {
//当前节点无前驱节点并且当前线程CAS更新状态成功;、
//表示当前线程公平的获取到锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//获得锁的线程就是当前线程?则获取次数加1,并设置状态(即次数)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
通过源码分析可知,当某个节点获取到锁时,会通过setExclusiveOwnerThread()方法记录获取独占锁的线程Thread;当某个线程获取锁时,当锁已被占用,会判断占用锁的线程是否为当前线程;是则直接更新锁状态,表示获取到锁;否则获取锁失败。
公平锁是通过FairSync实现的,其在tryAcquire获取锁时,会判断同步队列中当前节点是否有前驱节点;有前驱节点,则获取锁失败,进入同步队列,等待获取锁;无前驱节点时,表示当前节点是同步队列中等待锁时间最长的节点,则当前节点优先获取锁资源。
非公平锁是通过NonfairSync实现的,其在lock及tryAcquire时,会先通过CAS的方式尝试获取锁,获取失败才会进入同步队列等待。这就导致当某个线程刚释放锁,而同步队列中被unpark的头节点还未CAS获取到锁的时间间隙,当前线程先于同步队列头结点通过CAS获取锁。使得某些线程会等待很长时间才会获得锁,这是非公平性的。
ReetrantLock中的unlock()释放锁
在使用ReetrantLock这类显式锁时,获取锁之后也需要手动释放锁资源。unlock()释放锁的代码如下:
// ReetrantLock → unlock()方法
public void unlock() {
sync.release(1);
}
// AQS → release()方法
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
// 获取头结点用于判断
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒后继节点的线程
unparkSuccessor(h);
return true;
}
return false;
}
// ReentrantLock → Sync → tryRelease(int releases)方法
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;
// 设置Owner为null
setExclusiveOwnerThread(null);
}
// 设置更新同步状态
setState(c);
return free;
}
unlock()方法调用tryRelease(int releases)释放锁的,而tryRelease(int releases)则是ReetrantLock实现的方法,因为在AQS中没有提供具体实现,释放锁资源后会使用unparkSuccessor(h)唤醒后继节点的线程。unparkSuccessor(h)的代码如下:
private void unparkSuccessor(Node node) {
// node一般为当前线程所在的节点,获取当前线程的等待状态
int ws = node.waitStatus;
if (ws < 0) // 置零当前线程所在的节点状态,允许失败
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next; // 获取当前节点的后继节点
if (s == null || s.waitStatus > 0) { // 如果为空或已结束
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
// 等待状态<=0的节点,代表是还有效的节点
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 唤醒后继节点线程
}
总结
ReetrantLock总结。同步状态标识:对外显示锁资源的占有状态。同步队列:存放获取锁失败的线程。等待队列:用于实现多条件唤醒。Node节点:队列的每个节点,线程封装体。cas修改同步状态标识,获取锁失败加入同步队列阻塞,释放锁时唤醒同步队列第一个节点线程。
加锁过程:调用tryAcquire()修改标识state,成功返回true执行,失败加入队列等待。加入队列后判断节点是否为signal状态,是就直接阻塞挂起当前线程。如果不是则判断是否为cancel状态,是则往前遍历删除队列中所有cancel状态节点。如果节点为0或者propagate状态则将其修改为signal状态。阻塞被唤醒后如果为head则获取锁,成功返回true,失败则继续阻塞。
解锁过程:调用tryRelease()释放锁修改标识state,成功则返回true,失败返回false。释放锁成功后唤醒同步队列后继阻塞的线程节点,被唤醒的节点会自动替换当前节点成为head节点。