锁的应用场景

多线程访问共享资源时需要加锁,避免数据错乱。不同锁的代价和适用场景不同,选对了才能既有正确性又有性能。


选型速查(结论在前)

先看场景,再选手段:

需要超时、可中断、公平锁或多条件队列 → 用 ReentrantLock 而不是 synchronized。

加锁粒度尽量小,锁住的代码越少越好。


一、核心概念

临界区

临界区(Critical Section):访问共享资源、且在同一时刻只应被一个线程执行的那段代码。

锁类型总览

拿不到锁时适用场景典型例子
互斥锁阻塞,让出 CPU临界区较长扣款、写日志
自旋锁忙等临界区很短引用计数、原子变量
读写锁读可并发,写独占读多写少配置表、缓存
悲观锁先加锁再操作冲突概率高FOR UPDATE、synchronized
乐观锁先改再校验冲突概率低版本号、CAS、Git

二、可重入与不可重入

问题:同一线程能否对同一把锁连续加锁两次(或多次)而不死锁?

典型场景:加锁方法里又调用了另一个也要同一把锁的方法(如 transfer() 里调 deduct(),都用 synchronized(lock))。

类型行为典型实现
可重入允许,不死锁synchronized、ReentrantLock
不可重入会死锁或报错某些简单 Mutex

机制简述

可重入锁会记录「当前持有线程 + 持有次数」,同一线程再次拿同一把锁只把次数 +1,退出时 -1,不会死锁;

不可重入锁只记「有没有被占用」,同一线程第二次拿锁会等自己释放 → 死锁。常规开发用可重入锁即可(Java 的 synchronizedReentrantLock);不可重入锁只在环境只提供该类锁或刻意禁止重入时才会用到,持锁期间不能再拿同一把锁。


三、Java 实现

3.1 synchronized

private final Object lock = new Object();
public void add() {
    synchronized (lock) {
        count++;
    }
}

3.2 ReentrantLock

private final ReentrantLock lock = new ReentrantLock();

public void doSomething() {
    if (lock.tryLock(3, TimeUnit.SECONDS)) {
        try {
            // 临界区
        } finally {
            lock.unlock();
        }
    } else {
        // 拿不到锁的处理
    }
}

3.3 volatile

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

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 对比与选型

维度synchronizedvolatileAtomicXxxReentrantLockReentrantReadWriteLock
互斥无(CAS)写独占,读可并发
可见性
原子性有(块内)无(i++ 不安全)单变量
形式关键字关键字APIAPI,显式 lock/unlockreadLock / 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 才成功,否则重试

四、其他线程安全方式

除锁之外,还可通过以下方式实现线程安全(可组合使用):

方式说明典型用法
AtomicXxxCAS 无锁,单变量计数、状态位、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 等。