Appearance
第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,实际可能是98762. 为什么会出现问题?
count++ 不是原子操作,实际上是三步:
- 读取 count
- 计算 count + 1
- 写回 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
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 使用 | 自动加锁解锁 | 手动加锁解锁 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 可中断 | ❌ | ✅ |
| 尝试获取锁 | ❌ | ✅ |
| 性能 | 好 | 好 |
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. 避免死锁
- 按顺序获取锁
java
// 统一按 lock1 → lock2 的顺序获取
synchronized (lock1) {
synchronized (lock2) {
// ...
}
}- 使用 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 pthread | Java |
|---|---|---|
| 互斥锁 | pthread_mutex | synchronized / Lock |
| 条件变量 | pthread_cond | wait/notify / Condition |
| 可见性 | volatile/原子/内存屏障 | volatile + JMM |
| 死锁排查 | gdb/pstack | jstack/thread dump |
对 C 程序员来说,synchronized 可以理解为语言级互斥锁,但它还包含进入和退出临界区的内存可见性语义。
💡 最佳实践
- 尽量减少同步范围
java
// ❌ 不好:整个方法同步
public synchronized void method() {
// 100行代码...
}
// ✅ 好:只同步必要部分
public void method() {
// 非同步代码
synchronized (lock) {
// 同步代码
}
// 非同步代码
}- 避免在锁内部做耗时操作
java
// ❌ 不好
synchronized (lock) {
Thread.sleep(1000); // 持有锁时睡眠
}
// ✅ 好
synchronized (lock) {
// 快速操作
}- 优先使用并发工具类
java
// ✅ 使用线程安全的集合
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// ✅ 使用原子类
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();📝 练习
完成 练习/Ex24_Thread_Sync.java:
- synchronized 使用
- Lock 接口
- 死锁演示和避免
- 生产者消费者
- volatile 使用
- 综合:线程安全的银行账户
🎓 下一步
- 第26课:线程池 - Executor、ThreadPoolExecutor