前言

ReentrantLock(可重入锁),它提供了与synchronized关键字类似的同步功能,但比synchronized更加灵活。

image-20240326151919092

从类图上可以看到,它实现了Lock接口与Serializable接口,这意味着它有锁的特性,并且可以被序列化。同时它有三个内部类,而且其中的两个子类FairSync和NofairSync又继承了Sync,它俩的区别是FairSync会先检查同步队列中是否有在等待的线程,有就将当前线程加入队列的尾部;NofairSync则是直接尝试竞争锁,得到锁就执行自己的逻辑。

ReentrantLock的一些主要特性:

  1. 可重入性:和synchronized一样,ReentrantLock也支持可重入性。也就是说,一个线程可以多次获取同一个锁,而不会造成死锁
  2. 公平性和非公平性:ReentrantLock可以在创建时指定公平性。如果设置为公平锁,那么等待时间最长的线程将优先获取锁。如果设置为非公平锁(默认),那么哪个线程获取锁是不确定的。
  3. 条件变量:ReentrantLock提供了一个Condition类,可以用于多线程之间的精确唤醒。这是synchronized关键字所不具备的
  4. 锁状态的查询:ReentrantLock提供了一些方法,可以查询锁的状态,例如查询是否有线程正在等待获取此锁

一个关于锁的小demo

这里我们创建5000个线程对count字段进行累加,如果在未加锁的情况下,结果可能不是5000。如果使用了ReentrantLock那么结果必定为5000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;

public static void main(String[] args) {
for (int i = 0; i < 5000; i++) {
new Thread(() -> {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}).start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("结果" + count);
}

构造器

1
2
3
4
5
6
7
8
// 构造一个非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 根据给定的公平策略构造公平所或非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

关键方法

加锁

lock()

尝试加锁

1
2
3
4
// 调用的是Sync类的加锁方法
public void lock() {
sync.lock();
}

继续跟进,initialTryLock()为尝试加锁的逻辑,这里公平所和非公平锁的实现逻辑基本一致,只是公平锁多了一段检查队列是否有等待线程的逻辑。如果没有获取到锁,则调用AQS的acquire(1)方法,里面会再次尝试获取锁的逻辑,如果还获取不到,就将当前线程加入到同步队列中。

1
2
3
4
final void lock() {
if (!initialTryLock())
acquire(1);
}

以公平锁为例,看下initialTryLock()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final boolean initialTryLock() {
Thread current = Thread.currentThread();
int c = getState(); // 获取加锁状态
if (c == 0) { // c == 0, 说明当前没有线程持有锁
// 检测等待队列中是否有等待中的线程,队列中有值或者没值但是没有竞争到锁都返回false
if (!hasQueuedThreads() && compareAndSetState(0, 1)) {
// 竞争到锁,返回true
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) { // 没竞争到锁,检查下持有锁的线程是否当前线程,是的话就累加state值。
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}

tryLock()

尝试竞争锁,竞争到返回true,没竞争到直接返回false,并且当前线程不会进入同步队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean tryLock() {
return sync.tryLock();
}
// 竞争到锁返回ture,如果是重复加锁则要家里state。没竞争到锁返回false
final boolean tryLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (getExclusiveOwnerThread() == current) {
if (++c < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(c);
return true;
}
return false;
}

lockInterruptibly()

如果加锁的过程中线程被中断,则抛出InterruptedException。

释放锁

unlock()

尝试去释放锁,调用的是AQS中的release方法

1
2
3
public void unlock() {
sync.release(1);
}

点进AQS类,它会调用子类的tryRelease(arg)去释放锁,如果释放锁成功就唤醒下个线程就竞争锁。如果释放成功就返回true。

1
2
3
4
5
6
7
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁
signalNext(head); // 唤醒下一个线程
return true;
}
return false;
}

现在看下Sync类中的释放锁逻辑。因为ReetrantLock是可重入锁,每当线程释放锁时state都会减1,所以在释放锁的时候它会检测state是否为0,为0就表示当前线程已经不持有锁了,此时就返回true。

1
2
3
4
5
6
7
8
9
10
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (getExclusiveOwnerThread() != Thread.currentThread()) // 如果不是持有锁的线程释放锁,则抛出异常
throw new IllegalMonitorStateException();
boolean free = (c == 0);
if (free)
setExclusiveOwnerThread(null);
setState(c);
return free;
}

Condition

条件等待,ReetrantLock的Condition功能都是基于AQS的,可以参考《JAVA源码解读之AbstractQueuedSynchronizer》。