Lock:什么是锁、锁的类型及在同步中的使用

作者: IT Sectr 发布日期: 2026-03-19 阅读时间: 8 分钟

Lock是一种同步机制,为多线程应用程序中的关键代码段提供独占访问。根据Oracle, 2024,Lock接口相比传统的synchronized块提供了更灵活的同步控制,包括带超时的锁定尝试以及对多个等待队列的支持。

要点

  • Lock — Java中用于显式管理锁的接口。
  • ReentrantLock — 支持同一线程重复加锁的基础实现。
  • ReadWriteLock 将锁分为读锁和写锁以提高性能。
  • 死锁 — 同时使用多个锁时的主要风险。
  • 与synchronized不同,Lock支持超时和可中断等待。

什么是Lock?

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()时,标志被重置,并唤醒一个等待线程。

kotlin
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

ReentrantLock — Lock的基础且最常用的实现。它支持同一线程重复加锁:如果线程已经持有锁,重复调用lock()不会阻塞它。这可以防止递归调用中的死锁。

ReentrantReadWriteLock

ReadWriteLock将锁分为两种模式:和写。多个线程可以同时持有读锁,但写锁需要独占访问。这在频繁读取和少量写入时显著提高性能。

StampedLock

StampedLock — 最新实现,出现在Java 8中。它支持三种模式:写、读和乐观读。乐观读不会阻塞其他线程,并在读取后验证数据的有效性,相比ReadWriteLock提供10-20%的性能提升。

Java版本模式性能
ReentrantLockJava 5独占
ReadWriteLockJava 5读 + 写
StampedLockJava 8读 + 写 + optimistic非常高

ReentrantLock及其特性

ReentrantLock — Lock最流行的实现,提供了一系列synchronized中不可用的功能。了解其特性对于有效处理多线程至关重要。

锁的公平性

ReentrantLock的构造函数接受fair参数。当为true时,锁保证FIFO访问顺序;当为false时,新线程可以在等待线程之前加锁。公平模式防止饥饿,但由于维护队列的额外开销,吞吐量会降低10-20%。

超时与可中断等待

与synchronized不同,ReentrantLock支持带超时的tryLock。如果在指定时间内无法获取锁,线程将继续执行而不是无限阻塞。lockInterruptibly方法允许通过Thread.interrupt()中断等待中的线程。

kotlin
val lock = ReentrantLock()

fun tryTask() {
    if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
        try {
            println("锁定已获取")
        } finally {
            lock.unlock()
        }
    } else {
        println("无法获取锁定")
    }
}

条件变量(Conditions)

ReentrantLock通过newCondition()方法支持多个条件变量。每个Condition都有自己的等待队列,可以组织复杂的唤醒场景。await()和signal()方法取代了synchronized块中的wait()和notify(),但支持多个队列。

ReadWriteLock和StampedLock

ReadWriteLock和StampedLock解决了读取操作占主导时访问优化的问题。在读取比写入更频繁的场景中,它们比ReentrantLock高效得多。

ReadWriteLock实践

ReadWriteLock接口包含两个方法:readLock()和writeLock()。锁可由多个线程同时持有,写锁只能由一个线程持有。一个典型例子是线程安全缓存:多个线程读取数据,只有一个线程定期更新数据。

kotlin
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与乐观读

StampedLock增加了第三种模式——tryOptimisticRead。此模式不阻塞其他线程,仅记住状态的戳记(stamp)。读取后,开发人员调用validate(stamp)检查数据在读取期间是否发生变化。如果数据已更改,则需要重复操作。

移动开发中的锁

在移动应用中,用于协调线程之间对共享数据的访问。然而,由于设备资源有限以及需要保持界面响应性,使用锁需要特别小心。

Android中的锁(Kotlin)

在Android中,ReentrantLock在处理Room、缓存和文件时很有用。重要的是:永远不要在主线��上加锁。对于异步代码,更推荐使用kotlinx.coroutines中的协程和Mutex,它们不会阻塞线程,而是挂起协程。

iOS中的锁(Swift)

在iOS中,来自NSLock的标准Lock使用较少——开发者更喜欢使用带barrier标志的DispatchQueue或操作锁os_unfair_lock。Swift 5.7+通过actors提供现代同步机制,自动保护状态。

swift
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的最佳实践

使用Lock需要遵守纪律并遵循几条规则,以防止死锁和性能下降。这些实践是Java社区在20年使用java.util.concurrent包的过程中形成的。

在finally中释放

最重要的模式——在finally中解锁。无论关键段是成功完成还是出现异常,锁都必须被释放。这保证了其他线程不会因为一个错误而永远被阻塞。在Kotlin中,这个模式通过withLock扩展优雅地解决。

最小化持有时间

关键段应尽可能短。切勿在锁内部执行输入输出操作、网络请求或长时间计算。如果需要从服务器读取数据,先获取数据,然后仅锁定以更新共享状态。这减少了竞争并提高了系统吞吐量。

统一加锁顺序

为防止处理多个Lock时发生死锁,在整个项目中确定全局加锁顺序。如果先锁定lockA,然后锁定lockB——任何相反的序列都应被代码审查规则禁止。对于自动检查,使用静态分析器,如SpotBugs和IntelliJ Inspections。

常见问题

Lock和synchronized有什么区别?

Lock是显式接口,支持超时和可中断等待。synchronized自动加锁和解锁监视器,但不允许使用tryLock、lockInterruptibly和多个Condition。Lock更灵活,但需要在finally中手动释放。

什么是公平锁?

公平锁保证FIFO访问顺序:等待最久的线程最先获得锁。非公平锁可能绕过队列将访问权限给新线程,这提高了吞吐量,但可能导致等待线程饥饿。

如何使用Lock避免死锁?

遵守所有锁的固定顺序,使用带超时的tryLock代替无条件lock,并最小化同时持有的锁数量。应用Lock-Free数据结构也能降低死锁风险。

Lock中的Condition是什么?

Condition是Lock的wait/notify类似物,允许组织多个独立的等待队列。每次调用newCondition()创建一个单独的队列,与synchronized的单一队列相比,提供了对线程唤醒的更精确控制。

移动应用应该选择哪种锁?

对于使用协程的Android,使用kotlinx.coroutines中的Mutex——它挂起协程而不是阻塞线程。对于Swift 5.7+的iOS,推荐使用actors,它们自动同步状态访问。将ReentrantLock留给遗留代码和低级场景。

总结

  • Lock — 来自java.util.concurrent.locks的显式锁管理接口。
  • ReentrantLock — 支持重入和公平性的主要实现。
  • ReadWriteLock 将锁分为读和写,适用于读密集型场景。
  • StampedLock 增加乐观读以实现最大性能。
  • 超时和Condition — Lock相比synchronized的主要优势。
  • 死锁通过固定的加锁顺序和使用tryLock来预防。
  • 在移动开发中推荐使用协程(Android)和actors(iOS)。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读