Java多线程原子性、可见性与有序性深度解析

今天早睡
今天早睡
发布时间:2026-10-16| 7人浏览过
原创

在Java多线程开发中,理解并发的三大核心特性至关重要。本文将详细解析什么是原子性、可见性和有序性,剖析它们在JVM层面的运作机制以及可能导致的并发安全问题,并提供相应的解决方案与判断标准。

1. 原子性问题

所谓原子操作,即“不可中断的一个或一系列操作”。它意味着操作一旦开始,就会一直运行到结束,中间不会被任何线程调度机制打断,也不会发生线程切换。

以经典的 i++ 操作为例,它在 JVM 层面并非单步指令,而是会拆分为以下四步字节码执行:

getstatic i     // 获取静态变量i的值(从内存取值)
iconst_1        // 准备常量1
iadd            // 自增(寄存器增加1)
putstatic i     // 将修改后的值存入静态变量i(存值到内存)

在单线程环境下,这四行代码顺序执行,不会交错,因此没有问题。但在多线程场景下,这四个原子操作之间可能发生线程切换,导致结果不一致。

当多个线程同时执行这些步骤时,可能会出现以下两种典型错误情况:

  • 出现负数的情况:由于读取和写入的时间差,导致数据覆盖异常。
  • 出现正数的情况:部分更新被其他线程读取,导致状态混乱。

总结原子性问题:虽然“内存取值”、“寄存器增加1”和“存值到内存”本身是不可再分的原子操作,但当它们组合在一起形成复合操作(如先读后写)时,就不再具备原子性。因为操作间隙允许其他线程介入,从而导致读写数据不一致。

2. 可见性问题

内存可见性是指:当一个线程修改了共享变量的值,其他线程能够立即看到这个修改。如果无法立即看到,即为可见性问题。

底层原理:Java内存模型 (JMM)

JMM 规定了所有变量都存放在公共主存(Main Memory)中。每个线程拥有自己的私有工作内存(Working Memory),包含该线程使用到的变量的副本。线程对变量的所有读写操作都必须在工作内存中进行,不能直接读写主存。

可见性冲突示例:

  1. 主存中 sum=0。
  2. 线程A读取 sum=0 到工作内存,将其加1变为 1。此时线程A尚未将新值刷回主存。
  3. 线程B紧接着从主存读取 sum,发现仍为 0。线程B也将其加1变为 1 并尝试写回。
  4. 最终两个线程操作完,期望结果是 2,但实际结果可能是 1。

这种并发操作下的失效如下图所示:

解决方法:

要解决这个问题,必须保证线程间的通信,即一个线程修改了变量后,能通知其他线程刷新主存。在 Java 中,最简单的方式是使用 volatile 关键字修饰共享变量,强制线程直接从主存读写数据,并保持缓存一致性协议。

3. 有序性问题

有序性是指程序按照代码编写的先后顺序执行。如果在并发环境下,程序的实际执行顺序与预期顺序不同,并且导致了逻辑错误,这就是有序性问题。

指令重排序的影响:

CPU 为了提高执行效率,可能会进行指令重排序。在单核时代,这不会影响最终结果的正确性;但在多核并发环境下,若多个线程共享可变数据,指令重排序会导致严重的逻辑漏洞。

通过以下代码演示有序性问题的极端情况:

@Slf4j
public class Test3 {
    private static volatile int x=0,y=0;
    private static int a=0,b=0;

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;;i++){
            a=0; b=0; x=0; y=0;
            Thread t1 = new Thread(() -> {
                a = 1;
                x = b; // 注意这里读取的是b
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a; // 注意这里读取的是a
            });
            t1.start(); t2.start();
            t1.join(); t2.join();
            
            // 理论上x和y不可能同时为0
            if(x==0 && y==0){
                log.debug("x:{}, y:{}",x,y);
            }
        }
    }
}

理论上,无论哪个线程先执行,结果应该是 (1,0)、(0,1) 或 (1,1),绝对不可能出现 (0,0)。然而实际运行中,确实观测到了 x:0, y:0 的输出。

原因分析:

这是因为发生了指令重排序。例如线程 t1 中的 a=1 和 x=b 的顺序可能被交换,导致 x=b 先执行(此时 b 为0),然后 a=1 才执行。同理 t2 也可能发生类似重排,最终导致双零输出。

总结

并发程序的正确执行依赖于原子性、可见性和有序性的保障。只要其中任意一项未能得到妥善控制,都可能导致难以排查的并发错误。在实际开发中,应深刻理解这三者的机制,合理使用锁、volatile 或 java.util.concurrent 包下的高级工具来规避风险。

以上就是Java多线程的原子性、可见性与有序性的详细内容,更多关于Java并发编程的资料请关注本站其它相关文章!

相关标签:

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

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 算法库。