Java ReentrantLock:深入理解与高效应用
简介
在多线程编程中,确保共享资源的安全访问至关重要。Java 提供了多种同步机制,ReentrantLock
便是其中强大的一种。ReentrantLock
实现了 Lock
接口,相比传统的 synchronized
关键字,它提供了更灵活、更强大的同步控制能力。本文将详细介绍 ReentrantLock
的基础概念、使用方法、常见实践以及最佳实践,帮助你在多线程编程中更好地运用它。
目录
- 基础概念
- 使用方法
- 简单锁定与解锁
- 尝试锁定
- 公平性选择
- 常见实践
- 资源竞争控制
- 线程间通信
- 最佳实践
- 合理使用锁的粒度
- 避免死锁
- 小结
- 参考资料
基础概念
ReentrantLock
是一个可重入的互斥锁,这意味着同一个线程可以多次获取该锁。每次获取锁时,锁的持有计数会增加,而每次释放锁时,持有计数会减少。当持有计数为 0 时,锁被完全释放,其他线程可以获取。
与 synchronized
不同,ReentrantLock
提供了更灵活的锁获取和释放方式,并且支持公平性选择。公平锁会按照线程请求的顺序来分配锁,而非公平锁则允许线程在锁可用时立即尝试获取,这通常会带来更高的性能,但可能导致某些线程长时间等待。
使用方法
简单锁定与解锁
import java.util.concurrent.locks.ReentrantLock;
public class SimpleReentrantLockExample {
private static ReentrantLock lock = new ReentrantLock();
private static int sharedResource = 0;
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 1000; i++) {
sharedResource++;
}
} finally {
lock.unlock();
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
for (int i = 0; i < 1000; i++) {
sharedResource--;
}
} finally {
lock.unlock();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Shared resource value: " + sharedResource);
}
}
在这个示例中,我们创建了一个 ReentrantLock
实例,并在两个线程中使用它来保护共享资源 sharedResource
。通过 lock.lock()
获取锁,在 try
块中访问共享资源,最后在 finally
块中使用 lock.unlock()
释放锁,以确保无论是否发生异常,锁都会被正确释放。
尝试锁定
tryLock()
方法尝试获取锁,如果锁可用,则立即返回 true
,否则返回 false
,不会使线程阻塞。
import java.util.concurrent.locks.ReentrantLock;
public class TryLockExample {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
if (lock.tryLock()) {
try {
System.out.println("Thread 1 acquired the lock.");
// 访问共享资源
} finally {
lock.unlock();
}
} else {
System.out.println("Thread 1 could not acquire the lock.");
}
});
Thread thread2 = new Thread(() -> {
lock.lock();
try {
System.out.println("Thread 2 acquired the lock.");
// 长时间持有锁
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
lock.unlock();
}
});
thread2.start();
thread1.start();
}
}
在这个示例中,thread1
使用 tryLock()
尝试获取锁,如果无法获取,它不会等待,而是继续执行其他任务。
公平性选择
创建 ReentrantLock
时,可以通过构造函数指定是否为公平锁。
import java.util.concurrent.locks.ReentrantLock;
public class FairnessExample {
// 创建公平锁
private static ReentrantLock fairLock = new ReentrantLock(true);
// 创建非公平锁
private static ReentrantLock nonFairLock = new ReentrantLock(false);
public static void main(String[] args) {
// 使用公平锁和非公平锁的线程示例
}
}
默认情况下,ReentrantLock
创建的是非公平锁,因为非公平锁在大多数情况下性能更好。但在某些场景下,公平锁能确保线程按照请求顺序获取锁,避免线程饥饿。
常见实践
资源竞争控制
在多线程环境中,多个线程可能同时访问和修改共享资源,这会导致数据不一致问题。ReentrantLock
可以有效控制资源竞争。
import java.util.concurrent.locks.ReentrantLock;
public class ResourceContentionExample {
private static ReentrantLock lock = new ReentrantLock();
private static int sharedCounter = 0;
public static void incrementCounter() {
lock.lock();
try {
sharedCounter++;
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
incrementCounter();
}
});
threads[i].start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final counter value: " + sharedCounter);
}
}
在这个示例中,多个线程调用 incrementCounter()
方法,通过 ReentrantLock
确保 sharedCounter
的操作是线程安全的。
线程间通信
ReentrantLock
可以结合 Condition
实现线程间的通信。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadCommunicationExample {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static boolean flag = false;
public static void main(String[] args) {
Thread producer = new Thread(() -> {
lock.lock();
try {
while (flag) {
condition.await();
}
// 生产数据
flag = true;
System.out.println("Producer produced data.");
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread consumer = new Thread(() -> {
lock.lock();
try {
while (!flag) {
condition.await();
}
// 消费数据
flag = false;
System.out.println("Consumer consumed data.");
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
producer.start();
consumer.start();
}
}
在这个示例中,producer
线程和 consumer
线程通过 ReentrantLock
和 Condition
进行通信,实现了数据的生产和消费同步。
最佳实践
合理使用锁的粒度
锁的粒度指的是锁保护的代码范围大小。过大的锁粒度会导致线程竞争激烈,降低并发性能;过小的锁粒度则可能增加锁的获取和释放开销。应根据具体业务场景,合理确定锁的粒度。
避免死锁
死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放锁时,就会发生死锁。为避免死锁,应遵循以下原则:
- 尽量减少锁的嵌套使用。
- 确保锁的获取顺序一致。
- 使用 tryLock()
方法设置合理的等待时间,避免无限期等待。
小结
ReentrantLock
是 Java 多线程编程中强大的同步工具,它提供了比 synchronized
更灵活、更强大的功能。通过本文的介绍,你了解了 ReentrantLock
的基础概念、使用方法、常见实践以及最佳实践。在实际开发中,应根据具体需求选择合适的同步机制,合理使用 ReentrantLock
,以确保多线程程序的正确性和高效性。
参考资料
- Java 官方文档 - ReentrantLock
- 《Effective Java》 - Joshua Bloch
- 《Java Concurrency in Practice》 - Brian Goetz