目录
(一)基于 AQS(AbstractQueuedSynchronizer)
引言
在 Java 并发编程中,线程同步是确保多线程程序正确性和一致性的关键。synchronized
关键字是 Java 中最常用的同步机制,但从 Java 5 开始,java.util.concurrent.locks
包中引入了 ReentrantLock
类,它作为一种显式锁,提供了比 synchronized
更强大、更灵活的同步功能。深入理解 ReentrantLock
的特性、使用方法和实现原理,对于编写高效、健壮的并发程序至关重要。
一、ReentrantLock 概述
(一)基本概念
ReentrantLock
是可重入的互斥锁,这意味着同一个线程可以多次获取该锁而不会产生死锁。“可重入” 特性允许线程在持有锁的情况下,再次进入被该锁保护的代码块,每一次重入都会使锁的持有计数加 1,而每次释放锁时,持有计数减 1,只有当持有计数为 0 时,锁才会真正被释放。
(二)与 synchronized 的对比
- 灵活性:
ReentrantLock
提供了更丰富的 API,例如可以尝试获取锁(tryLock()
)、可以设置获取锁的超时时间(tryLock(long timeout, TimeUnit unit)
)、可以实现公平锁(ReentrantLock(boolean fair)
构造函数传入true
)等,而synchronized
则相对固定,缺乏这些灵活的控制。 - 锁的释放:
synchronized
是隐式锁,当代码块执行完毕或抛出异常时,锁会自动释放;而ReentrantLock
是显式锁,需要在finally
块中手动调用unlock()
方法来释放锁,否则可能会导致锁无法释放,造成死锁。 - 锁的获取:
synchronized
无法中断正在等待获取锁的线程,而ReentrantLock
可以通过lockInterruptibly()
方法允许线程在等待锁的过程中被中断。
二、ReentrantLock 的使用方法
(一)基本使用
以下是一个简单的使用 ReentrantLock
实现线程同步的示例:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockExample example = new ReentrantLockExample();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
example.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Count: " + example.getCount());
}
}
在上述代码中,increment()
和 getCount()
方法使用 ReentrantLock
来保证线程安全,通过 lock.lock()
获取锁,在 finally
块中使用 lock.unlock()
释放锁。
(二)尝试获取锁
tryLock()
方法用于尝试获取锁,如果锁当前可用,则获取锁并返回 true
;如果锁不可用,则立即返回 false
,不会阻塞线程。以下是一个使用 tryLock()
的示例:
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
if (lock.tryLock()) {
try {
// 执行需要同步的操作
System.out.println("获取到锁,执行操作");
} finally {
lock.unlock();
}
} else {
System.out.println("未获取到锁,执行其他操作");
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
example.doSomething();
}
}
(三)可中断的锁获取
lockInterruptibly()
方法允许线程在等待获取锁的过程中被中断。如果线程在等待锁的过程中被其他线程中断,则会抛出 InterruptedException
异常。以下是一个使用 lockInterruptibly()
的示例:
import java.util.concurrent.locks.ReentrantLock;
public class InterruptibleLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() throws InterruptedException {
lock.lockInterruptibly();
try {
// 执行需要同步的操作
System.out.println("获取到锁,开始工作");
Thread.sleep(5000);
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
InterruptibleLockExample example = new InterruptibleLockExample();
Thread t = new Thread(() -> {
try {
example.doWork();
} catch (InterruptedException e) {
System.out.println("线程被中断");
}
});
t.start();
try {
Thread.sleep(2000);
t.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
(四)公平锁
ReentrantLock
可以通过构造函数 ReentrantLock(boolean fair)
创建公平锁,当 fair
参数为 true
时,表示创建公平锁。公平锁会按照线程请求锁的顺序依次获取锁,避免某些线程长时间得不到锁的情况。但公平锁的性能相对较低,因为需要维护一个有序的线程队列。以下是一个使用公平锁的示例:
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
private final ReentrantLock fairLock = new ReentrantLock(true);
public void doSomething() {
fairLock.lock();
try {
// 执行需要同步的操作
System.out.println(Thread.currentThread().getName() + " 获取到公平锁");
} finally {
fairLock.unlock();
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
for (int i = 0; i < 5; i++) {
new Thread(() -> example.doSomething()).start();
}
}
}
三、ReentrantLock 的实现原理
(一)基于 AQS(AbstractQueuedSynchronizer)
ReentrantLock
的实现依赖于 Java 中的 AQS(AbstractQueuedSynchronizer),AQS 是一个用于构建锁和同步器的框架,它提供了一个基于 FIFO 队列的同步机制。ReentrantLock
通过继承 AQS 并实现其相关方法来实现锁的功能。
(二)状态变量
AQS 内部维护了一个 int
类型的状态变量 state
,用于表示锁的状态。对于 ReentrantLock
,当 state
为 0 时,表示锁未被任何线程持有;当 state
大于 0 时,表示锁已经被某个线程持有,并且 state
的值表示该线程重入锁的次数。
(三)线程队列
AQS 还维护了一个 FIFO 的线程队列,当线程请求锁但锁不可用时,线程会被加入到该队列中等待。当锁被释放时,队列中的第一个线程会被唤醒并尝试获取锁。
四、使用 ReentrantLock 的注意事项
(一)锁的释放
由于 ReentrantLock
是显式锁,必须在 finally
块中调用 unlock()
方法来释放锁,以确保无论是否发生异常,锁都能被正确释放。
(二)避免死锁
在使用 ReentrantLock
时,要注意避免死锁的发生。例如,不要在持有一个锁的情况下尝试获取另一个锁,或者在多个线程中以不同的顺序获取锁。
(三)性能考虑
虽然 ReentrantLock
提供了更强大的功能,但在一些简单的同步场景中,使用 synchronized
可能会更简单、性能更好。因为 synchronized
是 JVM 内置的同步机制,经过了大量的优化。
五、总结
ReentrantLock
作为 Java 中的显式锁,提供了比 synchronized
更灵活、更强大的同步功能。通过合理使用 ReentrantLock
的各种特性,如尝试获取锁、可中断的锁获取和公平锁等,可以编写出更高效、更健壮的并发程序。同时,深入理解 ReentrantLock
的实现原理和使用注意事项,能够帮助我们更好地应对并发编程中的各种挑战。在实际开发中,应根据具体的业务场景和需求,选择合适的同步机制。