并发编程是Java后端开发中最重要的技能之一。要写出正确高效的并发代码,必须深入理解Java内存模型(Java Memory Model,JMM)。本文将从JMM的核心概念出发,逐步深入到volatile、synchronized等关键字的底层原理,帮助你在实战中写出线程安全的代码。

1. 什么是Java内存模型

Java内存模型(JMM)是一种规范,它规定了Java虚拟机与计算机内存的交互方式。JMM的核心目标是屏蔽不同硬件和操作系统的内存访问差异,让Java程序在各种平台上都能有一致的内存访问效果。

JMM将内存分为两个区域:

  • 主内存:所有线程共享的内存区域,存储所有变量的值
  • 工作内存:每个线程私有的内存区域,存储该线程用到的变量的副本

线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量。不同的线程之间无法直接访问对方工作内存中的变量,必须通过主内存来传递。这就是著名的"线程-工作内存-主内存"三层架构:

线程1 ──→ 工作内存1 ──→ 主内存 ←── 工作内存2 ←── 线程2

2. Happens-Before 原则

Happens-Before 是JMM定义的多线程操作之间的偏序关系,是判断数据是否存在竞争、线程是否安全的主要依据。如果一个操作Happens-Before于另一个操作,那么第一个操作的结果对第二个操作是可见的,且第一个操作的执行顺序在第二个操作之前。

Java中关键的Happens-Before规则:

  • 程序次序规则:在一个线程中,按照代码顺序,前面的操作Happens-Before于后面的操作
  • 管程锁定规则:对一个锁的解锁Happens-Before于后续对这个锁的加锁
  • volatile变量规则:对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作
  • 线程启动规则:Thread.start() Happens-Before于该线程中的任何操作
  • 传递性:若A Happens-Before B,且B Happens-Before C,则A Happens-Before C

3. volatile 关键字详解

volatile是Java提供的最轻量级的同步机制,它保证了:

3.1 可见性

当一个变量被volatile修饰时,线程对该变量的写操作会立即刷新到主内存中,同时其他线程对该变量的读操作会从主内存中重新读取,从而保证了变量的可见性。

public class VolatileExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true;  // 写操作,立即刷新到主内存
    }

    public void reader() {
        if (flag) {   // 读操作,从主内存读取最新值
            // 正确处理
        }
    }
}

3.2 禁止指令重排序

volatile关键字通过插入内存屏障来禁止指令重排序优化。具体来说:

  • 在每个volatile写操作的前后插入Store屏障
  • 在每个volatile读操作的后面插入Load屏障

这确保了程序在并发环境下的执行顺序与代码逻辑一致。

注意:volatile不能保证原子性。对于像 count++ 这样的复合操作,仍然需要使用synchronized或Atomic类来保证原子性。

4. synchronized 底层原理

synchronized是Java中最常用的同步机制,它在JVM层面通过Monitor(监视器锁)来实现。在HotSpot虚拟机中,synchronized的底层实现经历了多次优化,从最初的重量级锁到现在的锁升级机制。

4.1 锁升级过程

JDK 1.6以后,synchronized引入了锁升级机制,整个过程如下:

  1. 偏向锁:当只有一个线程访问同步块时,偏向锁会记录线程ID,避免CAS操作
  2. 轻量级锁:当有多个线程交替获取锁时,升级为轻量级锁,通过CAS操作获取锁
  3. 重量级锁:当锁竞争激烈时,升级为重量级锁,由操作系统Mutex实现
public class SynchronizedExample {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    // 等价写法:synchronized方法
    public synchronized void incrementSync() {
        count++;
    }
}

4.2 synchronized vs Lock

对比维度 synchronized ReentrantLock
使用方式 关键字,自动获取/释放 API,需手动lock/unlock
锁类型 非公平锁 公平/非公平可选
可中断
条件等待 wait/notify Condition
性能 持续优化,与Lock接近 高并发下略优

5. 并发编程实战建议

在日常开发中,以下建议可以帮助你写出更安全的并发代码:

  • 优先使用不可变对象:不可变对象天然线程安全,是并发编程的首选
  • 合理选择锁粒度:锁的粒度太粗会降低并发性,太细则容易死锁
  • 使用线程安全容器:如 ConcurrentHashMap、CopyOnWriteArrayList 等
  • 避免锁竞争:使用 ThreadLocal、CAS 等无锁技术减少锁竞争
  • 使用线程池:通过 ThreadPoolExecutor 管理线程,避免手动创建线程
  • 关注死锁问题:遵循固定的加锁顺序,避免循环等待
// 线程安全的计数器示例
import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public int increment() {
        return count.incrementAndGet();  // CAS操作,无锁安全
    }

    public int getCount() {
        return count.get();
    }
}

并发编程是一个需要不断实践和积累经验的领域。深入理解JMM的原理,熟练掌握synchronized和volatile等工具,同时保持"少用锁、用好锁"的思维,才能写出高效可靠的并发程序。