JAVA多线程中数据一致性的问题

本文探讨了多线程编程中共享变量的一致性问题,分析了由于CPU缓存和重排序导致的数据不一致现象。Java内存模型的Happens-before规则被提出以确保线程间的正确执行顺序,从而解决并发环境下的数据一致性问题。文章还提到了并发控制的重要性,以防止数据错误和损坏。
摘要由CSDN通过智能技术生成

多线程中数据一致性的问题

说到一致性,除了在并发编程中保证共享变量数据的一致性之外,还有数据库的 ACID 中的 C(Consistency 一致性)、分布式系统的 CAP 理论中的 C(Consistency 一致性)。

并发编程是一种重要的程序设计方式,可提高程序的性能和效率。然而,由于并发线程之间的执行是交替进行的,因此共享变量的一致性问题是非常重要的。本文将讨论共享变量的一致性问题,并提供一些解决思路。

先通过一个经典的案例来说明下多线程操作共享变量可能出现的问题,假设我们有两个线程(线程 1 和线程 2)分别执行下面的方法count(),x 是共享变量:

代码

//代码1
public class Example {
    int x = 0;
    public void count() {
        x++;                     //1
        System.out.println(x)//2
    }
}

如果两个线程同时运行,两个线程的变量的值可能会出现以下三种结果:

Java 内存模型

2,1 和 1,2 的结果我们很好理解,那为什么会出现以上 1,1 的结果呢?

我们知道,Java 采用共享内存模型来实现多线程之间的信息交换和数据同步。在解释为什么会出现这样的结果之前,我们先通过下图来简单了解下 Java 的内存模型,程序在运行时,局部变量将会存放在虚拟机栈中,而共享变量将会被保存在堆内存中。

由于局部变量是跟随线程的创建而创建,线程的销毁而销毁,所以存放在栈中,Java 栈数据不是所有线程共享的,所以不需要关心其数据的一致性。

共享变量存储在堆内存或方法区中,数据是线程共享的。而堆内存中的共享变量在被不同线程操作时,会被加载到自己的工作内存中,也就是 CPU 中的高速缓存。

CPU 缓存可以分为一级缓存(L1)、二级缓存(L2)和三级缓存(L3),每一级缓存中所储存的全部数据都是下一级缓存的一部分。当 CPU 要读取一个缓存数据时,首先会从一级缓存中查找;如果没有找到,再从二级缓存中查找;如果还是没有找到,就从三级缓存或内存中查找。

如果是单核 CPU 运行多线程,多个线程同时访问进程中的共享数据,CPU 将共享变量加载到高速缓存后,不同线程在访问缓存数据的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。

如果是多核 CPU 运行多线程,每个核都有一个 L1 缓存,如果多个线程运行在不同的内核上访问共享变量时,每个内核的 L1 缓存将会缓存一份共享变量,这时就会产生数据不一致的问题。

了解完内存模型之后,结合以上解释,我们就可以回过头来看看第一段代码中的运行结果是如何产生的了。看到这里,相信你可以理解图中 1,1 的运行结果了。

重排序

除此之外,在 Java 内存模型中,还存在重排序的问题。请看以下代码:


//代码1
public class Example {
    int x = 0;
    boolean flag = false;
    public void writer() {
        x = 1;                //1
        flag = true;          //2
    }

    public void reader() {
        if (flag) {           //3
             int r1 = x;      //4
             System.out.println(r1==x)
        }
    }
}

例如,在以上案例中,编译器为了尽可能地减少寄存器的读取、存储次数,会充分复用寄存器的存储值。如果没有进行重排序优化,正常的执行顺序是步骤 1\2\3,而在编译期间进行了重排序优化之后,执行的步骤有可能就变成了步骤 1/3/2 或者 2/1/3,这样就能减少一次寄存器的存取次数。

在Java中,指令重排序会导致一些线程执行的顺序不确定,在多线程的情况下会产生一些无法预测和处理的问题,这就是Java内存模型中的指令重排序问题所在。

Happens-before 规则

为了解决这个问题,Java 提出了 Happens-before 规则来规范线程的执行顺序。

Happens-before 规则是指在一个多线程程序中,一个事件的执行对另一个事件的执行是可见的,那么我们称前一个事件先于后一个事件发生,这个规则是Java内存模型(Java Memory Model 简称JMM)中的一种定义。

Java中的happens-before规则,有以下几种情况:

1.程序的顺序性原则:在一个线程中,按照代码的顺序,前面的操作Happens-Before于后面的任意操作。

2.volatile变量规则:对一个volatile变量的写操作,Happens-Before于后续对这个变量的读操作。

3.传递性:如果操作A happens-before操作B,操作B happens-before操作C,那么操作A happens-before操作C。

4.管程锁定规则:对一个锁的解锁(unlock)操作,happens-before于任何后续对同一个锁的加锁(lock)操作。

5.线程启动规则:在子线程开始执行之前,其所在的线程可以有一系列的修改操作对新线程可见。

6.线程中断规则:对一个线程interrupt方法的调用,happens-before于被中断线程检测到该中断事件的发生。

7.线程终结规则:一个线程的所有操作都happens-before于它的终结操作,这个规则保证了在线程结束时,它对共享内存所做的所有修改都能够被其它线程所看到。

Happens-before 规则提供了在多线程环境下,保证线程安全的一些方法。通过对程序的改造,保证不同的操作之间的h-b关系,可以有效地避免线程间数据竞争和内存模型的混乱情况。

总结

在现代计算机系统中,多核处理器成为越来越普遍的事实。由于CPU的主频不再翻倍,同时发展方向也逐渐转向多核,使得并行计算成为计算机的发展趋势。

在JAVA多线程编程中,由于线程之间共享内存,当多个线程同时访问同一数据时,就会产生数据一致性问题,可能会导致数据的读写出现错误,甚至是数据的损坏,为避免此类问题,需要进行适当的同步和互斥控制。

好了,以上就是今天分享的全部内容,enjoy ~欢迎吐槽、交流~ 微信公众号【AI黑板报】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值