ReentrantLock是Java提供的强大且灵活的可重入锁,支持公平和非公平特性。在业务场景中,如在线售票系统,可用它解决多线程并发访问共享资源问题。通过互斥访问确保数据一致性和避免超卖,只有获锁的线程可执行出票操作。ReentrantLock的可重入性避免死锁,增强了系统的稳定性。
ReentrantLock是Java提供的一个可重入锁,并且支持在构造函数中指定公平或者非公平的特性,它的功能比synchronized关键字更为强大和灵活,在实际的业务场景中,经常用到ReentrantLock来解决多线程并发访问共享资源的问题。
假设有一个在线售票系统,这个系统允许多个用户同时购买不同场次的电影票,为了保证数据的一致性和避免超卖的情况,每次用户购票时,系统都需要先检查剩余的票数是否足够,如果足够则进行出票操作,并相应地减少库存。
在这里,可以使用ReentrantLock来实现对共享资源(即剩余的票数)的互斥访问,具体的做法是,在出票操作开始前,先尝试获取锁,如果获取成功,则执行出票操作,并在操作完成后释放锁;如果获取失败(说明有其他线程正在执行出票操作),则当前线程需要等待,直到获取到锁为止*(注意:这场景并不适合在分布式情况下,分布式情况下需要使用分布式锁来实现)*。
这样做的好处是,可以确保同一时间只有一个线程在执行出票操作,从而避免了多线程并发访问导致的数据不一致和超卖问题,同时,由于ReentrantLock支持可重入性,如果一个线程已经获取了锁,那么它可以再次获取同一个锁而不会发生死锁,这个特性非常有用。
下面是使用ReentrantLock实现的简单的“在线售票系统”Java代码示例,在这个示例中,TicketSeller类封装了售票的逻辑,使用一个ReentrantLock来保证线程安全,ClientThread类模拟了多个客户端同时购票的场景,如下代码:
import java.util.concurrent.locks.ReentrantLock;
// 售票系统类
public class TicketSeller {
// 假设总共有100张票
private int tickets = 100;
// 创建一个可重入锁
private final ReentrantLock lock = new ReentrantLock();
// 售票方法
public void sellTicket() {
lock.lock(); // 加锁
try {
// 检查是否有票可售
if (tickets > 0) {
System.out.println(Thread.currentThread().getName() + " 售出1张票,剩余票数:" + (--tickets));
} else {
System.out.println("票已售完!");
}
} finally {
lock.unlock(); // 释放锁
}
}
}
// 客户端线程类
public class ClientThread extends Thread {
private TicketSeller seller;
public ClientThread(String name, TicketSeller seller) {
super(name);
this.seller = seller;
}
@Override
public void run() {
// 模拟客户端连续购票
for (int i = 0; i < 5; i++) {
seller.sellTicket();
try {
// 每次购票后休眠一段时间,模拟真实场景中的时间间隔
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 主类,用于启动售票系统和客户端线程
public class TicketSystemDemo {
public static void main(String[] args) {
// 创建一个售票系统实例
TicketSeller seller = new TicketSeller();
// 创建多个客户端线程并启动
for (int i = 1; i <= 5; i++) {
new ClientThread("客户端" + i, seller).start();
}
}
}
在上面代码中,先定义了一个TicketSeller类,它包含了售票的业务逻辑,方法返回的是正等待获取此锁的线程数,且使用了ReentrantLock来保证在多线程环境下,对共享资源(剩余的票数)的访问是线程安全的。
Modifier and Type | Method | Description |
---|---|---|
int | getHoldCount() | 返回当前线程对此锁的持有计数,也就是说当前线程已经获取了这个锁多少次。 |
protected Thread | getOwner() | 返回当前持有该锁的线程Thread对象 |
protected Collection | getQueuedThreads() | 返回正等待获取此锁的线程集合。 |
int | getQueueLength() | 返回正等待获取此锁的线程数。 |
protected Collection | getWaitingThreads(Condition condition) | 返回与此锁关联的给定条件上等待的线程的集合。 |
int | getWaitQueueLength(Condition condition) | 返回与此锁关联的给定条件上等待的线程数。 |
boolean | hasQueuedThread(Thread thread) | 返回给定线程是否正在等待获取此锁。 |
boolean | hasQueuedThreads() | 返回是否有线程正在等待获取此锁。 |
boolean | hasWaiters(Condition condition) | 判断是否有线程正在等待获取该锁。 |
boolean | isFair() | 返回是否是公平锁。 |
boolean | isHeldByCurrentThread() | 返回线程自己当前是否持有这个锁。 |
boolean | isLocked() | 返回此锁是否被任何线程持有。 |
void | lock() | Acquires the lock. |
void | lockInterruptibly() | Acquires the lock unless the current thread is interrupted. |
Condition | newCondition() | Returns a Condition instance for use with this Lock instance. |
String | toString() | Returns a string identifying this lock, as well as its lock state. |
boolean | tryLock() | Acquires the lock only if it is not held by another thread at the time of invocation. |
boolean | tryLock(long timeout, TimeUnit unit) | Acquires the lock if it is not held by another thread within the given waiting time and the current thread has not been interrupted. |
void | unlock() | Attempts to release this lock. |
getHoldCount()方法返回当前线程对此锁的持有计数,也就是当前线程重复获取此锁的次数。
假设一个场景:在一个多线程环境中,有一个资源(比如一个文件)需要被多个方法共享访问,但为了保证数据的一致性和完整性,需要在每个方法内部对此资源进行加锁,比如:线程A在执行某个方法时首次获取了锁,并在方法内部调用了另一个也需要访问该资源的方法,由于锁是可重入的,线程A可以再次获取同一个锁而不会阻塞,这时,如果想知道线程A到底获取了多少次这个锁,就可以使用getHoldCount()方法。
下面代码案例,展示了如何使用ReentrantLock和getHoldCount()方法,如下代码:
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
// 创建一个可重入锁实例
private final ReentrantLock lock = new ReentrantLock();
// 计数器资源
private int counter = 0;
// 增加计数器的方法
public void increment() {
lock.lock(); // 加锁
try {
counter++;
System.out.println(Thread.currentThread().getName() + " incremented counter to " + counter);
// 打印当前线程对此锁的持有计数
System.out.println(Thread.currentThread().getName() + " hold count: " + lock.getHoldCount());
} finally {
lock.unlock(); // 释放锁
}
}
// 客户端调用示例
public static void main(String[] args) {
ReentrantLockDemo demo = new ReentrantLockDemo();
// 创建并启动两个线程来访问共享资源
Thread thread1 = new Thread(() -> {
demo.increment();
demo.increment(); // 线程重入锁
}, "Thread-1");
Thread thread2 = new Thread(() -> {
demo.increment();
}, "Thread-2");
// 启动线程
thread1.start();
thread2.start();
}
}
在上面代码汇中,有一个ReentrantLockDemo类,它包含一个ReentrantLock实例和一个计数器,increment()方法用于增加计数器的值,并且在每次调用时都会打印当前线程对锁的持有计数,在main()方法中,创建了两个线程,它们都会调用increment()方法,第一个线程会调用两次increment(),因此它会重入锁一次,第二个线程只调用一次increment()。
如下代码输出结果:
Thread-1 incremented counter to 1
Thread-1 hold count: 1
Thread-1 incremented counter to 2
Thread-1 hold count: 2
Thread-2 incremented counter to 3
Thread-2 hold count: 1
hasQueuedThreads()方法用于检查是否有任何线程正在等待获取此锁。
假设一个场景:当前有一个多线程应用,其中有一个共享资源需要被多个线程安全地访问,为了保护这个资源,可以使用ReentrantLock来确保每次只有一个线程能够访问它,但是,随着项目的不断迭代,应用规模变得越来越复杂,锁的竞争越来越频繁,因此希望在程序运行时获得是否有线程正在等待获取这个锁,以便做出优化和调整,这时,可以使用hasQueuedThreads()实现。
比如,在定时任务中定时通过调用hasQueuedThreads()来检查锁的等待队列,如果该方法返回true,说明有线程正在等待获取锁,这可能意味着共享资源的访问竞争比较激烈,可能需要考虑优化资源的使用方式,或者增加资源的数量来减少等待,反之,如果返回false,则说明当前没有线程在等待,资源的使用可能比较平稳,如下示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class ResourcePool {
// 模拟资源池
private final Object[] resources;
// 用于同步访问的可重入锁
private final ReentrantLock lock = new ReentrantLock();
public ResourcePool(int size) {
resources = new Object[size];
for (int i = 0; i < size; i++) {
resources[i] = new Object();
}
}
// 获取资源的方法
public Object getResource() {
lock.lock(); // 加锁
try {
// 模拟资源获取时的延迟
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回资源(这里仅作示例,实际上应该是返回资源池中的一个资源)
return new Object();
} finally {
lock.unlock(); // 释放锁
}
}
// 检查是否有线程在等待获取锁,并输出信息
public void checkIfThreadsWaiting() {
if (lock.hasQueuedThreads()) {
System.out.println("有线程正在等待获取锁!");
} else {
System.out.println("当前没有线程在等待获取锁。");
}
}
// 客户端调用示例
public static void main(String[] args) {
ResourcePool pool = new ResourcePool(5);
// 创建并启动多个线程来访问资源池
for (int i = 0; i < 10; i++) {
new Thread(() -> {
pool.getResource(); // 请求资源
System.out.println(Thread.currentThread().getName() + " 获取到资源。");
}).start();
}
// 让主线程稍微等待一下,以便其他线程有机会开始等待锁
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查是否有线程在等待获取锁
pool.checkIfThreadsWaiting();
}
}
ReentrantLock 类中的 hasWaiters() 方法主要用于检查是否有线程正在等待获取此锁。
举一个场景:当前正在开发一个多线程的在线书店应用,其中有一个关键的资源是库存量。每当有顾客下单购买书籍时,应用需要更新库存量,为了防止多个线程同时更新库存导致数据不一致,使用了 ReentrantLock 来确保一次只有一个线程可以更新库存。
但是在某些情况下,可能想知道是否有线程正在等待更新库存,例如,如果系统管理员想要监控应用的健康状况,了解是否有过多的线程在等待锁这是一个非常重要的监控指标,过多的等待线程可能意味着系统遇到了瓶颈,需要进行优化。
这时,hasWaiters() 方法就派上用场了,以通过查询这个方法来了解当前是否有线程在等待获取库存锁,如果 hasWaiters() 返回 true,那么就知道有线程在等待,可能需要进一步调查原因,比如检查是否有死锁情况发生,或者考虑增加资源以减少等待时间。如果返回 false,则说明目前没有线程在等待,系统可能运行得相对顺畅。
isLocked() 方法用于检查此锁是否被任何线程持有。
当前正在开发一个多线程的库存管理系统,在这个系统中,有一个共享资源,比如一个仓库,多个线程可能同时尝试访问或修改这个仓库的状态,在这种情况下,isLocked() 方法就非常有用了,可以调用这个方法来检查锁是否被持有,如果 isLocked() 返回 true,就知道有线程正在修改仓库,这时可以选择等待、尝试获取锁或者执行其他适当的操作,如果返回 false,就知道当前没有线程持有该锁,这时可以安全地执行操作。
ReentrantLock 是一个可重入互斥锁,它有两个模式:公平锁和非公平锁,以下是两者之间的主要区别:
公平锁(Fair Lock):
非公平锁(Non-Fair Lock):
总结:公平锁和非公平锁之间的选择取决于具体的应用场景和需求,如果希望确保所有线程都能按照请求锁的顺序获得锁,并且能够避免线程饥饿问题,那么公平锁是一个不错的选择,然而,如果,更关心性能,并且能够容忍一定程度的不确定性和可能的线程饥饿问题,那么非公平锁可能是一个更好的选择。
import java.util.concurrent.locks.ReentrantLock;
public class NonFairLockExample {
// 创建一个非公平的ReentrantLock
private final ReentrantLock lock = new ReentrantLock(false); // false表示非公平锁
public void doSomething() {
lock.lock(); // 获取锁
try {
// 模拟一些工作
System.out.println(Thread.currentThread().getName() + " is doing something.");
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
NonFairLockExample example = new NonFairLockExample();
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> example.doSomething()).start();
}
}
}
NonFairLockExample 类中有一个 ReentrantLock 的实例 lock,在构造它时传递了 false 作为参数,表示这是一个非公平锁。在 main 方法中,创建了 NonFairLockExample 的实例,并启动了5个线程来调用 doSomething 方法,由于这是一个非公平锁,所以线程获取锁的顺序是不确定的,每次运行这个程序时,输出中线程的顺序都可能会不同,如下输出示例(顺序可能会变化):
Thread-0 is doing something.
Thread-3 is doing something.
Thread-2 is doing something.
Thread-4 is doing something.
Thread-1 is doing something.
import java.util.concurrent.locks.ReentrantLock;
public class FairLockExample {
// 创建一个公平的ReentrantLock
private final ReentrantLock lock = new ReentrantLock(true); // true表示公平锁
public void doSomething() {
lock.lock(); // 获取锁
try {
// 模拟一些工作
System.out.println(Thread.currentThread().getName() + " is doing something.");
Thread.sleep(1000); // 让线程暂停一段时间,以便观察锁的行为
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) {
FairLockExample example = new FairLockExample();
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
final int threadNum = i;
new Thread(() -> {
// 为了使输出更有序,让线程在开始之前稍微等待一下
try {
Thread.sleep(100 * threadNum);
} catch (InterruptedException e) {
e.printStackTrace();
}
example.doSomething();
}, "Thread-" + i).start();
}
}
}
FairLockExample 类中有一个 ReentrantLock 的实例 lock,在构造它时传递了 true 作为参数,表示这是一个公平,在 main 方法中,创建了 FairLockExample 的实例,并启动了5个线程来调用 doSomething 方法。
当运行这个程序时,由于使用的是公平锁,因此线程将大致按照它们尝试获取锁的顺序来执行。如下输出。然而,即使使用了公平锁,也不能保证线程执行的绝对顺序,因为线程调度仍然受到操作系统和JVM的影响。
输出示例(大致按照线程启动顺序):
Thread-0 is doing something.
Thread-1 is doing something.
Thread-2 is doing something.
Thread-3 is doing something.
Thread-4 is doing something.