深入探索ReentrantLock(一):入门与实战应用Java并发编程中,ReentrantLock作为可重入互斥锁,
前言
Java并发编程中,ReentrantLock
作为可重入互斥锁,提供了比synchronized
更灵活的控制能力,包括非阻塞锁获取、中断响应及公平锁机制。本文深入探讨ReentrantLock
的特性、常用方法及案例应用,助力开发者构建高效稳定的并发应用。
一、ReentrantLock特性、方法及案例
在Java并发编程中,ReentrantLock 是一个可重入的互斥锁,它提供了比 synchronized 关键字更加灵活和强大的锁机制。虽然 synchronized 是Java语言内置的同步机制,易于使用且自动管理锁,但 ReentrantLock 提供了更高的灵活性和控制力,特别是在需要尝试非阻塞地获取锁、响应中断,或者需要公平锁等场景下。
尽管在JDK 6.0及以后的版本中,synchronized 的性能得到了显著提升,使得它与 ReentrantLock 在许多场景下的性能差距变得不那么明显,但 ReentrantLock 提供的额外功能(如尝试非阻塞加锁、可中断的锁获取、超时锁等)仍然使其在许多复杂并发控制场景中成为首选。
1.ReentrantLock重入锁的特性
在并发编程的广阔领域中,锁机制是确保数据一致性和线程安全性的基石。随着多线程应用的日益普及,对锁的性能、灵活性和公平性的需求也日益增长。传统的synchronized关键字虽然简单易用,但在某些复杂场景下,其局限性逐渐显现。正是在这样的背景下,ReentrantLock作为java.util.concurrent.locks包中的核心类,以其独特的核心特性,成为了并发控制中的一把利器,其核心特性如下:
- 可重入性:ReentrantLock 允许同一个线程多次获得锁,每次获得锁后,必须释放相同次数的锁,以避免死锁。这种特性使得它非常适合递归调用的场景。
- 灵活性:与 synchronized 相比,ReentrantLock 提供了更多的控制选项,如尝试获取锁(tryLock)、可中断的锁获取(lockInterruptibly)、尝试在给定时间内获取锁(tryLock(long time, TimeUnit unit))等。
- 公平性:ReentrantLock 支持公平锁和非公平锁两种模式。公平锁会按照请求锁的顺序来获取锁,而非公平锁则不保证顺序,这可能导致饥饿现象(某些线程长时间得不到执行机会),但通常非公平锁的效率更高。
2.常用方法
ReentrantLock作为并发编程领域中的核心锁实现,其卓越的设计精髓体现在一系列精心策划与实现的常用API接口上。这些接口不仅功能全面且强大,各自在锁管理机制中扮演着不可或缺的关键角色,共同构建了一个既复杂又高效的锁控制架构。对于开发者而言,若能熟练掌握这些接口,包括基础的锁申请(lock())、支持中断响应的锁申请(lockInterruptibly())、非阻塞式的尝试锁获取(tryLock())、带有时限的锁获取尝试(tryLock(long time, TimeUnit unit))、当前线程锁持有状态的检查(isHeldByCurrentThread())以及锁的显式释放(unlock()),将能够在并发编程中实现对线程同步行为的精细调控,进而开发出既高效运行又稳定可靠的并发应用程序。以下是ReentrantLock提供的一些常用方法:
- void lock() :基本的获取锁的方式,如果锁已被其他线程占用,则当前线程将阻塞,直到锁被释放。
- void lockInterruptibly() :与 lock() 类似,但这个方法允许在等待锁的过程中响应中断,如果当前线程在等待锁的过程中被中断,则会抛出 InterruptedException,并释放已经获得的锁(如果有的话)。
- boolean tryLock() :尝试非阻塞地获取锁,如果锁可用,则立即返回 true 并获得锁;如果锁已被其他线程占用,则立即返回 false,并不进行等待。
- boolean tryLock(long time, TimeUnit unit) :尝试在给定的时间内获取锁。如果在指定的时间内成功获得锁,则返回 true;如果超时还未获得锁,则返回 false。此方法提供了更细粒度的等待控制。
- boolean isHeldByCurrentThread() :判断当前线程是否持有这个锁,可以帮助开发者了解锁的持有情况。
- void unlock() :释放锁。每次成功调用 lock() 或 tryLock() 方法后,都必须调用 unlock() 方法来释放锁,否则将导致死锁。
案例:
以下是一个使用ReentrantLock的案例,该案例展示了ReentrantLock的基本用法。
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample implements Runnable { // 创建一个重入锁实例 private static ReentrantLock lock = new ReentrantLock(); // 共享资源 private static int count = 0; @Override public void run() { for (int i = 0; i < 10000; i++) { // 尝试获取锁 lock.lock(); try { // 访问或修改共享资源 count++; } finally { // 确保释放锁 lock.unlock(); } } } public static void main(String[] args) throws InterruptedException { ReentrantLockExample example = new ReentrantLockExample(); // 创建并启动多个线程 Thread thread1 = new Thread(example); Thread thread2 = new Thread(example); thread1.start(); thread2.start(); // 等待所有线程完成 thread1.join(); thread2.join(); // 输出最终结果 System.out.println("Count: " + count); } }
- 重入锁(ReentrantLock):在这个案例中,使用java.util.concurrent.locks.ReentrantLock类来创建一个重入锁实例。重入锁是一种可重入的互斥锁,它允许同一个线程多次获取同一个锁,而不会造成死锁。
- 共享资源:在这个案例中,count是一个共享资源,多个线程会尝试同时访问或修改它。为了防止数据不一致,案例使用重入锁来保护它。
- 加锁与解锁:在访问或修改共享资源之前,通过调用lock.lock()方法来获取锁。在访问或修改完成后,无论是否发生异常,我们都应该在finally块中调用lock.unlock()方法来释放锁,以确保锁一定会被释放,避免死锁。
- 多线程执行:该案例创建了两个线程(thread1和thread2),它们都执行相同的任务,即多次增加count的值。由于使用了重入锁来保护count,因此即使这两个线程同时运行,count的值也会正确地增加,不会出现数据不一致的情况。
- 等待线程完成:在主线程中,使用thread1.join()和thread2.join()来等待这两个线程完成它们的任务。这样做是为了确保在输出count的值时,所有的线程都已经完成了对count的修改。
- 输出结果:最后输出count的值,以验证多线程操作下的结果是否正确。由于使用了重入锁来保护共享资源,因此输出的结果应该是预期的20000(假设每个线程都成功地将count增加了10000次)。
运行结果:
之所以将ReentrantLock称为可重入锁,是因为它允许同一个线程多次获得锁,而不会被自己阻塞。这种特性确保了线程在递归调用或嵌套方法中能够安全地重新获取已经持有的锁,而不会导致死锁。这里的“多次获得锁”指的是同一个线程可以连续多次成功地对ReentrantLock加锁,而每一次加锁操作都需要对应的解锁操作来释放锁。如果某个线程尝试释放锁的次数超过了其实际获得的锁次数,那么根据Java的锁机制,将抛出java.lang.IllegalMonitorStateException异常,因为这违反了锁的基本使用规则。反之,如果释放锁的次数少于实际获得的锁次数,那么该线程仍然持有锁,直到所有锁都被正确释放为止。
案例:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample implements Runnable { private static ReentrantLock lock = new ReentrantLock(); private static int count = 0; @Override public void run() { lock.lock(); try { count++; } finally { lock.unlock(); lock.unlock(); } } public static void main(String[] args) throws InterruptedException { ReentrantLockExample example = new ReentrantLockExample(); Thread thread1 = new Thread(example); thread1.start(); thread1.join(); System.out.println("Count: " + count); } }
运行结果:
总结
在Java并发编程的浩瀚领域中,ReentrantLock以其独特的可重入性与高度灵活性,成为保障多线程环境下数据一致性与线程安全性的关键工具。相较于Java内置的synchronized关键字,ReentrantLock不仅提供了更为丰富的锁控制选项,如尝试非阻塞加锁、响应中断及超时锁获取等,还通过支持公平锁模式,有效缓解了线程饥饿问题。尽管synchronized在JDK 6.0及以后版本中性能得到显著提升,但ReentrantLock凭借其在复杂并发场景下的卓越表现,依然是实现精细线程同步的首选方案。本文主要介绍了ReentrantLock的核心特性及其常用方法,并通过具体案例展示其在实际应用中的功能。
转载自:https://juejin.cn/post/7411043646130962484