多线程编程进阶:死锁、内存可见性与线程协调

22 阅读7分钟

1. 线程死锁

1.1 构成死锁的场景

a) 一个线程一把锁 - Java的可重入机制

java

public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    
    Thread t1 = new Thread(() -> {
        synchronized (locker) {
            synchronized (locker) {  // 同一个锁,可重入
                System.out.println("成功进入嵌套同步块");
            }
        }
    });
    
    t1.start();
    t1.join();
    System.out.println("程序正常结束");
}

关键点:Java的synchronized锁是可重入的,同一个线程可以多次获取同一把锁

b) 两个线程两把锁 - 经典死锁场景

java

public class DeadlockDemo {
    public static void main(String[] args) throws InterruptedException {
        Object soySauce = new Object();  // 酱油锁
        Object vinegar = new Object();   // 醋锁
        
        Thread xiaoming = new Thread(() -> {
            synchronized (soySauce) {
                System.out.println("小明拿到了酱油");
                try {
                    Thread.sleep(1000);  // 模拟处理时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (vinegar) {
                    System.out.println("小明拿到了醋和酱油");
                }
            }
        }, "小明");
        
        Thread xiaoliang = new Thread(() -> {
            synchronized (vinegar) {
                System.out.println("小亮拿到了醋");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (soySauce) {
                    System.out.println("小亮拿到了酱油和醋");
                }
            }
        }, "小亮");
        
        xiaoming.start();
        xiaoliang.start();
        
        xiaoming.join();
        xiaoliang.join();
        System.out.println("程序结束");  // 这行可能永远不会执行
    }
}

c) N个线程M把锁 - 哲学家就餐问题

java

public class PhilosopherDining {
    private static final int PHILOSOPHER_COUNT = 5;
    private static final Object[] chopsticks = new Object[PHILOSOPHER_COUNT];
    
    static {
        for (int i = 0; i < PHILOSOPHER_COUNT; i++) {
            chopsticks[i] = new Object();
        }
    }
    
    public static void main(String[] args) {
        for (int i = 0; i < PHILOSOPHER_COUNT; i++) {
            final int philosopherId = i;
            new Thread(() -> {
                Object leftChopstick = chopsticks[philosopherId];
                Object rightChopstick = chopsticks[(philosopherId + 1) % PHILOSOPHER_COUNT];
                
                while (true) {
                    synchronized (leftChopstick) {
                        synchronized (rightChopstick) {
                            System.out.println("哲学家 " + philosopherId + " 开始用餐");
                            try {
                                Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    
                    // 思考
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "哲学家-" + i).start();
        }
    }
}

1.2 死锁的四个必要条件

  1. 互斥 - 资源不能被共享,只能独占
  2. 不可剥夺 - 资源只能由持有者释放,不能被强制抢占
  3. 请求和保持 - 持有资源的同时请求其他资源
  4. 循环等待 - 等待关系形成环状

1.3 如何避免死锁

a) 打破请求和保持 - 使用并列锁

java

public class AvoidDeadlockByParallel {
    public static void main(String[] args) {
        Object soySauce = new Object();
        Object vinegar = new Object();
        
        Thread xiaoming = new Thread(() -> {
            // 先释放酱油锁,再重新获取两个锁
            synchronized (soySauce) {
                System.out.println("小明拿到酱油");
            }
            
            try {
                Thread.sleep(100);  // 给其他线程机会
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            // 并列获取两个锁
            synchronized (soySauce) {
                synchronized (vinegar) {
                    System.out.println("小明同时拿到酱油和醋");
                }
            }
        }, "小明");
        
        Thread xiaoliang = new Thread(() -> {
            synchronized (vinegar) {
                System.out.println("小亮拿到醋");
            }
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized (soySauce) {
                synchronized (vinegar) {
                    System.out.println("小亮同时拿到酱油和醋");
                }
            }
        }, "小亮");
        
        xiaoming.start();
        xiaoliang.start();
    }
}

b) 打破循环等待 - 统一加锁顺序

java

public class AvoidDeadlockByOrder {
    public static void main(String[] args) {
        Object soySauce = new Object();
        Object vinegar = new Object();
        
        // 定义资源顺序:先酱油后醋
        Thread xiaoming = new Thread(() -> {
            acquireLocksInOrder(soySauce, vinegar, "小明");
        }, "小明");
        
        Thread xiaoliang = new Thread(() -> {
            acquireLocksInOrder(soySauce, vinegar, "小亮");
        }, "小亮");
        
        xiaoming.start();
        xiaoliang.start();
    }
    
    private static void acquireLocksInOrder(Object first, Object second, String name) {
        synchronized (first) {
            System.out.println(name + "拿到第一个资源");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            synchronized (second) {
                System.out.println(name + "同时拿到两个资源");
            }
        }
    }
}

c) 使用超时机制

java

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

public class TimeoutDeadlockAvoidance {
    private static final Lock soySauce = new ReentrantLock();
    private static final Lock vinegar = new ReentrantLock();
    
    public static void main(String[] args) {
        Thread xiaoming = new Thread(() -> {
            try {
                if (soySauce.tryLock(1, TimeUnit.SECONDS)) {
                    try {
                        System.out.println("小明拿到酱油");
                        Thread.sleep(1000);
                        
                        if (vinegar.tryLock(1, TimeUnit.SECONDS)) {
                            try {
                                System.out.println("小明拿到醋和酱油");
                            } finally {
                                vinegar.unlock();
                            }
                        } else {
                            System.out.println("小明获取醋超时");
                        }
                    } finally {
                        soySauce.unlock();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "小明");
        
        Thread xiaoliang = new Thread(() -> {
            // 类似的超时逻辑
        }, "小亮");
        
        xiaoming.start();
        xiaoliang.start();
    }
}

2. 内存可见性问题

可见性问题示例

java

public class VisibilityProblem {
    static int counter = 0;
    static boolean running = true;
    
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) {
                // 空循环 - 编译器可能优化为只读取一次running
            }
            System.out.println("Worker thread finished");
        });
        
        worker.start();
        Thread.sleep(1000);
        
        // 主线程修改标志位
        running = false;
        System.out.println("Main thread set running to false");
        
        worker.join();
        System.out.println("Main thread finished");
    }
}

问题原因

  • 编译器优化:将循环条件缓存到寄存器
  • CPU缓存:线程在自己的缓存中读取数据,不感知主内存变化
  • 指令重排序:编译器/CPU可能重新安排指令执行顺序

3. volatile 关键字

3.1 volatile 保证内存可见性

java

public class VolatileVisibility {
    static volatile boolean running = true;  // 添加volatile
    
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            while (running) {
                // 现在能正确感知running的变化
            }
            System.out.println("Worker thread finished");
        });
        
        worker.start();
        Thread.sleep(1000);
        
        running = false;
        System.out.println("Main thread set running to false");
        
        worker.join();
        System.out.println("Main thread finished");
    }
}

3.2 volatile 不保证原子性

java

public class VolatileNonAtomic {
    static volatile int count = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count++;  // 这不是原子操作
            }
        });
        
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                count++;
            }
        });
        
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        
        System.out.println("Final count: " + count);  // 结果可能小于200000
    }
}

volatile 的正确使用场景

java

public class VolatileCorrectUsage {
    private volatile boolean shutdownRequested = false;
    
    public void shutdown() {
        shutdownRequested = true;  // 写操作
    }
    
    public void doWork() {
        while (!shutdownRequested) {  // 读操作
            // 执行工作
        }
    }
    
    // 状态标志位
    private volatile boolean initialized = false;
    
    public void initialize() {
        if (!initialized) {
            synchronized (this) {
                if (!initialized) {
                    // 初始化代码
                    initialized = true;  // 发布初始化完成
                }
            }
        }
    }
}

4. wait 和 notify 机制

4.1 wait() 方法基础

java

public class WaitBasic {
    public static void main(String[] args) {
        Object locker = new Object();
        
        Thread waiter = new Thread(() -> {
            synchronized (locker) {
                System.out.println("线程进入同步块,开始等待");
                try {
                    locker.wait();  // 释放锁并等待
                    System.out.println("线程被唤醒,继续执行");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        waiter.start();
        
        // 主线程3秒后唤醒等待线程
        try {
            Thread.sleep(3000);
            synchronized (locker) {
                locker.notify();
                System.out.println("主线程发送通知");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.2 带超时的wait()

java

public class WaitWithTimeout {
    public static void main(String[] args) {
        Object locker = new Object();
        
        Thread waiter = new Thread(() -> {
            synchronized (locker) {
                System.out.println("开始等待,超时时间2秒");
                try {
                    long start = System.currentTimeMillis();
                    locker.wait(2000);  // 最多等待2秒
                    long end = System.currentTimeMillis();
                    System.out.println("等待结束,耗时: " + (end - start) + "ms");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        
        waiter.start();
    }
}

4.3 notify() 随机唤醒

java

public class NotifyRandom {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        
        for (int i = 1; i <= 3; i++) {
            final int threadId = i;
            new Thread(() -> {
                synchronized (locker) {
                    try {
                        System.out.println("线程" + threadId + "开始等待");
                        locker.wait();
                        System.out.println("线程" + threadId + "被唤醒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        
        Thread.sleep(1000);
        
        synchronized (locker) {
            System.out.println("主线程发送单个notify");
            locker.notify();  // 随机唤醒一个线程
        }
        
        Thread.sleep(1000);
        
        synchronized (locker) {
            System.out.println("主线程再发送一个notify");
            locker.notify();  // 再唤醒一个
        }
    }
}

4.4 notifyAll() 唤醒所有

java

public class NotifyAllDemo {
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        
        for (int i = 1; i <= 3; i++) {
            final int threadId = i;
            new Thread(() -> {
                synchronized (locker) {
                    try {
                        System.out.println("线程" + threadId + "开始等待");
                        locker.wait();
                        System.out.println("线程" + threadId + "被唤醒");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        
        Thread.sleep(1000);
        
        synchronized (locker) {
            System.out.println("主线程发送notifyAll");
            locker.notifyAll();  // 唤醒所有等待线程
        }
    }
}

4.5 生产者和消费者模式

java

public class ProducerConsumer {
    private static final int BUFFER_SIZE = 5;
    private static final Queue<Integer> buffer = new LinkedList<>();
    private static final Object lock = new Object();
    
    static class Producer extends Thread {
        public void run() {
            int value = 0;
            while (true) {
                synchronized (lock) {
                    // 缓冲区满时等待
                    while (buffer.size() == BUFFER_SIZE) {
                        try {
                            System.out.println("缓冲区满,生产者等待");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    
                    // 生产数据
                    buffer.offer(value);
                    System.out.println("生产: " + value);
                    value++;
                    
                    // 通知消费者
                    lock.notifyAll();
                }
                
                try {
                    Thread.sleep(500);  // 模拟生产时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    static class Consumer extends Thread {
        public void run() {
            while (true) {
                synchronized (lock) {
                    // 缓冲区空时等待
                    while (buffer.isEmpty()) {
                        try {
                            System.out.println("缓冲区空,消费者等待");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    
                    // 消费数据
                    int value = buffer.poll();
                    System.out.println("消费: " + value);
                    
                    // 通知生产者
                    lock.notifyAll();
                }
                
                try {
                    Thread.sleep(1000);  // 模拟消费时间
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public static void main(String[] args) {
        new Producer().start();
        new Consumer().start();
    }
}

4.6 wait 和 sleep 对比总结

特性wait()sleep()
所属类ObjectThread
锁释放释放锁不释放锁
使用条件必须在同步块内任何地方
唤醒方式notify()/notifyAll()/超时超时/interrupt()
异常处理需要处理InterruptedException需要处理InterruptedException
用途线程间协调单纯暂停执行

java

public class WaitVsSleep {
    private static final Object lock = new Object();
    
    public static void main(String[] args) throws InterruptedException {
        // wait示例 - 会释放锁
        Thread waitThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("waitThread获取锁,开始wait");
                try {
                    lock.wait(2000);  // 释放锁,等待2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("waitThread被唤醒");
            }
        });
        
        // sleep示例 - 不会释放锁
        Thread sleepThread = new Thread(() -> {
            synchronized (lock) {
                System.out.println("sleepThread获取锁,开始sleep");
                try {
                    Thread.sleep(2000);  // 不释放锁,休眠2秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("sleepThread醒来");
            }
        });
        
        waitThread.start();
        Thread.sleep(100);  // 确保waitThread先启动
        sleepThread.start();
    }
}

总结

关键要点

  1. 死锁预防

    • 统一加锁顺序
    • 使用超时机制
    • 避免嵌套锁
  2. 内存可见性

    • volatile保证可见性,不保证原子性
    • 适合状态标志位场景
  3. 线程协调

    • wait/notify用于线程间通信
    • 总是要在循环中检查条件
    • 理解wait会释放锁,sleep不会
  4. 最佳实践

    • 尽量减少锁的持有时间
    • 使用更高级的并发工具(如Lock、Condition)
    • 合理设计程序结构,避免复杂的锁依赖

掌握这些多线程核心概念,能够帮助你编写出更安全、高效的多线程程序!