Java内存模型(JMM)深度解析:从并发问题到解决方案

一、为什么需要内存模型(JMM)?

在多核CPU时代,并发编程是提升程序性能的核心手段,但是并发带来了三大经典问题:

  1. 可见性问题:线程A修改的变量,线程B无法立即看到

  2. 原子性问题:i++操作在机器指令层面可能被中断

  3. 有序性问题:代码执行顺序与编写顺序不一致

这些问题的根源在于:

  • CPU多级缓存架构导致内存可见性问题

  • 编译器/处理器优化引发指令重排序

  • 线程切换带来的原子性破坏

CPU Cache 示意图

二、JMM架构设计

2.1 内存抽象模型

JMM定义了两个核心概念:

  • 主内存(Main Memory):所有线程共享的内存区域

  • 工作内存(Working Memory):每个线程私有的内存副本

从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:

  1. 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
  2. 线程 2 到主存中读取对应的共享变量的值。

也就是说,JMM 为共享变量提供了可见性的保障。

不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:

  1. 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
  2. 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。

2.2 内存交互协议

JMM定义了8种原子操作保证内存可见性

操作作用范围说明
lock主内存变量标记变量为线程独占状态
unlock主内存变量释放变量的锁定状态
read主内存变量将变量值传输到线程工作区
load工作内存将read的值放入变量副本
use工作内存将变量值传递给执行引擎
assign工作内存将执行结果赋值给变量副本
store工作内存将变量值传送到主内存
write主内存变量将store的值放入主内存变量

三、happens-before原则

3.1 核心规则

  1. 程序顺序规则:单线程内的操作按代码顺序保证有序性

  2. volatile规则:volatile写操作先于后续的读操作

  3. 锁规则:解锁操作先于后续的加锁操作

  4. 传递性规则:A→B且B→C,则A→C

  5. 线程启动规则:Thread.start()先于线程内所有操作

下面这张是 《Java 并发编程的艺术》这本书中的一张 JMM 设计思想的示意图:

3.2 实际案例

// 示例:双重检查锁定单例模式
public class Singleton {
    private volatile static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {                 // 第一次检查
            synchronized (Singleton.class) {    // 加锁
                if (instance == null) {         // 第二次检查
                    instance = new Singleton(); // volatile写
                }
            }
        }
        return instance;
    }
}

这里volatile的happens-before关系保证了:

  1. 对象初始化完成 → 写操作

  2. 写操作 → 读操作

四、内存屏障与指令重排序

4.1 屏障类型

屏障类型作用
LoadLoad屏障禁止操作重排序
StoreStore屏障禁止操作重排序
LoadStore屏障禁止读后写重排序
StoreLoad屏障禁止写后读重排序(全能屏障)

4.2 volatile实现原理

public class VolatileExample {
    private volatile int flag = 0;
    
    public void writer() {
        flag = 1;  // StoreStore屏障 + StoreLoad屏障
    }
    
    public void reader() {
        if (flag == 1) {  // LoadLoad屏障 + LoadStore屏障
            // do something
        }
    }
}

volatile变量的读写会插入内存屏障:

  • 写操作前插入StoreStore屏障

  • 写操作后插入StoreLoad屏障

  • 读操作前插入LoadLoad屏障

  • 读操作后插入LoadStore屏障

五、JMM与JVM内存结构对比

特性JMMJVM内存结构
关注点多线程内存可见性问题内存区域划分与管理
核心概念主内存、工作内存堆、栈、方法区等
规范级别语言级内存模型虚拟机实现规范
可见性保证通过happens-before规则不直接处理可见性问题
典型应用volatile、synchronized语义对象分配、垃圾回收

六、并发问题解决方案

6.1 可见性问题

可见性问题概述在多线程环境中,每个线程都有自己的工作内存,线程对变量的操作是先从主内存拷贝到工作内存,操作完成后再写回主内存。这就可能导致一个线程对变量的修改,其他线程不能及时看到,从而引发可见性问题。

  • volatile关键字:强制所有读写直接操作主内存


public class VolatileVisibilityExample {
    private volatile boolean flag = false;

    public void writer() {
        flag = true; // 写操作直接更新主内存
    }

    public void reader() {
        while (!flag) {
            // 等待 flag 变为 true
        }
        // 由于 flag 是 volatile 变量,能及时看到 writer 线程对 flag 的修改
        System.out.println("Flag is now true");
    }
}

  • synchronized同步块:解锁时自动刷新工作内存到主内存

public class SynchronizedVisibilityExample {
    private boolean flag = false;
    private final Object lock = new Object();

    public void writer() {
        synchronized (lock) {
            flag = true; // 修改共享变量
        } // 退出同步块,将修改刷新到主内存
    }

    public void reader() {
        synchronized (lock) {
            // 进入同步块,从主内存读取最新的 flag 值
            if (flag) {
                System.out.println("Flag is true");
            }
        }
    }
}

  • final关键字:正确发布的不可变对象保证可见性

public class FinalVisibilityExample {
    private final int value;

    public FinalVisibilityExample(int value) {
        this.value = value; // 初始化 final 变量
    }

    public int getValue() {
        return value;
    }
}

6.2 原子性问题

原子性问题概述:原子性是指一个操作或一系列操作要么全部执行要么全部不执行,不会被其他线程中断。在多线程环境中,如果多个线程同时对一个共享变量进行读写操作,可能会导致数据不一致的问题,因为这些操作可能不是原子性的。

  • Atomic原子类:基于CAS实现无锁编程

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increment() {
        counter.incrementAndGet(); // 原子性自增操作
    }

    public int getCounter() {
        return counter.get();
    }
}

  • synchronized同步:通过互斥保证原子性

public class SynchronizedAtomicExample {
    private int counter = 0;

    public synchronized void increment() {
        counter++; // 同步方法,保证原子性
    }

    public synchronized int getCounter() {
        return counter;
    }
}

  • Lock接口实现:显式锁控制临界区

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

public class LockAtomicExample {
    private int counter = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            counter++; // 临界区,保证原子性
        } finally {
            lock.unlock();
        }
    }

    public int getCounter() {
        lock.lock();
        try {
            return counter;
        } finally {
            lock.unlock();
        }
    }
}

6.3 有序性问题

有序性问题概述:在多线程环境中,编译器和处理器为了提高性能,可能会对指令进行重排序。重排序可能会导致程序的执行顺序与代码的编写顺序不一致,从而引发有序性问题。

  • volatile:禁止指令重排序

public class VolatileOrderingExample {
    private int a = 0;
    private volatile boolean flag = false;

    public void writer() {
        a = 1;         // 操作 1
        flag = true;   // 操作 2,由于 flag 是 volatile 变量,操作 1 不会重排序到操作 2 之后
    }

    public void reader() {
        if (flag) {    // 操作 3
            int i = a; // 操作 4,操作 4 不会重排序到操作 3 之前
        }
    }
}

  • synchronized:保证临界区内代码串行执行

public class SynchronizedOrderingExample {
    private int a = 0;
    private int b = 0;
    private final Object lock = new Object();

    public void writer() {
        synchronized (lock) {
            a = 1; // 操作 1
            b = 2; // 操作 2,操作 1 和操作 2 会按顺序执行
        }
    }

    public void reader() {
        synchronized (lock) {
            int x = b; // 操作 3
            int y = a; // 操作 4,操作 3 和操作 4 会按顺序执行
        }
    }
}

  • final:正确构造的对象保证初始化安全

public class FinalOrderingExample {
    private final int value;

    public FinalOrderingExample(int value) {
        this.value = value; // 正确初始化 final 字段
    }

    public int getValue() {
        return value;
    }
}

七、实战:诊断内存可见性问题

public class VisibilityDemo {
    boolean ready = false;
    int result = 0;
    int number = 1;

    public void write() {
        number = 2;          // 操作1
        ready = true;        // 操作2
    }

    public void read() {
        if (ready) {         // 操作3
            result = number; // 操作4
        }
    }
}

可能出现的结果:

  • 线程A执行write()

  • 线程B执行read()可能得到result=1(指令重排序导致操作2先于操作1)

解决方案:

  1. 将ready声明为volatile

  2. 使用synchronized同步方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小梁不秃捏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值