基于本文回答
0
评论

Java 中的锁有哪几种类型?

在 Java 中,对“锁”的划分可以从不同的维度(如设计思想、公平性、共享性、实现方式等)来进行。同一种锁在不同的维度下可以有不同的名称,例如 synchronized 既是悲观锁,也是独占锁可重入锁非公平锁

以下是 Java 中常见锁类型的分类详解:


一、 按锁的思想 / 策略分类(乐观 vs 悲观)

  1. 悲观锁(Pessimistic Lock)

    • 概念:总是假设最坏的情况,认为每次去拿数据时别人都会修改,所以在每次拿数据时都会上锁。其他线程想拿数据必须阻塞等待。
    • 实现方式synchronized 关键字、ReentrantLock
    • 适用场景:多写少读、并发竞争激烈的场景。
  2. 乐观锁(Optimistic Lock)

    • 概念:总是假设最好的情况,认为每次去拿数据时别人不会修改,所以不会上锁;但在更新数据时会判断在此期间有没有人更新过该数据(通常基于 CAS(Compare And Swap) 机制或版本号机制)。
    • 实现方式java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)、StampedLock 的乐观读策略。
    • 适用场景:多读少写、并发竞争较轻的场景。

二、 按公平性分类(公平 vs 非公平)

  1. 公平锁(Fair Lock)

    • 概念:多个线程按照申请锁的顺序来获取锁,类似于排队,先到先得(FIFO)。
    • 实现方式new ReentrantLock(true)
    • 优缺点:吞吐量较低,但不会导致线程“饥饿”。
  2. 非公平锁(Non-fair Lock)

    • 概念:获取锁的顺序不按申请顺序,允许“插队”。新来的线程有机会直接抢占锁,抢不到才进入等待队列。
    • 实现方式synchronized(只能是非公平锁)、new ReentrantLock(false)(默认模式)。
    • 优缺点:整体吞吐量较高,但可能导致队列中的某些线程长时间抢不到锁(饥饿)。

三、 按共享/独占性分类(独占 vs 共享)

  1. 独占锁 / 排他锁 / 互斥锁(Exclusive Lock)

    • 概念:该锁一次只能被一个线程持有。如果一个线程加了独占锁,其他线程不能再对该资源加任何锁。
    • 实现方式synchronizedReentrantLockReentrantReadWriteLock.WriteLock(写锁)。
  2. 共享锁(Shared Lock)

    • 概念:该锁可被多个线程同时持有。并发读取资源时可以提高效率。
    • 实现方式ReentrantReadWriteLock.ReadLock(读锁)、Semaphore(信号量)、CountDownLatch

四、 按可重入性分类(可重入 vs 不可重入)

  1. 可重入锁 / 递归锁(Reentrant Lock)

    • 概念:同一个线程在已经获取外层方法的锁之后,再进入该锁保护的内层方法时,可以自动再次获取该锁,不会因为自身持锁而导致死锁。
    • 实现方式synchronizedReentrantLock。内部通过维护一个持锁线程标识计数器(Hold Count)来实现。
  2. 不可重入锁(Non-reentrant Lock)

    • 概念:同一个线程再次获取已持有的锁时会被阻塞(会导致死锁)。
    • 实现方式:如 ThreadPoolExecutor$Worker 自自定义实现的锁。

五、 按线程等待策略分类(自旋锁 vs 阻塞锁)

  1. 自旋锁(Spin Lock)

    • 概念:当线程尝试获取锁失败时,不立即挂起阻塞,而是执行一个忙循环(不断 CAS 重试),等待锁被释放。
    • 优缺点:避免了线程上下文切换(用户态与内核态切换)的开销,适合锁占用时间极短的场景;但若持锁时间长,会白白浪费 CPU 资源。
  2. 自适应自旋锁(Adaptive Spin Lock)

    • 概念:自旋的次数/时间不再固定,而是根据前一次在同一个锁上的自旋时间及锁持有者的状态来动态调整。JVM 会自动优化。
  3. 阻塞锁

    • 概念:获取锁失败后,线程进入阻塞状态(如 BLOCKEDWAITING),放弃 CPU 时间片,等待被唤醒。

六、 synchronized 内部的锁升级形态(JVM 锁膨胀过程)

在 Java 6 之后,为了优化 synchronized 的性能,JVM 引入了锁升级机制(存储在 Java 对象头的 Mark Word 中):

  1. 无锁状态(No Lock):没有线程竞争。
  2. 偏向锁(Biased Lock):锁一直被同一个线程获取,锁会在 Mark Word 中记录该线程 ID,后续该线程进入无需同步操作。(注:JDK 15 开始标记废弃,JDK 21 已移除
  3. 轻量级锁(Lightweight Lock):当有不同线程交替竞争锁时,偏向锁升级为轻量级锁,采用自旋/CAS的方式获取锁,不挂起线程。
  4. 重量级锁(Heavyweight Lock):当竞争加剧(如自旋达到一定次数或多线程同时抢锁),锁升级为重量级锁,底层依赖操作系统的 Mutex Lock(互斥量),导致线程阻塞和上下文切换。

注:锁升级方向通常是不可逆的(无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁)。


七、 按代码实现体系分类(Java 实际开发角度)

  1. 内置锁 / 隐式锁(synchronized

    • JVM 层面实现,使用简单的关键字,自动加锁和释放锁。
    • 可以修饰实例方法(锁当前实例对象)、静态方法(锁当前类的 Class 对象)、代码块(锁指定对象)。
  2. 显式锁 / JUC 锁(Lock 接口体系)

    • 基于 Java 代码实现(在 java.util.concurrent.locks 包下),需要手动调用 lock()unlock()(通常配合 try-finally)。
    • 主要实现类
      • ReentrantLock:可重入锁,支持公平/非公平模式、可中断响应、超时获取等。
      • ReentrantReadWriteLock:读写锁(读共享,写独占)。
      • StampedLock(JDK 8+):读写锁的增强版,引入了乐观读机制,性能更高。

总结对照表

锁类型名称 代表性实现 特点 / 适用场景
悲观锁 synchronized, ReentrantLock 假定竞争多,先加锁,适合写多读少
乐观锁 AtomicInteger, StampedLock 假定竞争少,CAS 重试,适合读多写少
可重入锁 synchronized, ReentrantLock 同一线程可重复获取,防止死锁
公平锁 new ReentrantLock(true) 按顺序排队,无饥饿,吞吐量较低
非公平锁 synchronized, new ReentrantLock() 可插队,吞吐量高,可能导致饥饿
独占锁 ReentrantLock, 写锁 一次仅一个线程持有
共享锁 ReadLock, Semaphore 多个线程可同时持有
读写锁 ReentrantReadWriteLock 读读共享、读写互斥、写写互斥
右滑查看面试常问