锁的应用场景
多线程访问共享资源时需要加锁,避免数据错乱。不同锁的代价和适用场景不同,选对了才能既有正确性又有性能。
选型速查(结论在前)
先看场景,再选手段:
- 临界区较长(如一段业务逻辑)→ 互斥锁(synchronized、ReentrantLock)
- 临界区很短(如改一个计数)→ 自旋锁 / 原子变量(AtomicInteger、LongAdder)
- 读多写少(如配置、缓存)→ 读写锁 或 CopyOnWrite、ConcurrentHashMap
- 冲突概率高 → 悲观锁(先加锁再操作)
- 冲突概率低 → 乐观锁(先改再校验,版本号或 CAS)
- 只关心「一个变量写了别人立刻看到」、且无复合操作 → volatile(标志位、DCL 单例)
需要超时、可中断、公平锁或多条件队列 → 用 ReentrantLock 而不是 synchronized。
加锁粒度尽量小,锁住的代码越少越好。
一、核心概念
临界区
临界区(Critical Section):访问共享资源、且在同一时刻只应被一个线程执行的那段代码。
- 加锁就是为了保护临界区,保证不会有两个线程同时进入同一临界区。
- 直观理解:被锁包住的那段代码、需要互斥执行的那一段。
锁类型总览
| 锁 | 拿不到锁时 | 适用场景 | 典型例子 |
|---|---|---|---|
| 互斥锁 | 阻塞,让出 CPU | 临界区较长 | 扣款、写日志 |
| 自旋锁 | 忙等 | 临界区很短 | 引用计数、原子变量 |
| 读写锁 | 读可并发,写独占 | 读多写少 | 配置表、缓存 |
| 悲观锁 | 先加锁再操作 | 冲突概率高 | FOR UPDATE、synchronized |
| 乐观锁 | 先改再校验 | 冲突概率低 | 版本号、CAS、Git |
- 互斥锁:阻塞、让出 CPU(有上下文切换)。
- 自旋锁:忙等、不释放 CPU,一般用 CAS;适合极短临界区。
- 读写锁:读共享、写独占;互斥锁、自旋锁、读写锁都是悲观锁的实现。
- 乐观锁:先改再验证,失败则重试。
二、可重入与不可重入
问题:同一线程能否对同一把锁连续加锁两次(或多次)而不死锁?
典型场景:加锁方法里又调用了另一个也要同一把锁的方法(如 transfer() 里调 deduct(),都用 synchronized(lock))。
| 类型 | 行为 | 典型实现 |
|---|---|---|
| 可重入 | 允许,不死锁 | synchronized、ReentrantLock |
| 不可重入 | 会死锁或报错 | 某些简单 Mutex |
机制简述
可重入锁会记录「当前持有线程 + 持有次数」,同一线程再次拿同一把锁只把次数 +1,退出时 -1,不会死锁;
不可重入锁只记「有没有被占用」,同一线程第二次拿锁会等自己释放 → 死锁。常规开发用可重入锁即可(Java 的 synchronized、ReentrantLock);不可重入锁只在环境只提供该类锁或刻意禁止重入时才会用到,持锁期间不能再拿同一把锁。
三、Java 实现
3.1 synchronized
- 作用:互斥 + 可见性(加锁/解锁时与主存同步)。
- 写法:
synchronized void method()锁当前实例;synchronized static void method()锁类;synchronized (obj) { ... }锁指定对象。 - 注意:多线程要互斥访问同一份数据时,必须用同一把锁(同一对象);锁对象建议
private final Object lock = new Object();。
private final Object lock = new Object();
public void add() {
synchronized (lock) {
count++;
}
}
3.2 ReentrantLock
- 作用:与 synchronized 一样的可重入互斥锁,显式调用
lock()/unlock(),需在try-finally里解锁,避免异常导致锁未释放。 - 特点:
tryLock()、tryLock(超时)、lockInterruptibly();可设公平锁new ReentrantLock(true);可多个Condition分开等待/唤醒。synchronized 拿不到就阻塞,无法超时或中断。 - 何时用:需要可超时、可中断、公平锁或多条件队列时用 ReentrantLock;一般互斥用 synchronized 更简单。
private final ReentrantLock lock = new ReentrantLock();
public void doSomething() {
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// 临界区
} finally {
lock.unlock();
}
} else {
// 拿不到锁的处理
}
}
3.3 volatile
- 作用:可见性 + 有序性(禁止重排)。不保证原子性,
count++仍会丢更新,需用 synchronized 或 AtomicXxx。 - 适用:状态标志位(一写多读)、一次性发布、双重检查锁单例的
instance。 - 加和不加:不加可能读旧值(可见性缺失)或看到半初始化对象(重排);加了则保证该变量的可见性 + 有序性。
private volatile boolean running = true;
// DCL 单例:instance 必须 volatile
private static volatile MyClass instance;
static MyClass getInstance() {
if (instance == null) {
synchronized (MyClass.class) {
if (instance == null) instance = new MyClass();
}
}
return instance;
}
3.4 AtomicXxx
- 作用:对单个变量做原子读改写,无需加锁。底层用 CAS(Compare-And-Swap):先读当前值,改完后用 CAS 写回,只有当前值未被别人改过才成功,否则自旋重试。属于自旋锁/乐观锁的一种实现。
- 常见类:
AtomicInteger、AtomicLong、AtomicReference、LongAdder等。高并发计数推荐 LongAdder(内部多段累加,减少竞争)。 - 何时用:单变量计数、状态位、引用替换(如懒加载)。临界区极短、只改一个变量时用;临界区较长或多行逻辑用 synchronized。
- 和 synchronized:AtomicXxx 无阻塞、无上下文切换;synchronized 有互斥、可保护多行。
private final AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();
count.compareAndSet(expect, update);
private final LongAdder total = new LongAdder();
total.add(1);
private final AtomicReference<Heavy> ref = new AtomicReference<>();
ref.compareAndSet(null, new Heavy());
3.5 对比与选型
| 维度 | synchronized | volatile | AtomicXxx | ReentrantLock | ReentrantReadWriteLock |
|---|---|---|---|---|---|
| 互斥 | 有 | 无 | 无(CAS) | 有 | 写独占,读可并发 |
| 可见性 | 有 | 有 | 有 | 有 | 有 |
| 原子性 | 有(块内) | 无(i++ 不安全) | 单变量 | 有 | 有 |
| 形式 | 关键字 | 关键字 | API | API,显式 lock/unlock | readLock / writeLock |
何时用谁:一般互斥 → synchronized;单变量原子、高并发计数 → AtomicXxx;单变量可见、无复合操作 → volatile;要超时/中断/公平/多条件 → ReentrantLock;读多写少 → ReentrantReadWriteLock。
3.6 代码示例汇总
// 互斥
public synchronized void deduct(int amount) {
if (balance >= amount) balance -= amount;
}
lock.lock(); try { ... } finally { lock.unlock(); }
// 原子
refCount.incrementAndGet();
// 读写锁
rwLock.readLock().lock(); try { return cache.get(key); } finally { rwLock.readLock().unlock(); }
rwLock.writeLock().lock(); try { cache.put(key, cfg); } finally { rwLock.writeLock().unlock(); }
// 悲观锁(DB):SELECT ... FOR UPDATE 再 UPDATE
// 乐观锁:UPDATE ... WHERE version=? ,影响行数=1 才成功,否则重试
四、其他线程安全方式
除锁之外,还可通过以下方式实现线程安全(可组合使用):
| 方式 | 说明 | 典型用法 |
|---|---|---|
| AtomicXxx | CAS 无锁,单变量 | 计数、状态位、LongAdder |
| 线程安全集合 | 内部同步或无锁 | ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue |
| 不可变对象 | 无共享可变状态 | final、只读 data class |
| ThreadLocal | 每线程一份 | 连接、请求上下文、SimpleDateFormat |
| 并发工具 | 协调步调 | CountDownLatch、CyclicBarrier、Semaphore |
| Kotlin Mutex | 协程互斥,挂起 | mutex.withLock { } |
| 单线程 Dispatcher | 协程单线程 | 串行更新共享状态 |
| Channel / Flow | 通信代替共享可变 | 生产者-消费者、事件流 |
选择思路:单变量读改写 → AtomicXxx 或 volatile(仅标志位);一段代码互斥 → synchronized / ReentrantLock / Mutex;读多写少 → 读写锁或 CopyOnWrite、ConcurrentHashMap;能无共享 → 不可变、ThreadLocal、单线程 Dispatcher;协作 → CountDownLatch、Semaphore、Channel 等。