Java内存模型(JMM)与顺序一致性:理解并发编程的数据竞争与同步机制

Luna
Luna
发布时间:2026-10-16| 6人浏览过
原创

在Java并发编程中,数据竞争是导致程序行为不可预测的主要原因之一。本文旨在厘清Java内存模型(JMM)与理论上的顺序一致性内存模型之间的关系,解释JMM如何通过正确的同步机制(如synchronized和volatile)来保证程序的执行结果符合开发者的预期,并探讨未同步代码可能面临的指令重排序及64位变量原子性缺失等底层细节。

JMM规范下的数据竞争定义

在多线程环境中,如果程序编写不当,极易引发数据竞争。JVM规范对“数据竞争”有明确的定义,当以下三个条件同时满足时,即构成数据竞争:

  • 在一个线程中对一个共享变量进行写操作;
  • 在另一个线程中对该同一变量进行读操作;
  • 这两个读写操作之间没有通过某种同步机制(如锁或volatile)来确立先后顺序。

如果一个多线程程序消除了数据竞争,通常意味着它使用了正确的同步手段。只有彻底消除数据竞争,程序才能具备确定的执行逻辑和预期的输出结果。

顺序一致性内存模型及其特性

顺序一致性(Sequential Consistency)是一种理想化的内存模型理论参照。JVM规范指出,如果程序被正确地同步了,那么它的执行结果应当与在该顺序一致性模型中的执行结果一致。

核心特性

  • 单线程内顺序执行:每个线程内的所有操作都必须按照代码编写的程序顺序执行。
  • 全局单一视图:无论是否同步,所有线程只能看到一种全局的操作执行顺序。每个操作必须是原子的,且执行后立即对所有其他线程可见。

在概念上,该模型存在一个单一的全局内存空间。任意时刻,系统通过一个开关机制将全局内存连接到某一个线程,确保同一时间只有一个线程可以访问内存。这种机制将所有内存读写操作串行化,从而实现了全序关系。

执行效果示例对比

假设存在两个线程A和B,线程A的执行序列为 A1 - A2 - A3,线程B的执行序列为 B1 - B2 - B3。

正确同步时的执行效果

若使用监视器锁对A和B进行正确同步(例如A执行完释放锁,B再获取同一把锁),则整个程序表现为严格的线性顺序,即线程A的三个操作完成后,线程B才开始执行。

未同步时的执行效果

在没有同步的情况下,虽然整体执行是交错的,但在顺序一致性模型中,依然强制要求所有线程看到一致的交错顺序。例如,线程A和线程B看到的统一顺序可能是:A1 - B1 - A2 - B2 - A3 - B3。这是因为该模型强制要求每个操作必须立即对所有线程可见。

JMM与现实的区别:真实的JMM并不保证这一点。在未同步程序中,由于CPU缓存的存在,一个线程写入的数据可能仅保留在本地寄存器或缓存中,尚未刷新到主内存。此时,其他线程观察不到该写操作,导致不同线程看到的操作顺序可能不一致,进而产生多种运行结果。

同步程序在JMM中的表现

JMM的设计哲学是在不改变正确同步程序执行结果的前提下,允许编译器和处理器进行优化。对于临界区内部的代码,即使发生重排序,只要不逸出临界区,就不会破坏同步语义。

同步示例代码分析

以下示例展示了如何使用 synchronized 关键字消除重排序带来的不确定性。

package com.lizba.p1;

public class SynReorderExample {
    // 定义变量a
    int a = 0;
    // flag变量是个标记,用来标志变量a是否被写入
    boolean flag = false;

    public synchronized void writer() {     // 获取锁
        a = 1;
        flag = true;
    }                                       // 释放锁

    public synchronized void reader() {     // 获取锁
        if (flag) {
            int i = a * a;
            System.out.println("i:" + i);
        }
    }                                       // 释放锁
}

测试驱动:

public static void main(String[] args) {
    final SynReorderExample re = new SynReorderExample();

    new Thread(() -> re.writer()).start();

    new Thread(() -> re.reader()).start();
}

在上述代码中,writer() 和 reader() 均被 synchronized 修饰,保证了互斥执行。多次测试结果均为 i: 1,证明了变量的有序可见性。

结论

在顺序一致性模型中,所有操作严格按程序顺序串行执行。而在JMM中,虽然允许临界区内发生指令重排序,但进入和退出临界区的特殊处理(如获取和释放监视器锁)确保了在特定时间点上,线程拥有与顺序一致性模型相同的内存视图。由于锁的互斥特性,线程B无法观察到线程A在锁内部的重排序,从而保证了逻辑的正确性。

未同步程序的风险与限制

对于未正确同步的代码,JMM提供的保护非常有限。虽然Java语言规范禁止“空中楼阁”现象(Out Of Thin Air,即读取的值不能既非默认值也非任何线程写入的值),但并未提供强顺序保证。

比较内容\模型名称 顺序一致性模型 JMM模型
单线程内顺序执行 √ ×
一致的操作执行顺序 √ ×
64位long型和double型变量写原子性 √ ×

64位长整型(double/long)的原子性问题

总线事务与CPU架构限制

在计算机底层,数据通过总线在处理器和内存间传递,称为总线事务(Bus Transaction)。总线具有仲裁机制,确保同一时刻只有一个处理器可以进行内存读写,从而在物理层面上保证了总线事务的原子性。

然而,在一些32位的老旧处理器架构上,处理64位数据(如 long 和 double)时,硬件层面会将一次64位写操作拆分为两次32位写操作,分属不同的总线事务。这意味着64位写操作失去了原子性。

具体风险分析

如果线程A正在拆分写入一个 long 类型的变量,而线程B在同一时间读取该变量,线程B极有可能读到一半是新值、一半是旧值的“撕裂”数据,这是无效值。

解决方案:从JDK 1.5开始引入JSR-133新内存模型后,虽然允许64位写操作被拆分为两个32位事务,但规定读操作必须在单个事务中完成以尽可能捕获有效值。对于需要严格原子性的64位操作场景,建议使用 volatile 修饰该变量或借助 java.util.concurrent.atomic 包下的原子类。

以上就是Java内存模型与顺序一致性机制的详细内容,更多关于Java并发编程中JMM原理的资料请关注本站其它相关文章!

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。