JAVA多线程学习笔记(二)

140 阅读4分钟

二、线程同步与锁详解


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,通过monitorentermonitorexit字节码指令实现锁的获取与释放。
  • 锁升级(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
特性synchronizedLock
锁获取方式自动获取与释放手动调用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)避免无限等待。
  • 检测与恢复:定期检查死锁并释放资源(如数据库死锁处理)。

关键总结

  1. 线程安全的核心是同步:确保共享资源的原子性、可见性和有序性。
  2. 锁的选择
    • 简单场景:优先使用synchronized
    • 复杂需求:使用ReentrantLock(如可中断锁、公平锁)。
  3. 避免死锁:设计时遵循加锁顺序,使用工具(如jstack)分析线程转储。
  4. 线程通信:合理使用wait()/notify()Condition协调线程协作。