在Java的并发编程库中,
ReentrantLock
是一种非常重要的同步工具,它提供了一种比内置synchronized关键字更加灵活和可定制的锁定机制。在本文中,我们将详细讨论ReentrantLock
的工作原理、特性以及如何使用它来解决多线程并发问题。
ReentrantLock
,也被称为“可重入锁”,是一个同步工具类,在java.util.concurrent.locks包下。这种锁的一个重要特点是,它允许一个线程多次获取同一个锁而不会产生死锁。这与synchronized
关键字提供的锁定机制非常相似,但ReentrantLock
提供了更高的扩展性。
ReentrantLock
的一个主要特点是它的名字所表示的含义——“可重入”。简单来说,如果一个线程已经持有了某个锁,那么它可以再次调用lock()方法而不会被阻塞。这在某些需要递归锁定的场景中非常有用。锁的持有计数会在每次成功调用lock()方法时递增,并在每次unlock()方法被调用时递减。synchronized
关键字不同,ReentrantLock
提供了一个公平锁的选项。公平锁会按照线程请求锁的顺序来分配锁,而不是像非公平锁那样允许线程抢占已经等待的线程的锁。公平锁可以减少“饥饿”的情况,但也可能降低一些性能。ReentrantLock
的获取锁操作(lockInterruptibly()
方法)可以被中断。这提供了另一个相对于synchronized
关键字的优势,因为synchronized
不支持响应中断。ReentrantLock
类中还包含一个Condition
接口的实现,该接口允许线程在某些条件下等待或唤醒。这提供了一种比使用wait()
和notify()
更灵活和更安全的线程通信方式。ReentrantLock与synchronized都是Java中用于多线程同步的机制,但它们在使用方式、功能和灵活性上有一些不同。
总的来说,ReentrantLock提供了比synchronized更灵活、更强大的锁机制,但使用起来也更复杂,需要更谨慎地处理锁的获取和释放。synchronized虽然功能相对简单,但在很多情况下已经足够使用,并且由于是内建关键字,使用起来也更方便。
用
下面代码模拟了一个账户转账的场景,展示了ReentrantLock
如何保证多线程下的数据安全性。
import java.util.concurrent.locks.ReentrantLock;
public class Account {
// 账户余额
private int balance;
// 锁对象
private final ReentrantLock lock = new ReentrantLock();
public Account(int balance) {
this.balance = balance;
}
// 存钱
public void deposit(int amount) {
lock.lock(); // 获取锁
try {
balance += amount;
System.out.println("存入金额: " + amount + ",当前余额: " + balance);
} finally {
lock.unlock(); // 释放锁
}
}
// 取钱
public void withdraw(int amount) {
lock.lock(); // 获取锁
try {
if (balance >= amount) {
balance -= amount;
System.out.println("取出金额: " + amount + ",当前余额: " + balance);
} else {
System.out.println("余额不足,取款失败!");
}
} finally {
lock.unlock(); // 释放锁
}
}
// 获取账户余额
public int getBalance() {
return balance;
}
// 主函数,模拟多线程下的转账操作
public static void main(String[] args) {
final Account account = new Account(1000);
// 启动一个线程进行存钱操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.deposit(100);
try {
Thread.sleep(100); // 模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 启动一个线程进行取钱操作
new Thread(() -> {
for (int i = 0; i < 5; i++) {
account.withdraw(50);
try {
Thread.sleep(100); // 模拟延时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
Account
类表示一个账户,包含了存钱(deposit
)和取钱(withdraw
)两个操作。为了保证账户余额在多线程环境下的数据安全性,我们在这两个方法中使用了ReentrantLock
来确保同时只有一个线程能够修改账户余额。
deposit
方法中,先获取锁,然后进行余额的增加操作,最后释放锁。同样地,在withdraw
方法中也是先获取锁,然后判断余额是否足够,足够则进行扣款操作,否则输出提示信息,最后释放锁。
在main
方法中,创建了一个账户对象,并启动了两个线程,一个进行多次存钱操作,另一个进行多次取钱操作。由于我们使用了ReentrantLock
来保证同步,因此即使在多线程环境下,账户的余额也不会出现不一致的情况。
ReentrantLock
的实现依赖于内部的Sync
类,这个类是AbstractQueuedSynchronizer
(AQS)的一个实现。AQS是Java并发库中许多同步工具(包括Semaphore
、CountDownLatch
和CyclicBarrier
等)的核心。
AQS使用一个int类型的变量来表示同步状态,ReentrantLock
用它来表示锁的持有计数和持有线程的信息。当计数为0时,表示锁未被任何线程持有。当一个线程首次成功获取锁时,JVM会记录这个锁的持有线程,并将计数器设置为1。如果同一个线程再次请求这个锁,它将能够再次获得这个锁,并且计数器会递增。当线程释放锁时(通过调用unlock()方法),计数器会递减。如果计数器递减为0,则表示锁已经完全释放,其他等待的线程有机会获取它。
此外,AQS还维护了一个队列,用于管理那些等待锁的线程。这个队列遵循FIFO原则,但也可以通过设置为公平锁来严格按照线程请求锁的顺序来排队。
首先,ReentrantLock
的核心实现是基于AbstractQueuedSynchronizer
(AQS),它是一个用于构建锁和同步器的框架。ReentrantLock
内部有一个静态内部类Sync
,它继承了AQS并实现了所需的同步状态管理。
public class ReentrantLock implements Lock, java.io.Serializable {
// 默认使用非公平锁
private final Sync nonfairSync;
// 公平锁
private final Sync fairSync;
// 抽象队列同步器,实际是nonfairSync或fairSync
private final Sync sync;
// 构造函数,默认非公平锁
public ReentrantLock() {
sync = nonfairSync = new NonfairSync();
}
// 构造函数,可指定公平性
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 实现Lock接口的lock方法
public void lock() {
sync.lock();
}
// ... 其他方法,如tryLock, unlock等
// 抽象队列同步器的实现
abstract static class Sync extends AbstractQueuedSynchronizer {
// ...
// 是否处于占用状态
final boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取锁
final boolean tryAcquire(int acquires) {
// ... 省略具体实现
}
// 释放锁
protected final boolean tryRelease(int releases) {
// ... 省略具体实现
}
// ... 其他方法
}
// 非公平锁实现
static final class NonfairSync extends Sync {
// ...
// 锁获取
final void lock() {
// ... 省略具体实现
}
// ... 其他方法
}
// 公平锁实现
static final class FairSync extends Sync {
// ...
// 锁获取,考虑公平性
final void lock() {
// ... 省略具体实现
}
// ... 其他方法
}
}
上面的代码只是ReentrantLock
的一个简版框架,通过这个框架,我们可以理解ReentrantLock
的基本结构和关键组成部分:
ReentrantLock
使用AQS的同步状态来管理锁的持有情况。当状态为0时,表示锁未被任何线程持有;当状态为1时,表示锁被某个线程持有。对于可重入锁,每次重入都会增加状态值,每次释放都会减少状态值。但这里简化的表示只用状态值1来表示锁被持有,实际实现中会有更复杂的状态管理。
NonfairSync
和FairSync
中,lock()
方法实现了锁的获取逻辑。非公平锁在尝试获取锁时不会考虑队列中的等待线程,而公平锁则会严格按照FIFO原则来处理等待线程。这些方法最终会调用AQS的acquire()
方法,该方法会处理同步状态的变更、线程的阻塞和唤醒等。
unlock()
方法实现,最终会调用AQS的release()
方法。这个方法会负责减少同步状态、唤醒等待线程等。在释放锁之前,必须确保当前线程是锁的持有者。
ReentrantLock
还提供了newCondition()
方法,用于创建条件变量。这些条件变量可以用于实现更复杂的线程同步和通信逻辑。条件变量的实现也是基于AQS的。
ReentrantLock
的实现主要依赖于AQS框架,通过扩展AQS并实现特定的同步状态管理逻辑来实现可重入锁的功能。它提供了比synchronized
关键字更灵活和可定制的同步机制,包括公平性选择、可中断的锁获取操作以及条件变量等。在使用ReentrantLock
时,需要注意正确管理锁的获取和释放,以避免死锁和性能问题。
finally
块中释放锁:为了确保锁能够在所有情况下都被正确释放(包括在可能抛出异常的代码中),你应该总是在finally
块中调用unlock()方法。
Condition
接口提供了一种灵活的线程通信方式,但如果不当使用,也可能导致死锁或活锁等问题。你应该确保在使用条件变量时始终遵循正确的模式(如在调用await()
方法之前检查条件,并在修改条件之后调用signal()
或signalAll()
方法)。
synchronized
关键字相比,ReentrantLock
在某些情况下可能提供更好的性能。但是,这也意味着你需要更小心地管理锁的获取和释放,以及处理可能出现的竞争和死锁问题。此外,过度使用锁(无论是synchronized
还是ReentrantLock
)都可能导致性能下降和可伸缩性问题。因此,在设计并发程序时,应该尽量使用无锁或低锁竞争的数据结构和算法。
ReentrantLock
是 Java 提供的一种可重入的互斥锁,它具有与 synchronized
关键字类似的同步和锁定能力,但比 synchronized
更灵活。ReentrantLock
支持中断获取锁、尝试获取锁(限时/非限时)和可轮询的获取锁等特性,适用于需要更高级锁定控制的场景。