Java中的可重入锁(synchronized和ReentrantLock)
可重入锁是一种互斥锁,它具备“可重入”的特性。可重入的意思是在拥有锁的线程尝试再次获取该锁时,其获取锁的请求会成功,并且不会导致线程阻塞。
可重入锁的设计使得同一线程可以多次获得同一把锁,对于递归函数和对同一个资源的多层次锁定非常有用,因为它防止了线程在尝试获得它已经持有的锁时阻塞自己,这在非可重入锁中会导致死锁。
在实际的开发过程中,函数之间的调用关系可能错综复杂,一不小心就可能在多个不同的函数中反复调用 lock(),从而导致线程自身“卡死”。对于希望采用“傻瓜式”编程的我们来说,可重入锁就是用来解决这个问题的。
可重入锁使得同一个线程可以对同一把锁,在不释放的前提下反复获取,而不会导致线程卡死。因此,如果我们使用的是重入锁,那么程序就可以正常工作。唯一需要保证的是使用 unlock() 的次数和使用 lock() 的一样多。
在 Java 中,synchronized 关键字和 ReentrantLock 都是可重入锁,下面我们分别看一下两者的实现示例。
下面是一个使用 ReentrantLock 的示例:
注意,每次调用 lock() 时都需要在 finally 块中匹配一个 unlock() 调用,以确保锁一定会被释放,防止死锁。
可重入锁的实现基于每个锁关联一个请求计数器和一个持有它的线程。当线程请求一个未被持有的锁时,JVM 会记下锁的持有者,并且将请求计数器的值设置为 1。如果同一个线程再次请求这个锁,请求计数器的值会递增。每当持有者线程退出同步块时,JVM 会将请求计数器的值递减。当请求计数器的值达到0时,锁被释放。
以下是可重入锁的几个关键功能:
Java 中的 ReentrantLock 是一个标准的可重入锁的实现,它提供了以上描述的所有功能。ReentrantLock 内部使用 AQS 作为实现同步状态管理的框架,AQS 用一个整数变量来表示锁的持有次数,并使用一个节点队列来表示线程及其等待状态。
可重入性是 Java 并发编程中一个很重要的概念,它在多线程环境下简化了锁的管理,使得编程模型更容易理解和实现,尤其是在存在复杂的锁定模式与递归锁定时。
可重入锁的设计使得同一线程可以多次获得同一把锁,对于递归函数和对同一个资源的多层次锁定非常有用,因为它防止了线程在尝试获得它已经持有的锁时阻塞自己,这在非可重入锁中会导致死锁。
在实际的开发过程中,函数之间的调用关系可能错综复杂,一不小心就可能在多个不同的函数中反复调用 lock(),从而导致线程自身“卡死”。对于希望采用“傻瓜式”编程的我们来说,可重入锁就是用来解决这个问题的。
可重入锁使得同一个线程可以对同一把锁,在不释放的前提下反复获取,而不会导致线程卡死。因此,如果我们使用的是重入锁,那么程序就可以正常工作。唯一需要保证的是使用 unlock() 的次数和使用 lock() 的一样多。
在 Java 中,synchronized 关键字和 ReentrantLock 都是可重入锁,下面我们分别看一下两者的实现示例。
Java synchronized可重入锁
Java 的 synchronized 关键字具有可重入锁特性,具体示例代码如下:public class ReentrantExample { public synchronized void outerMethod() { System.out.println("在外部方法中"); innerMethod(); } public synchronized void innerMethod() { System.out.println("在内部方法中"); // 这里可以再次进入outerMethod(),因为这个线程已经拥有了这个对象的锁 } public static void main(String[] args) { ReentrantExample example = new ReentrantExample(); example.outerMethod(); } }在上述代码中,当线程在 outerMethod() 中调用 synchronized 修饰的 innerMethod() 时,不会发生阻塞,因为它已经持有了锁,而且该锁具有可重入性。
Java ReentrantLock可重入锁
在 Java 中,ReentrantLock 类是 java.util.concurrent.locks 包中提供的一个可重入互斥锁,它具有与 synchronized 关键字类似的基本行为和语义,但它更灵活。下面是一个使用 ReentrantLock 的示例:
import java.util.concurrent.locks.ReentrantLock; public class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); public void perform() { lock.lock(); // 获取锁 try { System.out.println("Locked: " + Thread.currentThread().getName()); doAdditionalWork(); } finally { lock.unlock(); // 在finally块中释放锁以确保锁一定会被释放 } } public void doAdditionalWork() { lock.lock(); // 再次获取锁,由于该锁是可重入的,因此这个操作是被允许的 try { System.out.println("ReentrantLock: " + Thread.currentThread().getName()); } finally { lock.unlock(); // 释放锁 } } public static void main(String[] args) { ReentrantLockExample example = new ReentrantLockExample(); example.perform(); } }在上述代码中,ReentrantLock 实例 lock 在 perform() 方法和 doAdditionalWork() 方法中实施锁定操作。当线程在 perform() 方法中获取锁时,它可以在 doAdditionalWork() 方法中再次获取相同的锁,而不会发生死锁,因为 ReentrantLock 允许线程进入任何已经锁定的由同一线程持有的代码块。
注意,每次调用 lock() 时都需要在 finally 块中匹配一个 unlock() 调用,以确保锁一定会被释放,防止死锁。
可重入锁的实现基于每个锁关联一个请求计数器和一个持有它的线程。当线程请求一个未被持有的锁时,JVM 会记下锁的持有者,并且将请求计数器的值设置为 1。如果同一个线程再次请求这个锁,请求计数器的值会递增。每当持有者线程退出同步块时,JVM 会将请求计数器的值递减。当请求计数器的值达到0时,锁被释放。
以下是可重入锁的几个关键功能:
- 持有者线程:锁需要知道当前持有者是哪个线程。在可重入锁的实现中,通常会有一个关联的数据结构用来记录这个信息;
- 请求计数器:请求计数器用于记录锁被同一个线程获取的次数,这个计数器有时称为重入计数器;
- 锁获取和释放:当线程首次获取锁时,计数器的值设置为1。如果这个线程尝试再次获取这个锁,计数器的值就会增加。只有当线程释放锁,计数器的值才会减少。当计数器的值回到 0 时,锁被完全释放;
- 可中断性和条件变量:大多数可重入锁的实现支持可中断的锁获取(即在等待锁的时候可以响应中断)和条件变量(允许线程在特定条件下等待,比如队列为空或为满),这为线程间的协调提供了更多控制;
- 公平性:一些可重入锁的实现允许用户选择是采用公平锁(等待时间最长的线程优先获得锁)还是非公平锁(抢占式获取锁)。
Java 中的 ReentrantLock 是一个标准的可重入锁的实现,它提供了以上描述的所有功能。ReentrantLock 内部使用 AQS 作为实现同步状态管理的框架,AQS 用一个整数变量来表示锁的持有次数,并使用一个节点队列来表示线程及其等待状态。
可重入性是 Java 并发编程中一个很重要的概念,它在多线程环境下简化了锁的管理,使得编程模型更容易理解和实现,尤其是在存在复杂的锁定模式与递归锁定时。