跳到主要内容

锁(补充中..)

· 阅读需 9 分钟

本文介绍锁

ReentrantLock:可重入锁(JDK提供)

Java并发基石ReentrantLock:深入解读其原理与实现-腾讯云开发者社区-腾讯云

  • 最常用、最典型的 Lock 实现。
  • 支持可重入(同一个线程可多次加锁)。
  • 提供公平锁和非公平锁两种模式(构造函数可选)。
  • 基于 AQS(AbstractQueuedSynchronizer) 实现。

它的“可重入”特性意味着:如果一个线程已经持有了该锁,那么它可以在不释放锁的情况下再次调用 lock() 方法而不会被阻塞,也不会报错。

示例代码:


import java.util.concurrent.locks.ReentrantLock;

public class ReentrantExample {
private static final ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) {
lock.lock(); // 第一次获取锁,hold count = 1
try {
System.out.println("第一次加锁");

lock.lock(); // 再次加锁,hold count = 2
try {
System.out.println("第二次加锁");
} finally {
lock.unlock(); // hold count = 1
}
} finally {
lock.unlock(); // hold count = 0,真正释放锁
}
}
}

ReentrantReadWriteLock:读写锁(JDK提供)

注意:ReentrantReadWriteLock 本身不直接实现 Lock 接口,但它的两个内部类 ReadLockWriteLock 都实现了 Lock

ReentrantReadWriteLock 适用于读远多于写的场景,能显著提升并发性能。合理使用读写锁,可避免 synchronized 的过度串行化问题。

  • ReentrantReadWriteLock.ReadLock

ReentrantReadWriteLock 的内部类。实现了 Lock 接口,代表读锁。 多个线程可同时持有读锁(只要没有写锁)。 读锁是可重入的。

  • ReentrantReadWriteLock.WriteLock

ReentrantReadWriteLock 的另一个内部类。实现了 Lock 接口,代表写锁。 写锁是独占的(同一时间只能一个线程持有)。 写锁也是可重入的,并且支持锁降级(先获取写锁,再获取读锁,然后释放写锁)。

示例代码:

示例代码

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class CacheExample {
private final Map<String, String> cache = new HashMap<>();
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

// 读操作:多个线程可同时读
public String get(String key) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取读锁,读取 key: " + key);
return cache.get(key);
} finally {
readLock.unlock();
}
}

// 写操作:仅一个线程可写
public void put(String key, String value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + " 获取写锁,写入 key: " + key);
try {
Thread.sleep(100); // 模拟写入耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cache.put(key, value);
} finally {
writeLock.unlock();
}
}

// 演示锁降级:先写后读(在持有写锁的情况下获取读锁)
public String getOrCreate(String key, String defaultValue) {
writeLock.lock();
try {
String value = cache.get(key);
if (value == null) {
// 模拟计算或加载
value = defaultValue + "_computed";
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入新值: " + value);
}

// 锁降级:在释放写锁前先获取读锁
readLock.lock(); // 降级关键步骤
try {
writeLock.unlock(); // 释放写锁,但仍然持有读锁
System.out.println(Thread.currentThread().getName() + " 降级为读锁,返回: " + value);
return value;
} finally {
readLock.unlock(); // 最终释放读锁
}
} finally {
// 注意:写锁已在 try 块中释放,所以这里不能再次 unlock
// 因此上面的结构必须小心处理
}
}

// 简化版(无锁降级)更安全的写法:
public String getOrCreateSafe(String key, String defaultValue) {
// 先尝试读(无锁竞争时快速返回)
readLock.lock();
String value = cache.get(key);
if (value != null) {
try {
return value;
} finally {
readLock.unlock();
}
}
readLock.unlock(); // 注意:此处需确保未在 try 中,否则可能重复 unlock

// 未命中,升级为写锁
writeLock.lock();
try {
// 双重检查(防止多个线程同时写)
value = cache.get(key);
if (value == null) {
value = defaultValue + "_computed";
cache.put(key, value);
System.out.println(Thread.currentThread().getName() + " 写入新值: " + value);
}

// 保持写锁直到结束(不降级),或直接返回
return value;
} finally {
writeLock.unlock();
}
}

// 测试主方法
public static void main(String[] args) throws InterruptedException {
CacheExample cache = new CacheExample();

// 启动多个读线程
for (int i = 0; i < 3; i++) {
final int id = i;
new Thread(() -> {
String val = cache.get("name");
if (val == null) {
cache.put("name", "Alice");
}
}, "Reader-" + id).start();
}

// 启动一个写线程
new Thread(() -> {
cache.put("age", "30");
}, "Writer").start();

Thread.sleep(2000); // 等待线程执行完成
}
}

提示

关键点说明:

  • 读锁(ReadLock):
    • 多个线程可同时持有读锁。
    • 读锁期间不能有写锁。
  • 写锁(WriteLock):
    • 独占,同一时间只能一个线程持有。
    • 写锁会阻塞所有读和其他写。
  • 可重入性:
    • 同一线程可以多次获取读锁或写锁(计数器机制)。
  • 锁降级(Lock Downgrading):
    • 允许从写锁降级为读锁(先获取写锁 → 再获取读锁 → 释放写锁 → 最后释放读锁)。
    • 不允许升级(即持有读锁时不能直接获取写锁,会导致死锁)。
  • 双重检查(Double-Check):
    • 在 getOrCreateSafe 中展示了一种更常见的“先读再写”模式,避免不必要的写锁开销。

⚠️ 注意事项:

  • 不要在持有读锁时尝试获取写锁(会导致死锁,因为写锁需等待所有读锁释放,而当前线程自己持有一个读锁)。

  • 锁降级代码要小心 unlock() 顺序,避免 IllegalMonitorStateException。

  • 推荐优先使用 “先读再写” + 双重检查 的模式,比锁降级更简单安全。

概念

无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁:

在 Java 的并发机制中,synchronized 关键字的底层实现经历了从 JDK 1.6 开始的重大优化,引入了 锁升级(Lock Elimination & Lock Coarsening) 和 四种锁状态:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

这些锁状态是 JVM 为了在不同竞争程度下平衡性能与开销而设计的。下面逐一详解:

1. 无锁(No Lock / Unlocked)

  • 含义:对象未被任何线程锁定。
  • Mark Word 状态:存储对象的哈希码、分代年龄等信息。
  • 特点:
    • 没有同步操作。
    • 多个线程可自由访问共享资源(但可能产生竞态条件,需靠 volatile、CAS 等无锁编程保证安全)。
  • 不是 synchronized 的状态,而是“尚未加锁”的初始状态。

✅ 示例:普通对象刚创建时就是无锁状态。

2. 偏向锁(Biased Locking)

🎯 目标

优化只有一个线程反复进入同步块的场景(比如单线程应用或低竞争环境)。

✅ 工作原理

  • 当第一个线程访问同步块时,JVM 会将对象头的 Mark Word 偏向该线程 ID
  • 后续该线程再次进入同步块时,无需 CAS 操作,直接判断是否是自己,如果是就继续执行。
  • 零成本获取锁(除了第一次设置偏向)。

⚠️ 注意

  • 偏向锁默认在 JDK 15 被废弃,JDK 17+ 默认禁用(因为现代应用多线程竞争更常见,偏向锁反而增加复杂度)。
  • 可通过 -XX:+UseBiasedLocking 启用(JDK 8~14 默认开启)。

🔄 撤销偏向锁

  • 当第二个线程尝试竞争锁时,JVM 会撤销偏向锁(需要 STW 安全点),升级为轻量级锁。

3. 轻量级锁(Lightweight Locking)

🎯 目标

处理短时间、低竞争的同步场景,避免直接进入 OS 互斥量(重量级锁)。

✅ 工作原理

  • 线程在栈帧中创建一个 Lock Record。
  • 使用 CAS 尝试将对象头的 Mark Word 替换为指向 Lock Record 的指针。
    • 成功 → 获取锁(轻量级)。
    • 失败 → 说明有竞争,升级为重量级锁。
  • 解锁时再用 CAS 恢复原 Mark Word。

⚠️ 特点

  • 自旋等待:在升级前可能短暂自旋(JDK 1.6+ 引入自旋优化)。
  • 适合:同步块执行快、线程交替执行(非同时竞争)。
  • 不适合:高并发、长时间持有锁(会导致大量 CAS 失败,浪费 CPU)。

4. 重量级锁(Heavyweight Locking)

🎯 目标 处理高竞争、长时间持有锁的场景。

✅ 工作原理

  • 依赖操作系统的 互斥量(Mutex) 实现。
  • 线程阻塞时会被挂起(进入内核态),由 OS 调度器管理。
  • 阻塞/唤醒开销大(上下文切换、系统调用)。

⚠️ 特点

  • 性能最差,但能保证强一致性。
  • 一旦升级为重量级锁,不会降级(JDK 中锁只能升级,不能降级)。
  • 对象头 Mark Word 指向 monitor 对象(ObjectMonitor),包含 _owner_WaitSet_EntryList 等。

❗ 锁升级是单向的:偏向 → 轻量 → 重量,不会降级。

📊 对比总结

锁类型适用场景是否阻塞底层机制性能开销
无锁无同步-最低
偏向锁单线程反复进入同步块Mark Word 偏向极低
轻量级锁短时间、低竞争否(自旋)CAS + Lock Record
重量级锁高竞争、长时间持有OS Mutex

💡 补充说明

  • 锁消除(Lock Elimination):JIT 编译器发现同步代码块只被单线程访问(如局部变量),直接去掉锁。

  • 锁粗化(Lock Coarsening):将多个相邻的同步块合并成一个,减少频繁加解锁开销。

  • 对象头 Mark Word 结构(32位 JVM 示例):

    | 25 bit hashcode | 4 bit age | 1 bit biased_lock | 2 bit lock |
    lock 字段值:
    01 → 无锁 / 偏向锁
    00 → 轻量级锁
    10 → 重量级锁
    11 → GC 标记

✅ 最佳实践建议:

  • 尽量减小 synchronized 块范围(提高并发度)。
  • 避免在同步块中做 I/O 或耗时操作(防止升级为重量级锁)。
  • 在高并发场景,可考虑 ReentrantLock(支持公平锁、超时、中断等)。