重入锁ReentrantLock详解
目录
ReentrantLock简介
可重入性
ReentrantLock的特性
中断响应
公平锁与非公平锁
非阻塞获取锁
Condition类
与synchronized的比较
总结
参考
ReentrantLock简介
重入锁ReentrantLock是Java并发包中提供的一种可重入锁,它相较于Java的synchronized关键字具有更多的功能和更细粒度的控制。以下是关于ReentrantLock的详细介绍:
可重入性
可重入性是指同一个线程在没有释放锁的情况下,可以多次获得同一个锁。可重入性对于递归调用非常有用,因为它允许线程在持有锁的情况下再次获取该锁,而不会导致死锁。synchronized和ReentrantLock都是可重入的。
ReentrantLock的特性
中断响应
使用关键字synchronized若产生了死锁,那么陷入死锁的线程将一直等待,而ReentrantLock提供了一种中断响应机制,使用ReentrantLock的lockInterruptibly()方法获取锁线程可以响应中断,如果获取锁时,或者持有锁线程陷入阻塞等待,若此时线程被中断,将会抛出InterruptedException异常,中断机制对处理死锁有一定帮助,也为并发编程提供了更多的灵活性,请看下面示例:
import java.util.concurrent.locks.ReentrantLock;public class RLock {public static void main(String[] args) {ReentrantLock lock1 = new ReentrantLock();ReentrantLock lock2 = new ReentrantLock();Thread thread1 = new Thread(() -> {try {lock1.lockInterruptibly();System.out.println("线程1获取到锁1");Thread.sleep(2000);lock2.lockInterruptibly();System.out.println("线程1获取到锁2,线程1任务完成");} catch (InterruptedException e) {System.out.println("线程1被中断,放弃任务");} finally {//判断当前线程是否保持此锁定if (lock1.isHeldByCurrentThread()) lock1.unlock();if (lock2.isHeldByCurrentThread())lock2.unlock();}});Thread thread2 = new Thread(() -> {try {lock2.lockInterruptibly();System.out.println("线程2获取到锁2");Thread.sleep(2000);lock1.lockInterruptibly();System.out.println("线程2获取到锁1,线程2任务完成");} catch (InterruptedException e) {System.out.println("线程2被中断,放弃任务");} finally {if (lock1.isHeldByCurrentThread()) lock1.unlock();if (lock2.isHeldByCurrentThread())lock2.unlock();}});thread1.start();thread2.start();try {Thread.sleep(3000);thread1.interrupt();} catch (InterruptedException e) {e.printStackTrace();}}
}
//输出
线程1获取到锁1
线程2获取到锁2
线程1被中断,放弃任务
线程2获取到锁1,线程2任务完成
上面示例代码中,利用两个线程交替获取两个锁产生了死锁,若没有其它措施,这两个线程将一直保持阻塞状态,得益于中断响应机制,我们在3秒后向线程1发送中断请求,线程1退出阻塞状态并释放锁,线程2就可以获取到全部锁完成任务,而线程1放弃了任务,释放资源,通过中断机制避免了死锁。
公平锁与非公平锁
-
公平锁指的是锁的分配机制是公平的,通常先对锁提出获取请求的线程会先被分配到锁。
-
非公平锁是允许插队的锁,即后来请求的线程可能先获得锁。非公平锁可以减少线程切换和上下文切换的开销,因此性能通常比公平锁高。
synchronized是非公平锁,ReentrantLock默认情况下也是非公平锁,但是在构造函数中提供了公平锁的初始化方式。
非阻塞获取锁
ReentrantLock提供了tryLock()方法,该方法尝试获取锁,如果锁被其他线程占用,则立即返回false,而不是阻塞当前线程。这可以用于实现一些非阻塞的并发控制逻辑。tryLock()方法还能设置超时时间,即尝试等待一段时间,如果在这段时间内锁没有被获取到,则线程将不再等待,直接返回false。
Condition类
如果你知道wait()方法和notify()方法,那么理解Condition就很容易了。它与wait()方法和notify()方法的作用是大致相同的。但是wait()方法和notify()方法是与synchronized关键字合作使用的,而Condition是与ReentrantLock配合使用的。其主要方法如下:
-
await()方法会使当前线程等待,同时释放当前锁,与wait()方法相似。
-
awaitUninterruptibly()方法与await()方法基本相同,但是它不响应中断。
-
singal()方法用于唤醒一个在等待中的线程,与notify()方法类似,singalAll()方法与notifyAll()方法类似。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class RLock {public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();Condition condition = lock.newCondition();Thread thread = new Thread(() -> {try {lock.lock();condition.await();System.out.println("线程执行完成");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}});thread.start();Thread.sleep(3000);lock.lock();condition.signal();lock.unlock();}
}
上面例子中启动了一个子线程,调用await()方法让子线程陷入阻塞,主线程在3秒钟后通过signal()方法将其唤醒,子线程得以从阻塞状态退出,完成接下来的任务。需要注意的是执行await()方法和signal()方法都需要获取锁,这点也跟wait()方法和notify()方法一样。
与synchronized的比较
与synchronized相比,ReentrantLock提供了更多的功能和灵活性,例如支持公平锁、可中断的锁获取、尝试非阻塞地获取锁等,ReentrantLock可以实现更细粒度的锁控制和更复杂的同步控制逻辑。在性能方面在Java6之前synchronized是重量级锁,其性能会比较差,Java6引入了锁升级策略,提高了synchronized的并发性能,与ReentrantLock的性能相差不大。使用方面synchronized更简单方便,ReentrantLock需要显示地获取锁和释放锁,且获取锁和释放锁的次数要相等,所以一般要在finally块中释放锁,以确保锁被正确释放。
总结
ReentrantLock是Java并发包中提供的一种功能强大、使用灵活的可重入锁。它支持公平锁和非公平锁、显式锁控制、尝试非阻塞地获取锁、支持超时尝试获取锁、支持中断以及条件变量等特性。在需要更细粒度的锁控制或避免synchronized关键字的限制时,ReentrantLock是一个很好的选择。然而,在使用时需要注意确保在finally块中释放锁,以避免死锁的发生。
参考
《Java高并发程序设计》