目录
[1 互斥性](#1 互斥性)
[2 公平性](#2 公平性)
[3 可重入性](#3 可重入性)
[4 获取和释放锁](#4 获取和释放锁)
[5 尝试获取锁](#5 尝试获取锁)
[6 可中断的锁定](#6 可中断的锁定)
[7 条件变量](#7 条件变量)
[8 性能](#8 性能)
[9 使用场景](#9 使用场景)
ReentrantLock
是 Java 提供的一种可重入的互斥锁,位于 java.util.concurrent.locks
包中,它实现了 Lock
接口。这个锁提供了与内置监视器锁(通过 synchronized
关键字实现)类似的互斥性和内存可见性,但具有更强大的功能和灵活性。以下是 ReentrantLock
的一些基本概念:
1 互斥性
ReentrantLock
可以确保同一时间只有一个线程可以执行由该锁保护的代码块,从而避免了竞态条件。
2 公平性
- 非公平锁 :默认情况下,
ReentrantLock
是非公平的。这意味着线程获取锁的顺序并不是按照它们请求锁的顺序来确定的。非公平锁通常提供更高的吞吐量。 - 公平锁 :当创建
ReentrantLock
实例时,可以通过传入true
参数来指定为公平锁。公平锁保证线程将按照它们请求锁的顺序获得锁。这可能会降低吞吐量,但能减少饥饿现象。
示例代码: 默认是非公平锁,如果需要公平锁,则在构造函数中传入 true
。
java
import java.util.concurrent.locks.ReentrantLock;
// 默认非公平锁
ReentrantLock lock = new ReentrantLock();
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
3 可重入性
可重入意味着如果一个线程已经持有某个锁,那么它可以再次获取这个锁而不会被阻塞。这是非常有用的特性,因为它允许方法在调用其他可能也需要相同锁的方法时不会导致死锁。
示例代码:
java
public void reentrantExample() {
lock.lock();
try {
// 第一次获取锁
System.out.println("First time locked, thread: " + Thread.currentThread().getName());
// 在已经持有锁的情况下再次获取锁
lock.lock();
try {
System.out.println("Second time locked, thread: " + Thread.currentThread().getName());
} finally {
lock.unlock(); // 释放第二次获取的锁
}
} finally {
lock.unlock(); // 释放第一次获取的锁
}
}
输出结果:
java
First time locked, thread: main
Second time locked, thread: main
**使用可重入锁的具体例子:**假设有一个类,其中包含一个递归方法,且这个方法需要是线程安全的。由于方法会多次调用自身,因此需要一个可重入锁来确保同一个线程可以重复获取锁。
java
import java.util.concurrent.locks.ReentrantLock;
public class FactorialCalculator {
private final ReentrantLock lock = new ReentrantLock();
public int factorial(int number) {
lock.lock(); // 获取锁
try {
if (number <= 1) {
return 1;
} else {
return number * factorial(number - 1); // 递归调用
}
} finally {
lock.unlock(); // 释放锁
}
}
}
4 获取和释放锁
获取锁通过调用 lock()
方法,释放锁则通过 unlock()
方法。通常建议在 finally
块中释放锁,以确保即使发生异常也能正确释放锁。
java
public void doSomething() {
lock.lock();
try {
// 执行同步代码
} finally {
lock.unlock();
}
}
5 尝试获取锁
tryLock()
方法尝试获取锁,如果无法立即获得锁,则返回 false
,不会阻塞线程。
java
if (lock.tryLock()) { // 尝试获取锁,如果无法获取,则立即返回false
try {
// 执行同步代码
} finally {
lock.unlock();
}
} else {
// 锁未获得时的操作
}
tryLock()
方法有两个版本:
- 无参数的
tryLock()
:尝试获取锁,如果锁可用则立即获取并返回true
,否则立即返回false
。 - 带超时的
tryLock(long timeout, TimeUnit unit)
:尝试获取锁,如果在指定的时间内可以获取到锁,则返回true
;如果超过了指定时间仍无法获取到锁,则返回false
。
具体使用示例:
无参数的 tryLock():
java
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
if (lock.tryLock()) { // 尝试获取锁
try {
// 执行需要同步的操作
System.out.println("Thread " + Thread.currentThread().getName() + " is doing work.");
} finally {
lock.unlock(); // 确保释放锁
}
} else {
// 如果无法获取锁,执行其他操作
System.out.println("Thread " + Thread.currentThread().getName() + " could not get the lock and will do something else.");
}
}
public static void main(String[] args) {
TryLockExample example = new TryLockExample();
// 创建两个线程来调用 doWork 方法
Thread t1 = new Thread(() -> example.doWork(), "Thread-1");
Thread t2 = new Thread(() -> example.doWork(), "Thread-2");
t1.start();
t2.start();
}
}
在这个例子中,如果 Thread-1
先获取到了锁,那么 Thread-2
将无法获取锁,并且会直接输出"无法获取锁"的信息。
带超时的 tryLock(long timeout, TimeUnit unit)
:
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockWithTimeoutExample {
private final ReentrantLock lock = new ReentrantLock();
public void doWork() {
boolean locked = false;
try {
// 尝试在5秒内获取锁
locked = lock.tryLock(5, TimeUnit.SECONDS);
if (locked) {
// 执行需要同步的操作
System.out.println("Thread " + Thread.currentThread().getName() + " is doing work.");
} else {
// 如果在5秒内无法获取锁,执行其他操作
System.out.println("Thread " + Thread.currentThread().getName() + " could not get the lock within 5 seconds and will do something else.");
}
} catch (InterruptedException e) {
// 处理中断异常
Thread.currentThread().interrupt();
System.out.println("Thread " + Thread.currentThread().getName() + " was interrupted while waiting for the lock.");
} finally {
if (locked) {
lock.unlock(); // 确保释放锁
}
}
}
public static void main(String[] args) {
TryLockWithTimeoutExample example = new TryLockWithTimeoutExample();
// 创建两个线程来调用 doWork 方法
Thread t1 = new Thread(() -> example.doWork(), "Thread-1");
Thread t2 = new Thread(() -> example.doWork(), "Thread-2");
t1.start();
t2.start();
}
}
在这个例子中,如果 Thread-1
持有锁超过5秒钟,那么 Thread-2
将会在等待5秒后放弃尝试获取锁,并执行相应的逻辑。
使用场景
- 避免死锁 :当多个线程试图以不同的顺序获取多个锁时,可能会导致死锁。使用
tryLock()
可以帮助检测这种情况,并采取适当的措施。 - 提高响应性 :在某些情况下,你可能不希望线程一直等待锁,而是希望线程能够快速响应其他任务。这时可以使用
tryLock()
来检查锁是否可用,如果不可用就立即执行其他工作。 - 资源竞争控制 :在资源有限的情况下,可以使用
tryLock()
来尝试获取资源,如果获取不到则可以选择放弃或重试。
6 可中断的锁定
lockInterruptibly()
方法允许在等待获取锁的过程中响应中断。
java
try {
lock.lockInterruptibly(); // 尝试获取锁,可以响应中断
try {
// 执行同步代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断情况
}
示例代码:
java
import java.util.concurrent.locks.ReentrantLock;
public class CancellableTask {
private final ReentrantLock lock = new ReentrantLock();
private boolean isCancelled = false;
public void runTask() {
Thread thread = new Thread(() -> {
try {
// 尝试获取锁,同时可以响应中断
lock.lockInterruptibly();
try {
// 模拟任务执行
while (!isCancelled) {
System.out.println("Task is running...");
// 假设任务需要一段时间完成
Thread.sleep(1000);
}
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
System.out.println("Task was interrupted, stopping execution.");
// 通常在这里你会清理资源并退出
// 重新设置中断状态,以便调用者知道该线程已被中断
Thread.currentThread().interrupt();
}
});
thread.start();
// 在某个时刻决定取消任务
try {
Thread.sleep(3000); // 等待几秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
isCancelled = true;
thread.interrupt(); // 中断线程
}
public static void main(String[] args) {
CancellableTask task = new CancellableTask();
task.runTask();
}
}
当主线程决定取消任务时,它会中断工作线程。工作线程会在 lockInterruptibly()
调用处立即响应中断,并抛出 InterruptedException
,从而允许任务快速终止。
输出结果:
java
Task is running...
Task is running...
Task is running...
Task was interrupted, stopping execution.
除了使用 lockInterruptibly()
来实现中断锁,还可以使用带超时的 tryLock(long timeout, TimeUnit unit)来达成这一目的。以下是例子:
java
import java.util.concurrent.locks.ReentrantLock;
public class ResourceService {
private final ReentrantLock resourceLock = new ReentrantLock();
public void useResource() {
Thread thread = new Thread(() -> {
try {
if (resourceLock.tryLock()) {
try {
// 使用资源
System.out.println("Using resource...");
Thread.sleep(10000); // 模拟长时间操作
} finally {
resourceLock.unlock();
}
} else {
System.out.println("Could not acquire the resource, operation is cancelled.");
}
} catch (InterruptedException e) {
System.out.println("Resource usage was interrupted, releasing the resource.");
resourceLock.unlock(); // 如果已经获得锁,则释放
Thread.currentThread().interrupt(); // 保持中断状态
}
});
thread.start();
// 在某个时刻决定取消使用资源的操作
try {
Thread.sleep(5000); // 等待几秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt(); // 中断线程
}
public static void main(String[] args) {
ResourceService service = new ResourceService();
service.useResource();
}
}
是的,tryLock(long time, TimeUnit unit)
方法也可以响应中断。当一个线程调用 tryLock(long time, TimeUnit unit)
时,它会尝试在指定的时间内获取锁。如果在这段时间内没有获取到锁,方法将返回 false
。此外,如果在此期间线程被中断,该方法会立即抛出 InterruptedException
并且不会获取锁。
注意: ReentrantLock
的 lock()
方法确实不会响应中断。当一个线程调用 lock()
试图获取锁时,如果锁已经被其他线程持有,那么该线程将一直阻塞,直到它能够获取到锁为止。即使在此期间该线程被中断(例如通过调用 Thread.interrupt()
),它也不会抛出 InterruptedException
或者以其他方式退出等待状态。只有在获取到锁之后,中断状态才会被检查。
7 条件变量
ReentrantLock
提供了条件变量 Condition
,它可以替代传统的 Object.wait/notify
机制。这些对象类似于 Object
类中的 wait/notify
机制,但是更加灵活。每个 Condition
实例都可以独立地挂起和唤醒线程,这对于复杂的同步需求是非常有用的。
下面是一个具体的例子,展示了如何使用条件变量来实现生产者-消费者模式:
在这个模式中,有一个共享缓冲区,生产者向缓冲区添加元素,消费者从缓冲区移除元素。当缓冲区满时,生产者必须等待;当缓冲区空时,消费者必须等待。
java
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.LinkedList;
public class ProducerConsumerExample {
private final int BUFFER_SIZE = 5; // 缓冲区大小
private final LinkedList<Integer> buffer = new LinkedList<>();
private final ReentrantLock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 缓冲区不满的条件
private final Condition notEmpty = lock.newCondition(); // 缓冲区不空的条件
public void put(int value) throws InterruptedException {
lock.lock();
try {
// 如果缓冲区已满,则等待
while (buffer.size() == BUFFER_SIZE) {
notFull.await();
}
// 向缓冲区添加元素
buffer.add(value);
System.out.println("Produced: " + value);
// 唤醒可能正在等待的消费者
notEmpty.signal();
} finally {
lock.unlock();
}
}
public int get() throws InterruptedException {
lock.lock();
try {
// 如果缓冲区为空,则等待
while (buffer.isEmpty()) {
notEmpty.await();
}
// 从缓冲区移除元素
int value = buffer.removeFirst();
System.out.println("Consumed: " + value);
// 唤醒可能正在等待的生产者
notFull.signal();
return value;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
final ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
example.put(i);
Thread.sleep(100); // 模拟生产时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
Thread consumerThread = new Thread(() -> {
for (int i = 0; i < 20; i++) {
try {
example.get();
Thread.sleep(200); // 模拟消费时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
producerThread.start();
consumerThread.start();
try {
producerThread.join();
consumerThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
代码解释:
- ReentrantLock 和 Condition :我们使用
ReentrantLock
来保护对共享资源(即缓冲区)的访问,并且创建了两个条件变量notFull
和notEmpty
。 - put() 方法 :生产者调用此方法向缓冲区添加数据。如果缓冲区已满,则生产者将调用
notFull.await()
等待直到有空间可用。一旦添加了数据,就通过notEmpty.signal()
通知等待中的消费者。 - get() 方法 :消费者调用此方法从缓冲区获取数据。如果缓冲区为空,则消费者将调用
notEmpty.await()
等待直到有数据可用。一旦取出了数据,就通过notFull.signal()
通知等待中的生产者。 - 主线程:启动生产者和消费者线程,并等待它们完成。
8 性能
ReentrantLock
在某些情况下比 synchronized
更高效,尤其是在高度竞争的情况下。这是因为 ReentrantLock
可以使用自旋而不是完全阻塞来等待锁,这样可以减少上下文切换的成本。
9 使用场景
- 当你需要比
synchronized
更细粒度的控制时,例如尝试获取锁、响应中断或使用多个条件变量。 - 当你需要实现公平锁时。
- 当你希望在性能上有所提升,并且能够处理高级并发模式时。
示例代码:
java
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
// 获取锁
lock.lock();
try {
// 执行需要同步的操作
System.out.println("Thread " + Thread.currentThread().getName() + " is doing something.");
} finally {
// 确保锁最终会被释放
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
// 创建两个线程来调用 doSomething 方法
Thread t1 = new Thread(() -> example.doSomething(), "Thread-1");
Thread t2 = new Thread(() -> example.doSomething(), "Thread-2");
t1.start();
t2.start();
}
}
在这个例子中,doSomething
方法使用 ReentrantLock
来确保每次只有一个线程可以执行其中的代码。注意在 finally
块中释放锁,以防止因异常导致锁未被释放的情ss