Java 中常见的锁有哪些?如何分类?

#Java#面试八股

Java 中常见的锁有哪些?如何分类?

结论先行:Java 的锁没有"很多种",只是同一批机制从不同维度看有不同名字。抓住六个维度就能说全:线程是否阻塞(悲观/乐观)、是否公平、是否可重入、共享还是排他、JVM 内置还是 JDK 实现、以及 synchronized 的锁升级形态。

1. 六个维度的分类表

分类维度取值代表实现
线程是否阻塞等待悲观锁 / 乐观锁synchronized、ReentrantLock / CAS、AtomicInteger、StampedLock 乐观读
竞争时是否排队公平锁 / 非公平锁new ReentrantLock(true) / synchronized、默认 ReentrantLock
同一线程能否重复获取可重入 / 不可重入ReentrantLock、synchronized / 自研锁、StampedLock
能否被多线程同时持有排他锁 / 共享锁ReentrantLock / ReentrantReadWriteLock.ReadLock、Semaphore
实现层次JVM 内置 / JDK 实现monitor(对象头 Mark Word)/ AQS(volatile state + CLH 队列)
优化形态偏向锁 / 轻量级锁 / 重量级锁只有 synchronized 有这套升级

2. 全景图

Java 锁
├── 关键字级:synchronized
│   └── 锁升级路径:无锁 → 偏向锁 → 轻量级锁(自旋)→ 重量级锁
└── API 级:java.util.concurrent.locks
    ├── ReentrantLock        可重入排他锁,基于 AQS,支持公平/超时/中断
    ├── ReentrantReadWriteLock  读写分离:读读共享、读写互斥
    ├── StampedLock          乐观读 + 悲观读写,不可重入,读性能更高
    └── Condition            await/signal,替代 wait/notify 的多条件队列

3. 四种典型用法

// 1. 内置锁:写起来最省事,JVM 自动释放
synchronized (lock) {
    balance -= amount;
}

// 2. 显式锁:可超时、可中断、可公平
ReentrantLock lock = new ReentrantLock(true);   // fair = true
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    try { balance -= amount; } finally { lock.unlock(); }
}

// 3. 读写锁:读多写少时让读并发
ReadWriteLock rw = new ReentrantReadWriteLock();
rw.readLock().lock();
try { return cache.get(key); } finally { rw.readLock().unlock(); }

// 4. 乐观读:先读后校验,失败再加悲观锁重试
long stamp = sl.tryOptimisticRead();
int cur = x;
if (!sl.validate(stamp)) {            // 期间有人写过,重来
    stamp = sl.readLock();
    try { cur = x; } finally { sl.unlockRead(stamp); }
}

4. 这几个名字容易混

说法实际含义常见误区
自旋锁抢不到锁时循环重试,不睡眠自旋适合临界区极短的场景,否则白烧 CPU
分段锁把数据分成多段、各段各一把锁JDK 1.7 的 ConcurrentHashMap 用;1.8 改成 CAS + 桶头锁
共享锁多个线程可同时持有不等于"无锁",写线程仍需互斥
分布式锁跨进程、跨机器的互斥不属于 JVM 锁,靠 Redis/ZooKeeper 实现,和 synchronized 不是一套东西
死锁互相等对方持有的锁是状态不是锁的类型,排查看 jstack 的 Found one Java-level deadlock

5. 怎么选:一张决策表

场景推荐理由
临界区短、逻辑简单synchronizedJVM 有锁升级优化,代码自动释放锁,不易出错
需要超时、可中断、公平ReentrantLocktryLock(timeout)、lockInterruptibly()
读远多于写(如本地缓存)ReentrantReadWriteLock读读不互斥,吞吐明显提升
读极多、写极少、可接受重试StampedLock乐观读几乎无开销,但不可重入
单纯限流/控制并发数Semaphore它是许可计数,不是互斥锁
单变量计数无锁:AtomicLong / LongAdder避免一切锁开销

6. 常见追问

追问一:synchronized 和 Lock 到底怎么选? 优先 synchronized:它是 JVM 原生支持,异常时自动释放,不会有"忘记 unlock"的灾难。只有需要超时、可中断、公平、多个条件队列时才上 Lock。记住一条铁律:lock() 之后必须把业务代码放进 try,unlock() 放 finally。

追问二:读写锁有什么坑? 写线程可能饥饿:读请求源源不断时,写线程永远排不上(ReentrantReadWriteLock 非公平模式下尤其明显)。另外读锁不能升级为写锁(会死锁),写锁却可以降级为读锁。

追问三:为什么说锁的本质在对象头? HotSpot 把锁状态记在对象头的 Mark Word 里(偏向线程 ID、轻量级锁指针、重量级锁的 monitor 指针)。这也是为什么 synchronized 必须锁一个对象,而不能锁基本类型。

追问四:Semaphore 算锁吗? 广义上算同步工具,狭义上不算互斥锁:它允许多个线程同时持有许可(new Semaphore(5) 就是 5 个并发),常用于限流和连接池控制。

小结

回答"Java 有哪些锁",正确姿势不是罗列名词,而是给出一条分类轴:悲观/乐观 → 公平/非公平 → 可重入 → 排他/共享 → 实现层次 → 升级形态,再各举一个实现。这样既显得体系化,也不容易被追问倒。

笔记加载中…