详解Java可重入锁ReentrantLock
Java的ReentrantLock
是一个高级的线程同步工具,提供了比传统的synchronized
方法和语句更灵活、更丰富的线程同步机制。这个锁属于可重入锁(也称为递归锁),意味着同一个线程可以多次获取同一个锁而不会产生死锁。下面详细解释和说明ReentrantLock
的特性和用法:
介绍
1. 可重入性
- 定义: 可重入意味着同一个线程可以多次获得同一把锁。如果一个线程已经持有某个锁,再次请求这个锁时会立即成功,并且会增加一个内部的计数器。
- 用途: 这个特性在递归函数或者有多个同步方法需要同一把锁时非常有用。
2. 公平性和非公平性
- 公平锁: 公平锁意味着锁的分配按照线程等待的顺序进行,即“先来先服务”。
- 非公平锁: 非公平锁不保证这种顺序。一个新请求的锁可能会在等待队列中的线程之前获得锁。
- 选择: 在创建
ReentrantLock
实例时可以选择是公平锁还是非公平锁,默认是非公平锁。
3. 锁的获取和释放
- 获取锁: 使用
lock()
方法获取锁。如果锁已被其他线程持有,则当前线程会等待。 - 释放锁: 使用
unlock()
方法释放锁。每个lock()
调用都需要配对的unlock()
调用。
4. 中断响应
ReentrantLock
提供了一种能力,使得线程能够响应中断。当在锁等待过程中,线程可以选择放弃等待并继续执行。
5. 条件变量
ReentrantLock
提供了条件变量(Condition)功能,允许线程在某些条件不满足时挂起,直到条件改变时再继续执行。
6. 与synchronized
的比较
synchronized
是Java内置的同步机制,更简单,但功能也较少。ReentrantLock
提供更高的灵活性,比如可中断的锁等待、公平锁选择、多个条件变量等。
使用示例
import java.util.concurrent.locks.ReentrantLock;
public class Example {
private final ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock(); // 获取锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
}
}
在使用ReentrantLock
时,最佳实践是始终在try
/finally
块中释放锁,以确保即使在临界区代码中发生异常时,锁也会被释放。通过这种方式,ReentrantLock
提供了比synchronized
更精细和更灵活的线程同步控制。
QA
ReentrantLock是怎么实现非公平锁的?
ReentrantLock
的非公平锁实现主要基于几个核心概念和机制,这些机制和概念使得ReentrantLock
在处理非公平锁时表现出高效和灵活性。下面是ReentrantLock
非公平锁实现的关键方面:
1. 锁状态的管理
ReentrantLock
使用一个内部的变量(通常是一个volatile
字段)来维护锁的状态,这个状态指示锁是否被某个线程持有。- 在非公平锁的情况下,当线程尝试获取锁时,它首先检查锁是否已被其他线程持有。
2. CAS(Compare-And-Swap)操作
ReentrantLock
使用CAS操作来尝试获取锁。CAS是一种原子操作,用于更新锁的状态,如果当前状态是未锁定的,则将其设置为锁定状态。- 在非公平锁的实现中,线程首先尝试通过CAS操作直接抢占锁,而不是先检查等待队列。如果成功,线程就获取了锁。
3. 等待队列
- 如果CAS操作失败(意味着其他线程已经持有锁),线程将被放入等待队列。
- 但与公平锁不同,即使线程在等待队列中,它仍然有机会在其他线程释放锁时,通过再次尝试CAS操作来抢占锁。
4. 锁的释放
- 当持有锁的线程完成其临界区代码后,它会释放锁,通常是通过将锁状态变量设置回未锁定状态。
- 在锁被释放时,通知等待队列中的线程锁已可用,但线程尝试获取锁的顺序并不保证是它们进入队列的顺序。
5. 性能优势
- 非公平锁的这种实现方式在多线程环境中通常可以提供更好的性能,因为它减少了线程之间的上下文切换和队列管理的开销。
- 但这种方法可能导致某些线程饥饿,即长时间得不到执行,因为新到的线程可能会在等待较久的线程之前获得锁。
总结
ReentrantLock
的非公平锁实现通过允许线程直接尝试获取锁,而不是按照等待队列中的顺序,从而实现非公平性。这种方法优化了性能,但也可能导致线程饥饿的问题。在设计并发控制时,选择使用公平锁还是非公平锁取决于特定应用的需求和性能考量。
不存在新线程的情况下,非公平锁已加入队列的线程是按照FIFO原则获取锁对吗
是的,在不存在新线程争抢的情况下,非公平锁中已加入等待队列的线程通常会按照FIFO(先进先出)原则获取锁。这意味着,尽管非公平锁允许新到达的线程有机会在加入等待队列之前获取锁,但一旦线程进入了等待队列,它们通常会按照它们加入队列的顺序来获取锁。
非公平锁的工作机制
-
锁争抢:在锁被释放时,如果有新的线程尝试获取锁,这个线程可以直接尝试获取锁,而不需要加入等待队列。
-
等待队列:如果直接获取锁失败,线程将加入等待队列。一旦线程处于等待队列中,它将等待锁按照队列的顺序释放给前面的线程。
-
按顺序获取:在没有新线程争抢的情况下,当锁被释放时,通常是等待队列中的第一个线程(即最长时间等待的线程)获得机会来获取锁。
等待队列的行为
- 尽管非公平锁的设计允许在某些情况下“跳过”等待队列,但一旦线程加入了队列,它们之间的相对顺序通常是被尊重的。
- 这意味着在没有新线程介入的情况下,非公平锁的行为在实际应用中可能与公平锁非常相似。
总结
在没有新线程竞争的情况下,非公平锁的行为类似于公平锁,即遵循FIFO原则,按照线程加入等待队列的顺序来获取锁。然而,非公平锁的关键区别在于它提供了新到达线程直接尝试获取锁的机会,这可能在某些情况下导致等待队列中的线程被“插队”。
ReentrantLock的公平锁和synchronized
有什么区别?
ReentrantLock
的非公平锁和Java的synchronized
关键字在多个方面有着显著的区别。这些区别涵盖了锁的行为、灵活性、功能和性能等多个方面。
锁的行为
- ReentrantLock的非公平锁: 允许线程“插队”,即新请求的锁可能在等待队列中的线程之前获得锁。这可以在某些情况下提高性能,但可能导致等待时间较长的线程饥饿。
synchronized
: 不保证任何关于线程获取锁的顺序。虽然它也是非公平的,但在具体实现中,它可能表现出某种隐性的公平性或随机性。或者说ReentrantLock
的非公平锁相对于synchronized
会更不公平。
灵活性和控制
- ReentrantLock: 提供更多的灵活性和控制。它允许尝试非阻塞地获取锁(
tryLock
)、可以响应中断、支持条件变量(Condition
)等。 synchronized
: 相对简单,没有提供上述高级特性。它是自动管理锁的,即在进入和退出同步块时自动获取和释放锁。
功能性
- ReentrantLock: 支持多个条件变量,允许更复杂的线程间通信和等待/通知模式。
synchronized
: 只与单一的条件队列相关联,即每个对象的内置锁关联一个条件队列。
性能
- ReentrantLock: 在高竞争的环境下可能提供更好的性能,尤其是在使用非公平锁时。
synchronized
: 近年来,Java虚拟机对synchronized
做了大量优化(如偏向锁和轻量级锁),使得其性能显著提高,尤其是在低竞争环境下。
使用和易用性
- ReentrantLock: 需要显式地管理锁的获取和释放,通常将释放锁的操作放在
finally
块中。 synchronized
: 更易于使用,因为进入和退出同步块时锁的获取和释放是自动的。
总结
尽管ReentrantLock
的非公平锁和synchronized
都不保证公平性,但ReentrantLock
提供了更多的功能和灵活性。它允许更精细的锁控制,包括响应中断、尝试锁定、支持多个条件变量等。然而,这些额外的功能也使得ReentrantLock
的使用比synchronized
更复杂。