Java 中常见的锁有哪些?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官抛出 “Java 并发常见的锁有哪些?” 这个问题,通常不是想听你背一个列表,而是希望通过你的回答考察以下几点:

  1. 对并发锁体系的理解:你是否清楚 Java 中锁的分类方式(比如从语法层面、功能层面、设计层面)?
  2. 对不同锁特性的掌握:比如 synchronized 和 ReentrantLock 的区别,读写锁和普通互斥锁的适用场景,以及乐观锁和悲观锁的思想。
  3. 实际应用经验:在真实项目中,你会根据什么场景选择哪种锁?能否说出各自的优缺点和注意事项?
  4. 对底层机制的了解:是否知道锁的实现原理(比如 synchronized 的 monitor 机制、AQS 框架、CAS 等)?
  5. 对锁优化的认知:是否了解 JVM 对锁做的优化(如偏向锁、轻量级锁、锁粗化、锁消除)?

核心答案

Java 中的锁可以从多个维度来划分,常见的锁包括:

  • 语法/实现层面synchronized 关键字(内置锁)、Lock 接口及其实现类(如 ReentrantLockReentrantReadWriteLockStampedLock)。
  • 特性层面:可重入锁、公平锁/非公平锁、读写锁、乐观锁/悲观锁、自旋锁、分段锁等。
  • 并发工具包中的辅助锁Semaphore(信号量,可看作共享锁)、CountDownLatchCyclicBarrier 等虽不是严格意义上的锁,但也用于控制线程协作。

下面我们从最常用的几个维度展开聊聊。

技术深度解析

1. 从语法和 API 层面看

synchronized(内置锁)

  • 使用方式:修饰实例方法、静态方法或代码块。
  • 原理:基于对象头的 Mark Word 和 monitor 对象实现。JDK 1.6 以后引入了偏向锁、轻量级锁、重量级锁的升级过程,以优化性能。
  • 特点:使用简单,自动释放锁(异常或正常退出时),可重入。但无法中断等待锁的线程,也无法设置超时。
  • 最佳实践:适用于锁的竞争不激烈、代码简单的情况。比如单例模式的同步方法、简单的线程安全计数器。

Lock 接口(显式锁)

最核心的实现是 ReentrantLock,它提供了比 synchronized 更灵活的锁操作。

  • ReentrantLock

    • 可重入:同一个线程可以多次获得同一把锁。
    • 公平/非公平:构造函数可指定是否公平。公平锁按线程等待顺序获取锁,非公平锁允许插队(默认非公平,吞吐量更高)。
    • 可中断lockInterruptibly() 允许在等待锁时响应中断。
    • 超时获取tryLock(long time, TimeUnit unit) 可以指定等待时间。
    • 配合 Condition:可实现多个等待/通知条件,比 wait/notify 更精细。

    示例:

    ReentrantLock lock = new ReentrantLock(true); // 公平锁
    lock.lock();
    try {
        // 临界区
    } finally {
        lock.unlock(); // 必须手动释放
    }
    
  • ReentrantReadWriteLock

    • 维护一对锁:读锁(共享)和写锁(排他)。读多写少场景下可大幅提升并发度。
    • 读锁可以被多个读线程同时持有,但写锁独占,且读写互斥。
    • 可能出现“写饥饿”问题,但在非公平模式下,写锁优先级通常较高。
  • StampedLock(JDK 8 引入)

    • 进一步优化读多写少场景,支持三种模式:写锁、读锁、乐观读。
    • 乐观读:不加锁,直接读取数据,之后通过 validate 检查是否有写操作发生。若无效再升级为读锁重读。
    • 特点:不可重入,不支持条件变量,适合读线程远多于写线程的场景。

2. 从锁的特性层面看

  • 悲观锁 vs 乐观锁

    • 悲观锁:假设并发冲突概率高,每次操作都加锁(如 synchronized、ReentrantLock)。适合写多场景。
    • 乐观锁:假设冲突少,先操作,提交时用 CAS(Compare And Swap)检测冲突,冲突则重试。如 java.util.concurrent.atomic 包下的原子类。适合读多写少、冲突概率低的场景。
  • 可重入锁
    同一线程可以重复获取同一把锁,避免死锁。Java 中的 synchronized 和 ReentrantLock 都是可重入的。

  • 公平锁 vs 非公平锁
    公平锁按线程请求锁的顺序获取锁,避免饥饿,但吞吐量较低;非公平锁允许插队,吞吐量更高,但可能导致某些线程长时间得不到锁。

  • 自旋锁
    线程获取锁失败时不立即挂起,而是循环尝试(自旋),减少线程上下文切换的开销。适合锁持有时间短的场景。JDK 内部大量使用了自旋锁(如 ConcurrentLinkedQueue 的入队操作)。Java 中也提供了 AbstractQueuedSynchronizer(AQS)对自旋和阻塞的支持。

  • 分段锁
    ConcurrentHashMap 在 JDK 1.7 中的实现,将数据分段,每段一把锁,降低锁粒度,提升并发度。JDK 1.8 改用 CAS + synchronized 对每个桶(数组元素)加锁,相当于进一步细化。

3. 并发工具包中的 “锁” 变体

  • Semaphore:计数信号量,可以控制同时访问资源的线程数量(共享锁)。比如限流场景。
  • CountDownLatch:让一个或多个线程等待,直到一组操作完成。
  • CyclicBarrier:让一组线程互相等待,到达某个屏障点后再同时执行。

虽然这些不是传统意义上的锁,但它们也常被用来解决线程协作问题,面试时可以作为补充提及。

常见误区与最佳实践

  • 误区一:认为 synchronized 性能总比 ReentrantLock 差。实际上 JDK 1.6 以后 synchronized 经过优化,性能与 ReentrantLock 差距不大,甚至在低竞争时更好。选择时应更关注功能需求。
  • 误区二:滥用读写锁。如果读操作很短,或者写操作频繁,读写锁可能不如普通互斥锁,因为维护读锁和写锁本身也有开销。
  • 误区三:忘记在 finally 中释放显式锁。这是新手最容易犯的错误,可能导致死锁。
  • 最佳实践
    • 优先使用 synchronized,简洁安全;需要高级功能(如可中断、超时、公平性、多 Condition)时才选用 ReentrantLock。
    • 读多写少且数据一致性要求不那么极致时,可考虑 StampedLock 的乐观读模式,或使用 CopyOnWrite 容器。
    • 对于简单的原子操作,优先使用 Atomic 系列类(乐观锁思想),避免锁开销。
    • 了解 JVM 的锁优化(如偏向锁、轻量级锁),有助于调优高并发应用。

总结

Java 中的锁家族非常丰富,从最基础的 synchronized 到灵活的 Lock 体系,再到各种并发工具和乐观锁实现,每种锁都有其特定的适用场景。掌握它们的特性、原理和优缺点,才能在实际开发中做出合理的选择,写出既安全又高效的并发程序。