ReentrantLock是Java5引入的可重入锁,Lock的实现类,相比synchronized它提供更精细的同步操作,高竞争场景表现好
主要有如下几个特点:
ReentrantLock只适用于代码块,以线程作为同步单位,需要显式进行获取与释放锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// do something
} finally {
lock.unlock();
}
有两点要注意:
ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的,AQS是Java并发包中,实现各种同步结构和部分其他组成单元(如线程池中的Worker)的基础,它将基础的同步相关操作抽象了出来
AQS内部数据和方法,分为以下几类:
state状态,使用volatile修饰的int型变量,0表示未加锁状态,1表示已加锁状态
private volatile int state;
等待队列,基于双链表的FIFO队列,与waitStatus配合实现多线程间竞争和等待
static final class Node {
volatile int waitStatus;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
...
volatile Node prev;
volatile Node next;
volatile Thread thread;
...
}
private transient volatile Node head;
private transient volatile Node tail;
各种基于CAS的基础操作方法,如CAS操作state状态或等待队列节点,以及各种用于同步的基础功能方法
protected final boolean compareAndSetState(int expect, int update) {...}
public final void acquire(int arg)
public final boolean release(int arg)
...
对于CAS实现方式不熟悉的可以参考Java AtomicInteger 原理
ReentrantLock中有个重要的成员sync,通过继承AQS这个抽象类然后重写相关方法来实现
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {...}
ReentrantLock有两个构造方法,无参构造方法默认创建非公平锁(NonfairSync),有参构造方法传入true则会创建公平锁(FairSync)
公平锁与非公平锁通过继承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();
}
首先来分析下获取锁的lock方法,通过多态性会调用FairSync或NonfairSync内重写的lock方法
public void lock() {
sync.lock();
}
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1)) // 直接用CAS修改状态位,争抢锁
setExclusiveOwnerThread(Thread.currentThread()); // 争抢成功设置当前线程独占锁
else
acquire(1);
}
}
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
}
可以看到非公平锁会直接在这里开始争抢锁,在compareAndSetState中会尝试使用CAS操作将state状态从未加锁(0)置为加锁(1),若CAS操作失败表示锁已被其他线程持有,则与公平锁一样执行acquire
acquire是AQS提供的基类方法,作用是通过tryAcquire尝试争抢锁,争抢失败就把线程加入等待队列,加入排队竞争阶段
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS的tryAcquire是个空方法(只抛异常),真正实现是在NonfairSync与FairSync中,两者相比,非公平锁在无人占有锁时,并不会检查队列中是否有线程在等待
// 非公平锁tryAcquire实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 获取当前AQS内部状态量
if (c == 0) {// 0表示无人占有,则直接用CAS修改状态位
if (compareAndSetState(0, acquires)) {// 不检查排队情况,直接争抢
setExclusiveOwnerThread(current);// 争抢成功设置当前线程独占锁
return true;
}
}//即使状态不是0,也可能当前线程是锁持有者,因为这是再入锁
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;
}
// 公平锁tryAcquire实现,省略相同的内容
protected final boolean tryAcquire(int acquires) {
...
if (c == 0) {
if (!hasQueuedPredecessors() &&// 队列中有线程排队,则放弃这次争抢锁的机会
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
...
}
addWaiter会把线程包装成一个独占式的节点对象,并通过CAS操作将节点放入队列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {// 队列已创建则把节点放入队尾
node.prev = pred;// 当前节点的前节点指向尾节点
if (compareAndSetTail(pred, node)) {// 使用CAS把原尾节点替换为当前节点,使当前节点成为新的尾节点
pred.next = node;// 原尾节点的后节点指向当前节点
return node;
}
}
enq(node);// 队列未创建或节点入队失败
return node;
}
private Node enq(final Node node) {
for (;;) {// 死循环CAS入队,直到成功退出循环
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))// 将头节点对象地址与null比较,相同则替换为new Node()
tail = head;// 队列创建成功
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
入队成功后进入acquireQueued,这时当前节点会处于不断等待和唤醒的死循环,直到唤醒时争抢到锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();// 获取前节点
if (p == head && tryAcquire(arg)) {// 如果前节点是头节点则当前节点去竞争锁
setHead(node);// 竞争成功将当前节点设为头节点
p.next = null; // 前节点出队
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&// 判断是否当前节点是否要堵塞
parkAndCheckInterrupt())// 当前线程会被堵塞在此处
interrupted = true;// 被中断唤醒过就会被标记为interrupted
}
} finally {
if (failed)
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire中会将前节点的状态更新为SIGNAL,表示告知前节点在释放锁时要唤醒它的后节点,否则前节点若是取消状态的话将被移出队列
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)// 前节点是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);// 将前节点状态置为SIGNAL
}
return false;
}
线程会在parkAndCheckInterrupt中被堵塞,堵塞使用的是LockSupport.park,会被其他线程使用unpark唤醒,如果线程被中断也会退出堵塞状态
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
然后来分析下释放锁的unlock方法,公平锁与非公平锁释放锁的逻辑一样,都会调用AQS的release方法
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {//释放锁
Node h = head;
if (h != null && h.waitStatus != 0)//头节点等待状态不为0则表示有后节点等待它唤醒
unparkSuccessor(h);//唤醒后节点
return true;
}
return false;
}
在tryRelease中释放锁,如果锁重入了则重入次数-1
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 由于是重入锁,每次释放锁-1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);//释放独占线程
}
setState(c);
return free;
}
unparkSuccessor会唤醒不是取消状态的离头节点最近的后节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);//头节点等待状态置为0
Node s = node.next;
if (s == null || s.waitStatus > 0) {// 头节点的后节点为空或是取消状态
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)// 从队尾向前遍历
if (t.waitStatus <= 0)
s = t;// 找到离头节点最近且不是取消状态的节点
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒节点
}
Java核心技术面试精讲
https://juejin.im/post/5c95df97e51d4551d06d8e8e#heading-5
https://ddnd.cn/2019/03/15/java-abstractqueuedsynchronizer/
https://www.cnblogs.com/waterystone/p/4920797.html