一、为什么需要了解Java内存模型

想象你在厨房和室友一起做饭。如果两个人同时往锅里加盐,最后菜可能咸得没法吃。Java程序也一样——当多个线程同时操作同一个数据时,如果没有明确的规则,结果就会混乱不堪。Java内存模型(JMM)就是解决这个问题的"厨房操作手册",它规定了线程如何安全地"共享食材"(内存数据)。

二、内存模型的底层原理

可以把JMM想象成一个快递仓库:

  1. 主内存相当于中央仓库,存放所有原始数据
  2. 工作内存是每个线程的私人储物柜,存放需要处理的临时数据

关键规则:

  • 线程不能直接修改主内存,必须先把数据"快递"到自己的工作内存
  • 修改完成后,需要特殊指令才能把数据"寄回"主内存
// 技术栈:Java 11
public class MemoryVisibilityDemo {
    private static /*volatile*/ boolean ready = false; // 试试去掉volatile会发生什么
    private static int number;
    
    public static void main(String[] args) {
        new Thread(() -> {
            while(!ready); // 循环等待
            System.out.println(number); // 可能看到0或42
        }).start();
        
        number = 42;
        ready = true; // 没有volatile时,这个修改可能对其他线程不可见
    }
}

三、并发编程的三大难题

1. 可见性问题

就像两个快递员同时更新库存表,但彼此看不到对方的修改。解决方案:

// 正确姿势:使用volatile
private volatile boolean flag = true;

// 或者使用synchronized
private synchronized void updateFlag() {
    flag = !flag;
}

2. 原子性问题

类似于银行转账时ATM突然故障,导致钱扣了但对方没收到。示例:

// 错误示范:非原子操作
private static int counter = 0;

public static void main(String[] args) throws InterruptedException {
    Runnable task = () -> {
        for (int i = 0; i < 10000; i++) {
            counter++; // 这不是原子操作!
        }
    };
    
    Thread t1 = new Thread(task);
    Thread t2 = new Thread(task);
    t1.start(); t2.start();
    t1.join(); t2.join();
    System.out.println(counter); // 通常小于20000
}

// 修复方案1:使用AtomicInteger
private static AtomicInteger atomicCounter = new AtomicInteger(0);

// 修复方案2:使用synchronized
private synchronized static void safeIncrement() {
    counter++;
}

3. 有序性问题

编译器优化可能会像不靠谱的厨师,擅自调整做菜步骤。解决方案:

// 使用happens-before规则
class InstructionReorder {
    int x = 0;
    boolean initialized = false;
    
    void writer() {
        x = 42;                   // 1
        initialized = true;        // 2
    }
    
    void reader() {
        if (initialized) {         // 3
            System.out.println(x); // 可能看到0
        }
    }
}

四、实战中的正确姿势

1. volatile的适用场景

适合做状态标志位,但要注意:

  • 不能保证复合操作的原子性
  • 会禁止指令重排序
// 典型用法:优雅终止线程
class Worker implements Runnable {
    private volatile boolean running = true;
    
    public void stop() { running = false; }
    
    @Override
    public void run() {
        while(running) {
            // 执行任务...
        }
    }
}

2. 锁的进阶用法

展示ReentrantLock比synchronized更灵活的特性:

import java.util.concurrent.locks.ReentrantLock;

class TicketSystem {
    private final ReentrantLock lock = new ReentrantLock();
    private int tickets = 100;
    
    void sellTicket() {
        lock.lock(); // 可在这里加tryLock()实现超时控制
        try {
            if (tickets > 0) {
                System.out.println(Thread.currentThread().getName() 
                    + "售出第" + tickets-- + "张票");
            }
        } finally {
            lock.unlock(); // 必须放在finally块
        }
    }
}

3. 线程安全容器

对比不同集合类的表现:

// 危险操作:普通HashMap
Map<String, Integer> unsafeMap = new HashMap<>();

// 安全方案1:ConcurrentHashMap
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();

// 安全方案2:Collections工具类
Map<String, Integer> synchronizedMap = 
    Collections.synchronizedMap(new HashMap<>());

五、避坑指南

  1. 不要过度同步:同步块太大反而会降低性能
  2. 注意锁的顺序:多个锁要按照固定顺序获取,避免死锁
  3. 警惕隐式锁:比如String.intern()的全局锁
  4. 优先使用并发工具类:如CountDownLatch、CyclicBarrier等
// 死锁示例:两个线程互相等待
public class DeadlockDemo {
    static Object lockA = new Object();
    static Object lockB = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                try { Thread.sleep(100); } 
                catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("Thread1 got both locks");
                }
            }
        }).start();
        
        new Thread(() -> {
            synchronized (lockB) {
                synchronized (lockA) {
                    System.out.println("Thread2 got both locks");
                }
            }
        }).start();
    }
}

六、总结与最佳实践

  1. 简单原则:能用volatile就不上锁
  2. 工具优先:优先使用java.util.concurrent包
  3. 避免重复造轮子:不要自己实现复杂同步机制
  4. 测试验证:多线程问题可能在高并发下才暴露

最后记住:理解内存模型不是为了让代码更复杂,而是为了写出更简单可靠的并发程序。就像交通规则——看似限制自由,实则是安全通行的保障。