Skip to content

第24课:线程同步

🎯 学习目标

  • 理解线程安全问题
  • 掌握 synchronized 关键字
  • 掌握 Lock 接口
  • 理解死锁和避免
  • 掌握线程通信

📖 一、线程安全问题

1. 什么是线程安全?

多个线程同时访问共享资源时,不会出现数据不一致的问题。

java
// 线程不安全的计数器
public class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // 不是原子操作
    }
    
    public int getCount() {
        return count;
    }
}

// 测试
Counter counter = new Counter();

// 创建10个线程,每个线程增加1000次
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
            counter.increment();
        }
    }).start();
}

Thread.sleep(2000);
System.out.println(counter.getCount());  // 期望10000,实际可能是9876

2. 为什么会出现问题?

count++ 不是原子操作,实际上是三步:

  1. 读取 count
  2. 计算 count + 1
  3. 写回 count

多个线程可能同时执行这三步,导致数据丢失。


📖 二、synchronized 关键字

1. 同步方法

java
public class Counter {
    private int count = 0;
    
    // 同步方法
    public synchronized void increment() {
        count++;
    }
    
    public synchronized int getCount() {
        return count;
    }
}

2. 同步代码块

java
public class Counter {
    private int count = 0;
    private Object lock = new Object();
    
    public void increment() {
        synchronized (lock) {  // 同步代码块
            count++;
        }
    }
}

3. synchronized 的锁对象

java
// 1. 实例方法 → 锁是 this
public synchronized void method() { }

// 等价于
public void method() {
    synchronized (this) { }
}

// 2. 静态方法 → 锁是 Class 对象
public static synchronized void method() { }

// 等价于
public static void method() {
    synchronized (Counter.class) { }
}

// 3. 代码块 → 锁是指定对象
synchronized (lock) { }

📖 三、Lock 接口

1. ReentrantLock

java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();  // 加锁
        try {
            count++;
        } finally {
            lock.unlock();  // 解锁(必须在 finally 中)
        }
    }
}

2. ReentrantLock vs synchronized

特性synchronizedReentrantLock
使用自动加锁解锁手动加锁解锁
公平性非公平可选公平/非公平
可中断
尝试获取锁
性能
java
Lock lock = new ReentrantLock();

// 尝试获取锁(不阻塞)
if (lock.tryLock()) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
} else {
    System.out.println("无法获取锁");
}

// 尝试获取锁(等待一段时间)
if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
}

📖 四、死锁

1. 什么是死锁?

两个或多个线程互相持有对方需要的锁,导致都无法继续执行。

java
// 死锁示例
public class DeadlockDemo {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    
    public static void main(String[] args) {
        // 线程1:先锁1后锁2
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread1: holding lock1");
                try { Thread.sleep(100); } catch (Exception e) {}
                
                synchronized (lock2) {
                    System.out.println("Thread1: holding lock2");
                }
            }
        }).start();
        
        // 线程2:先锁2后锁1
        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread2: holding lock2");
                try { Thread.sleep(100); } catch (Exception e) {}
                
                synchronized (lock1) {
                    System.out.println("Thread2: holding lock1");
                }
            }
        }).start();
    }
}

2. 避免死锁

  1. 按顺序获取锁
java
// 统一按 lock1 → lock2 的顺序获取
synchronized (lock1) {
    synchronized (lock2) {
        // ...
    }
}
  1. 使用 tryLock
java
if (lock1.tryLock()) {
    try {
        if (lock2.tryLock()) {
            try {
                // 业务逻辑
            } finally {
                lock2.unlock();
            }
        }
    } finally {
        lock1.unlock();
    }
}

📖 五、线程通信

1. wait/notify

java
public class Producer {
    private List<Integer> queue = new ArrayList<>();
    private int maxSize = 5;
    
    // 生产者
    public synchronized void produce() throws InterruptedException {
        while (queue.size() == maxSize) {
            wait();  // 队列满了,等待
        }
        
        int value = new Random().nextInt(100);
        queue.add(value);
        System.out.println("生产: " + value);
        
        notify();  // 唤醒消费者
    }
    
    // 消费者
    public synchronized void consume() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();  // 队列空了,等待
        }
        
        int value = queue.remove(0);
        System.out.println("消费: " + value);
        
        notify();  // 唤醒生产者
    }
}

2. Condition

java
import java.util.concurrent.locks.*;

public class BoundedQueue {
    private Lock lock = new ReentrantLock();
    private Condition notFull = lock.newCondition();
    private Condition notEmpty = lock.newCondition();
    private Object[] items = new Object[10];
    private int count, putIndex, takeIndex;
    
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length) {
                notFull.await();  // 队列满,等待
            }
            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal();  // 唤醒消费者
        } finally {
            lock.unlock();
        }
    }
    
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0) {
                notEmpty.await();  // 队列空,等待
            }
            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            notFull.signal();  // 唤醒生产者
            return x;
        } finally {
            lock.unlock();
        }
    }
}

📖 六、volatile 关键字

1. 可见性问题

java
// 线程1修改了 flag,但线程2可能看不到
private boolean flag = false;

// 线程1
public void writer() {
    flag = true;
}

// 线程2
public void reader() {
    while (!flag) {
        // 可能无限循环
    }
}

2. volatile 保证可见性

java
private volatile boolean flag = false;  // 加 volatile

// 现在线程2能立即看到 flag 的变化

3. volatile 的特点

  • ✅ 保证可见性
  • ✅ 禁止指令重排序
  • ❌ 不保证原子性
java
// ❌ volatile 不能保证 count++ 的原子性
private volatile int count = 0;

public void increment() {
    count++;  // 仍然不是线程安全的
}

// ✅ 需要配合 synchronized 或 AtomicInteger

⚠️ 常见陷阱

陷阱1:锁对象不一致

多个线程必须竞争同一把锁才有同步效果。锁不同对象等于没有互斥。

陷阱2:锁粒度过大

把耗时 IO、网络调用放在 synchronized 内,会放大阻塞,降低吞吐。

陷阱3:volatile 当锁用

volatile 保证可见性和禁止部分重排序,但不保证复合操作原子性,count++ 仍然不安全。

陷阱4:死锁后只看代码不看线程栈

死锁排查应使用 jstack 或线程 dump,看线程持有锁和等待锁的关系。


🆚 Java vs C 对比

特性C pthreadJava
互斥锁pthread_mutexsynchronized / Lock
条件变量pthread_condwait/notify / Condition
可见性volatile/原子/内存屏障volatile + JMM
死锁排查gdb/pstackjstack/thread dump

对 C 程序员来说,synchronized 可以理解为语言级互斥锁,但它还包含进入和退出临界区的内存可见性语义。


💡 最佳实践

  1. 尽量减少同步范围
java
// ❌ 不好:整个方法同步
public synchronized void method() {
    // 100行代码...
}

// ✅ 好:只同步必要部分
public void method() {
    // 非同步代码
    synchronized (lock) {
        // 同步代码
    }
    // 非同步代码
}
  1. 避免在锁内部做耗时操作
java
// ❌ 不好
synchronized (lock) {
    Thread.sleep(1000);  // 持有锁时睡眠
}

// ✅ 好
synchronized (lock) {
    // 快速操作
}
  1. 优先使用并发工具类
java
// ✅ 使用线程安全的集合
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

// ✅ 使用原子类
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();

📝 练习

完成 练习/Ex24_Thread_Sync.java

  1. synchronized 使用
  2. Lock 接口
  3. 死锁演示和避免
  4. 生产者消费者
  5. volatile 使用
  6. 综合:线程安全的银行账户

🎓 下一步

  • 第26课:线程池 - Executor、ThreadPoolExecutor