Lock是一种同步机制,为多线程应用程序中的关键代码段提供独占访问。根据Oracle, 2024,Lock接口相比传统的synchronized块提供了更灵活的同步控制,包括带超时的锁定尝试以及对多个等待队列的支持。
要点
Lock 是来自java.util.concurrent.locks包的接口,提供显式的加锁和解锁操作以同步数据访问。与synchronized不同,Lock为开发人员提供了对锁定机制的完全控制。
Lock接口在Java 5中作为内置synchronized机制的替代方案出现。主要方法包括lock、unlock、tryLock和lockInterruptibly。锁可以在多线程环境中组织对数据的安全访问,防止竞态条件和数据损坏。
Lock相比synchronized的主要优势在于灵活性。开发人员可以尝试带超时的锁定,无需阻塞即可检查锁定状态,或组织具有不同优先级的多个等待队列。
在Java 5中Lock接口出现之前,唯一的同步方式是synchronized,它受到诸多限制:缺少超时、无法中断等待以及单一队列。Doug Lea设计了java.util.concurrent包,将Lock作为基本构建模块包含其中。
锁通过内部状态标志和等待队列管理访问。当线程调用lock()时,机制检查锁是否空闲,然后锁定或将线程放入队列直到释放。
每个锁的基础是比较并交换(CAS)原子操作。调用lock()时,线程尝试原子性地设置占用标志。如果标志已设置,线程将被阻塞。调用unlock()时,标志被重置,并唤醒一个等待线程。
import java.util.concurrent.locks.ReentrantLock
val lock = ReentrantLock()
fun performTask() {
lock.lock()
try {
// 临界区
println("线程${Thread.currentThread().name}正在运行")
} finally {
lock.unlock()
}
}
ReentrantLock内部使用双向队列(CLH锁队列),其中每个等待线程由一个节点表示。当锁被释放时,队列的头节点被唤醒。公平模式保证FIFO顺序,而非公平模式允许新线程在等待线程之前加锁以提高吞吐量。
在现代Java技术栈中,存在多种锁的实现,每种都针对特定场景进行了优化。选择合适的锁直接影响多线程应用程序的性能和可靠性。
ReentrantLock — Lock的基础且最常用的实现。它支持同一线程重复加锁:如果线程已经持有锁,重复调用lock()不会阻塞它。这可以防止递归调用中的死锁。
ReadWriteLock将锁分为两种模式:读和写。多个线程可以同时持有读锁,但写锁需要独占访问。这在频繁读取和少量写入时显著提高性能。
StampedLock — 最新实现,出现在Java 8中。它支持三种模式:写、读和乐观读。乐观读不会阻塞其他线程,并在读取后验证数据的有效性,相比ReadWriteLock提供10-20%的性能提升。
| 锁 | Java版本 | 模式 | 性能 |
|---|---|---|---|
| ReentrantLock | Java 5 | 独占 | 高 |
| ReadWriteLock | Java 5 | 读 + 写 | 中 |
| StampedLock | Java 8 | 读 + 写 + optimistic | 非常高 |
ReentrantLock — Lock最流行的实现,提供了一系列synchronized中不可用的功能。了解其特性对于有效处理多线程至关重要。
ReentrantLock的构造函数接受fair参数。当为true时,锁保证FIFO访问顺序;当为false时,新线程可以在等待线程之前加锁。公平模式防止饥饿,但由于维护队列的额外开销,吞吐量会降低10-20%。
与synchronized不同,ReentrantLock支持带超时的tryLock。如果在指定时间内无法获取锁,线程将继续执行而不是无限阻塞。lockInterruptibly方法允许通过Thread.interrupt()中断等待中的线程。
val lock = ReentrantLock()
fun tryTask() {
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
println("锁定已获取")
} finally {
lock.unlock()
}
} else {
println("无法获取锁定")
}
}
ReentrantLock通过newCondition()方法支持多个条件变量。每个Condition都有自己的等待队列,可以组织复杂的唤醒场景。await()和signal()方法取代了synchronized块中的wait()和notify(),但支持多个队列。
ReadWriteLock和StampedLock解决了读取操作占主导时访问优化的问题。在读取比写入更频繁的场景中,它们比ReentrantLock高效得多。
ReadWriteLock接口包含两个方法:readLock()和writeLock()。读锁可由多个线程同时持有,写锁只能由一个线程持有。一个典型例子是线程安全缓存:多个线程读取数据,只有一个线程定期更新数据。
class SafeCache<K, V> {
private val map = mutableMapOf<K, V>()
private val rwLock = ReentrantReadWriteLock()
fun get(key: K): V? {
rwLock.readLock().lock()
return try { map[key] } finally { rwLock.readLock().unlock() }
}
fun put(key: K, value: V) {
rwLock.writeLock().lock()
return try { map[key] = value } finally { rwLock.writeLock().unlock() }
}
}
StampedLock增加了第三种模式——tryOptimisticRead。此模式不阻塞其他线程,仅记住状态的戳记(stamp)。读取后,开发人员调用validate(stamp)检查数据在读取期间是否发生变化。如果数据已更改,则需要重复操作。
在移动应用中,锁用于协调线程之间对共享数据的访问。然而,由于设备资源有限以及需要保持界面响应性,使用锁需要特别小心。
在Android中,ReentrantLock在处理Room、缓存和文件时很有用。重要的是:永远不要在主线��上加锁。对于异步代码,更推荐使用kotlinx.coroutines中的协程和Mutex,它们不会阻塞线程,而是挂起协程。
在iOS中,来自NSLock的标准Lock使用较少——开发者更喜欢使用带barrier标志的DispatchQueue或操作锁os_unfair_lock。Swift 5.7+通过actors提供现代同步机制,自动保护状态。
import Foundation
actor DataStore {
private var items: [String] = []
func add(_ item: String) {
items.append(item)
}
func getAll() -> [String] {
items
}
}
为避免死锁,请遵守项目中所有锁的统一加锁顺序。在可能长时间阻塞的地方使用带超时的tryLock代替lock()。考虑使用Lock-Free算法(AtomicReference、ConcurrentHashMap)代替传统锁。
使用Lock需要遵守纪律并遵循几条规则,以防止死锁和性能下降。这些实践是Java社区在20年使用java.util.concurrent包的过程中形成的。
最重要的模式——在finally中解锁。无论关键段是成功完成还是出现异常,锁都必须被释放。这保证了其他线程不会因为一个错误而永远被阻塞。在Kotlin中,这个模式通过withLock扩展优雅地解决。
关键段应尽可能短。切勿在锁内部执行输入输出操作、网络请求或长时间计算。如果需要从服务器读取数据,先获取数据,然后仅锁定以更新共享状态。这减少了竞争并提高了系统吞吐量。
为防止处理多个Lock时发生死锁,在整个项目中确定全局加锁顺序。如果先锁定lockA,然后锁定lockB——任何相反的序列都应被代码审查规则禁止。对于自动检查,使用静态分析器,如SpotBugs和IntelliJ Inspections。
常见问题
Lock是显式接口,支持超时和可中断等待。synchronized自动加锁和解锁监视器,但不允许使用tryLock、lockInterruptibly和多个Condition。Lock更灵活,但需要在finally中手动释放。
公平锁保证FIFO访问顺序:等待最久的线程最先获得锁。非公平锁可能绕过队列将访问权限给新线程,这提高了吞吐量,但可能导致等待线程饥饿。
遵守所有锁的固定顺序,使用带超时的tryLock代替无条件lock,并最小化同时持有的锁数量。应用Lock-Free数据结构也能降低死锁风险。
Condition是Lock的wait/notify类似物,允许组织多个独立的等待队列。每次调用newCondition()创建一个单独的队列,与synchronized的单一队列相比,提供了对线程唤醒的更精确控制。
对于使用协程的Android,使用kotlinx.coroutines中的Mutex——它挂起协程而不是阻塞线程。对于Swift 5.7+的iOS,推荐使用actors,它们自动同步状态访问。将ReentrantLock留给遗留代码和低级场景。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。