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. 怎么选:一张决策表
| 场景 | 推荐 | 理由 |
|---|---|---|
| 临界区短、逻辑简单 | synchronized | JVM 有锁升级优化,代码自动释放锁,不易出错 |
| 需要超时、可中断、公平 | ReentrantLock | tryLock(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 有哪些锁",正确姿势不是罗列名词,而是给出一条分类轴:悲观/乐观 → 公平/非公平 → 可重入 → 排他/共享 → 实现层次 → 升级形态,再各举一个实现。这样既显得体系化,也不容易被追问倒。