二、线程同步与锁详解
1. 线程安全问题
1.1 什么是线程安全?
当多个线程同时访问共享资源(如变量、文件、数据库连接)时,如果没有正确的同步,可能导致数据不一致或程序行为异常。
1.2 典型问题:竞态条件(Race Condition)
- 示例:两个线程同时修改一个计数器。
public class UnsafeCounter { private int count = 0; public void increment() { count++; // 非原子操作 } public int getCount() { return count; } }- 问题分析:
count++实际上分为三步(读取 → 修改 → 写入),多线程交叉执行会导致结果不可预测。
- 问题分析:
1.3 线程不安全的表现
- 数据不一致:最终结果小于预期值。
- 脏读:读取到中间状态的数据。
- 死锁/活锁:线程相互等待资源。
2. 同步机制
2.1 synchronized 关键字
- 作用:确保同一时间只有一个线程执行代码块或方法。
- 使用方式:
- 同步代码块(推荐,锁粒度更细):
private final Object lock = new Object(); // 锁对象 public void increment() { synchronized (lock) { // 获取锁 count++; } // 释放锁 } - 同步方法(锁对象为
this或类对象):public synchronized void increment() { count++; }- 静态方法锁的是
Class对象:public static synchronized void foo() { ... }
- 静态方法锁的是
- 同步代码块(推荐,锁粒度更细):
2.2 synchronized 底层原理
- Monitor(管程):每个Java对象关联一个Monitor,通过
monitorenter和monitorexit字节码指令实现锁的获取与释放。 - 锁升级(JDK6优化):
- 无锁 → 偏向锁 → 轻量级锁(CAS自旋) → 重量级锁(操作系统互斥量)。
2.3 synchronized 的缺点
- 不可中断:线程获取锁失败会一直阻塞。
- 非公平锁:无法按申请锁的顺序分配。
- 锁粒度固定:只能锁整个方法或代码块。
3. Lock 接口
3.1 为什么需要 Lock?
- 更灵活的控制:支持尝试锁(
tryLock)、超时锁(lockInterruptibly)、公平锁等。 - 分离锁的获取与释放:显式调用
lock()和unlock()。
3.2 ReentrantLock 使用
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须确保释放锁
}
}
- 特性:
- 可重入:同一线程可多次获取锁。
- 公平性:构造函数可指定公平锁(
new ReentrantLock(true))。 - 条件变量:通过
newCondition()创建多个等待队列。
3.3 Lock vs synchronized
| 特性 | synchronized | Lock |
|---|---|---|
| 锁获取方式 | 自动获取与释放 | 手动调用lock()/unlock() |
| 可中断性 | 不支持 | 支持lockInterruptibly() |
| 公平锁 | 不支持 | 支持 |
| 条件变量 | 单一wait()/notify() | 多个Condition |
| 性能 | JDK6后优化较好 | 高竞争场景更灵活 |
4. volatile 关键字
4.1 作用
- 可见性:确保变量的修改对所有线程立即可见。
- 禁止指令重排序:防止JVM和CPU优化打乱代码执行顺序。
4.2 适用场景
- 状态标志:如
volatile boolean running = true;。 - 单次写入的共享变量:变量赋值不依赖当前值(如配置加载)。
4.3 局限性
- 不保证原子性:例如
volatile int count = 0; count++仍不安全。
5. 线程通信
5.1 wait() / notify()
- 使用条件:必须在
synchronized代码块中调用。 - 经典示例:生产者-消费者模型
class Buffer { private Queue<Integer> queue = new LinkedList<>(); private int capacity = 10; public synchronized void produce(int value) throws InterruptedException { while (queue.size() == capacity) { wait(); // 队列满,等待消费 } queue.add(value); notifyAll(); // 唤醒消费者 } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) { wait(); // 队列空,等待生产 } int value = queue.poll(); notifyAll(); // 唤醒生产者 return value; } }
5.2 Condition 接口(配合 Lock 使用)
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 队列未满条件
private final Condition notEmpty = lock.newCondition(); // 队列非空条件
public void produce(int value) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 等待队列不满
}
queue.add(value);
notEmpty.signal(); // 通知队列非空
} finally {
lock.unlock();
}
}
- 优势:可创建多个条件变量,更精细控制线程唤醒。
6. 死锁与避免
6.1 死锁条件
- 互斥:资源只能被一个线程持有。
- 持有并等待:线程持有资源并等待其他资源。
- 不可剥夺:资源只能由持有者释放。
- 循环等待:多个线程形成环形等待链。
6.2 示例:死锁模拟
Object lockA = new Object();
Object lockB = new Object();
Thread t1 = new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockB) { // 等待lockB
System.out.println("t1执行");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
try { Thread.sleep(100); } catch (Exception e) {}
synchronized (lockA) { // 等待lockA
System.out.println("t2执行");
}
}
});
t1.start();
t2.start();
6.3 死锁避免策略
- 顺序加锁:所有线程按相同顺序获取锁。
- 超时放弃:使用
tryLock(timeout)避免无限等待。 - 检测与恢复:定期检查死锁并释放资源(如数据库死锁处理)。
关键总结
- 线程安全的核心是同步:确保共享资源的原子性、可见性和有序性。
- 锁的选择:
- 简单场景:优先使用
synchronized。 - 复杂需求:使用
ReentrantLock(如可中断锁、公平锁)。
- 简单场景:优先使用
- 避免死锁:设计时遵循加锁顺序,使用工具(如
jstack)分析线程转储。 - 线程通信:合理使用
wait()/notify()或Condition协调线程协作。