在多线程编程中,锁机制是保证线程安全的重要手段。ReentrantLock 是 Java 中实现 Lock 接口的一个常用类,它支持重入性,即同一个线程可以多次获取同一个锁而不会被阻塞。本文将深入探讨 ReentrantLock 的实现原理、使用方法以及与 synchronized 关键字的对比。
1 ReentrantLock 的实现原理
ReentrantLock 的核心在于其重入性,即允许同一个线程多次获取同一个锁。为了实现这一特性,ReentrantLock 需要解决两个关键问题:
线程重入问题:如果当前线程已经持有锁,再次获取锁时应该直接成功。锁的释放问题:锁被获取了 n 次,必须被释放 n 次后,锁才算完全释放。
1.1 重入性的实现
ReentrantLock 通过 AQS(AbstractQueuedSynchronizer)框架来实现其同步语义。以非公平锁为例,核心方法 nonfairTryAcquire 的实现如下:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//1. 如果该锁未被任何线程占有,该锁能被当前线程获取
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//2.若被占有,检查占有线程是否是当前线程
else if (current == getExclusiveOwnerThread()) {
// 3. 再次获取,计数加一
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
步骤1:如果锁未被任何线程持有(c == 0),则尝试通过 CAS 操作获取锁。步骤2:如果锁已经被当前线程持有(current == getExclusiveOwnerThread()),则增加同步状态(state)的值,表示重入次数加一。
1.2 锁的释放
锁的释放逻辑在 tryRelease 方法中实现:
protected final boolean tryRelease(int releases) {
//1. 同步状态减1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//2. 只有当同步状态为0时,锁成功被释放,返回true
free = true;
setExclusiveOwnerThread(null);
}
// 3. 锁未被完全释放,返回false
setState(c);
return free;
}
步骤1:减少同步状态的值。步骤2:只有当同步状态为 0 时,锁才算完全释放,返回 true。步骤3:如果同步状态不为 0,表示锁还未完全释放,返回 false。
2 公平锁与非公平锁
ReentrantLock 支持两种锁模式:公平锁和非公平锁。
非公平锁:线程获取锁的顺序可能与请求锁的顺序不同,可能导致某些线程获取锁的速度较快。公平锁:线程按照请求锁的顺序获取锁,即先到先得(FIFO)。
ReentrantLock 的构造方法默认创建非公平锁:
public ReentrantLock() {
sync = new NonfairSync();
}
也可以通过传入 boolean 值来选择公平锁或非公平锁:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁在获取锁时会检查当前线程是否有前驱节点(hasQueuedPredecessors),以确保锁的获取顺序符合公平性原则。核心方法为:
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;
}
公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。
3 ReentrantLock 的使用
ReentrantLock 的使用方式与 synchronized 关键字类似,通过加锁和释放锁来实现同步。以下是一个简单的示例:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count);
}
}
在这个示例中,两个线程分别对 count 变量进行 10000 次累加操作,最终输出 count 的值为 20000,说明 ReentrantLock 支持重入性。
4 ReentrantLock 与 synchronized 的对比
ReentrantLock 与 synchronized 都是 Java 中常用的同步机制,但它们在实现和使用上有一些区别:
实现方式:ReentrantLock 是一个类,而 synchronized 是 Java 中的关键字。灵活性:ReentrantLock 可以绑定多个 Condition,实现多路通知;而 synchronized 只能通过 wait 和 notify/notifyAll 实现单路通知。锁的释放:ReentrantLock 必须手动释放锁,通常在 finally 块中调用 unlock 方法;而 synchronized 会自动释放锁,当同步块执行完毕时,由 JVM 自动释放。性能:在高竞争环境下,ReentrantLock 通常提供更好的性能;而在低竞争环境下,两者的性能差距不大。
以下是一个简单的性能比较示例:
import java.util.concurrent.locks.ReentrantLock;
public class PerformanceTest {
private static final int NUM_THREADS = 10;
private static final int NUM_INCREMENTS = 1_000_000;
private int count1 = 0;
private int count2 = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Object syncLock = new Object();
public void increment1() {
lock.lock();
try {
count1++;
} finally {
lock.unlock();
}
}
public void increment2() {
synchronized (syncLock) {
count2++;
}
}
public static void main(String[] args) throws InterruptedException {
PerformanceTest test = new PerformanceTest();
// Test ReentrantLock
long startTime = System.nanoTime();
Thread[] threads = new Thread[NUM_THREADS];
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment1();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
long endTime = System.nanoTime();
System.out.println("ReentrantLock time: " + (endTime - startTime) + " ns");
// Test synchronized
startTime = System.nanoTime();
for (int i = 0; i < NUM_THREADS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < NUM_INCREMENTS; j++) {
test.increment2();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
endTime = System.nanoTime();
System.out.println("synchronized time: " + (endTime - startTime) + " ns");
}
}
输出结果可能如下:
ReentrantLock time: 158786700 ns
synchronized time: 499476900 ns
5 总结
本文详细介绍了 ReentrantLock 的实现原理、使用方法以及与 synchronized 关键字的对比。ReentrantLock 通过 AQS 框架实现了重入性,支持公平锁和非公平锁,并且在高竞争环境下通常具有更好的性能。在实际开发中,根据具体需求选择合适的锁机制,可以有效提升程序的并发性能。
通过本文的学习,相信读者对 ReentrantLock 有了更深入的理解,能够在实际项目中灵活运用这一强大的同步工具。
6 思维导图
7 参考链接
深入理解Java并发重入锁ReentrantLock