首页 > 编程语言 >Java原子性、可见性、有序性的解析

Java原子性、可见性、有序性的解析

时间:2024-02-04 16:31:41浏览次数:30  
标签:执行 Java instance step 线程 内存 有序性 解析 CPU

一、原子性

原子性指操作在CPU执行的过程中,不可中断,也不可在中途切换,要么执行完成要么不执行。

package learn;

/**
 * @author qx
 * @date 2024/1/5
 * @des
 */
public class ThreadAtomicityTest {

    private int step;

    public int getStep() {
        return step;
    }

    public void increaseStep() {
        step++;
    }
}

线程执行的指令包含三大步骤:

1.将变量step从内存中加载到CPU的寄存器中;

2.在CPU的寄存器中执行step++操作;

3.将step++后的结果写入缓存(CPU缓存或计算机内存);

线程切换可能发生在任何一条指令完成之后,而不是Java某条语句完成后。

假设线程1和线程2同时执行increaseStep()方法,在线程1执行过程中,CPU完成指令码的步骤①后发生了线程切换,此时线程2开始执行指令码的步骤①。当两个线程都执行完整个increaseStep()方法后,得到的step的值是1而不是2。

代码实例如下所示:

package learn;

/**
 * @author qx
 * @date 2024/1/5
 * @des
 */
public class ThreadTest {
    public static void main(String[] args) {
        ThreadAtomicityTest threadAtomicityTest =new ThreadAtomicityTest();
        new Thread(()->{
            threadAtomicityTest.increaseStep();
        }).start();
        new Thread(()->{
            threadAtomicityTest.increaseStep();
        }).start();

        System.out.println(threadAtomicityTest.getStep());

    }
}

输出结果:

1

这个问题具体是怎么样产生的呢?

线程1将step=0加载到CPU的寄存器后,发生了线程切换。此时还没有执行step++操作,也没有将操作的结果写入内存,所以,内存中的step值仍为0。

线程2将step=0加载到CPU的寄存器中,执行step++操作,并将执行后的结果写入内存。此时,CPU切换到线程1继续执行,在执行线程1中的step++后,线程1中的step仍为1,线程1将step=1写入内存,最终内存中的step为1。

如果在CPU中存在正在执行的线程,此时,发生了线程切换,就可能导致并发编程的原子性问题。

所以,造成原子性问题的根本原因是在线程执行过程中发生了线程切换。

二、可见性

可见性指一个线程修改了共享变量,其他线程能够立刻读到共享变量的最新值。在并发编程中,有两种情况能实现当一个线程修改了共享变量后,其他线程立刻就能读到最新值。

1.串行

比如有2个线程,线程是串行执行的,线程1写完数据后,线程2才执行并从主内存中读取到数据,线程1向主内存中写入数据对线程2是可见的,所以线程1和线程2之间不存在可见性问题。

2.单核CPU

在单核CPU中,多个线程之间也不会出现可见性问题。在单核CPU中,只能有一个线程占用CPU资源来执行任务,其他线程获取CPU资源执行任务时,共享变量中的值一定是最新的。

3.多线程多CPU时可见性问题

Java中,多个线程在读写内存中的共享变量时,会先把主内存中的共享变量数据复制到线程的工作内存中。每个线程在对数据进行读写操作时,都是直接操作自身的工作内存中的数据。由于每个线程都有自己的工作内存,所以线程1的数据对线程2是不可见的。线程1修改了数据,线程2不一定能够立刻读到修改后的值,这就造成了可见性问题。

package learn;

/**
 * @author qx
 * @date 2024/1/5
 * @des
 */
public class SynchronizedTest {
    private static int count = 0;

    public static void incrementCount() {
        count++;
    }

    public static int increment() throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                incrementCount();
            }
        });


        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                incrementCount();
            }
        });
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("count=" + SynchronizedTest.increment());
    }
}

控制台输出:

count=1942

三、有序性

有序性指程序能够按照编写的代码顺序执行,不会发生跳过代码行的情况,也不会出现跳过CPU指令的情况。

为了提高程序的执行性能和编译性能,计算机和编译器有时候会修改程序的执行顺序。

在Java中一个典型的案例就是使用双重检测机制来创建单例对象。

package learn;

/**
 * @author qx
 * @date 2024/1/5
 * @des
 */
public class SingleInstance {
    private SingleInstance() {
    }

    private static SingleInstance singleInstance;

    public static SingleInstance getInstance() {
        if (singleInstance == null) {
            synchronized (SingleInstance.class) {
                if (singleInstance == null) {
                    singleInstance = new SingleInstance();
                }
            }
        }
        return singleInstance;
    }
}

假如线程1和线程2同时调用getInstance()方法获取对象实例,两个线程会同时发现instance为空,同时对SingleInstance.class加锁,而JVM会保证只有一个线程获取到锁,这里我们假设线程1获取到锁,线程2因为未获取到锁而进行等待。接下来,线程1再次判断instance对象为空,从而创建instance对象的实例,然后释放锁。此时,线程2被唤醒,再次尝试获取锁,获取锁成功后,线程2检查此时的instance对象已经不再是空,线程2不再创建instance对象。

上述流程看起来没有什么问题,但是,在高并发、大流量的场景下获取instance对象时,使用new关键字创建SingleInstance类的实例对象时,会因为编译器或解释器对程序的优化而出现问题。

instance = new SingleInstance();

对于上面的代码包含三个步骤:

① 分配内存空间

② 初始化对象

③ 将instance引用指向内存空间

正常执行的CPU指令顺序为①②③,CPU对程序进行重排序后的执行顺序是①③②,此时就会出现问题。

当线程1判断instance为空时,为对象分配内存空间,并将instance指向内存空间。此时还没有进行对象的初始化,发生了线程切换,线程2获取到CPU资源执行任务。线程2判断此时的instance不为空,则不再执行创建对象的操作,直接返回未初始化的instance对象。

所以,造成有序性问题的根本原因是编译器对程序进行优化,从而可能造成有序性问题。

四、解决方案

在Java中解决原子性问题的方案包括synchronized、Lock、ReentranLock、ReadWriteLock、CAS操作、Java中提供的原子类等。

解决可见性和有序性问题,可以禁用CPU缓存和编译器优化。

JVM提供了禁用缓存和编译优化的方法,包括volatile关键字、synchronized、final关键字以及Java内存模型中的Happens-Before原则。


标签:执行,Java,instance,step,线程,内存,有序性,解析,CPU
From: https://blog.51cto.com/u_13312531/9587304

相关文章

  • 【JAVA】Java 使用 XPath表达式定位节点读取自定义XML方法
    *加载配置文件节点*@paramattributeValue节点属性值*@paramareaCode节点属性值*/publicstaticMap<String,String>getConfigXml(StringattributeValue,StringareaCode){StringfilePath="config.xml";Map<St......
  • 深入浅出Java多线程(七):重排序与Happens-Before
    引言大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第七篇内容:重排序与Happens-Before。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!在上一篇文章中,我们简单提了一下重排序与Happens-Before。在这篇文章中我们将深入讲解一下重排序与Happens-Before,然......
  • java代码实现自动生成数据库表er图
    最近有同事看到字节跳动产品设计文档里有数据库表er图。就想问问又没有现成的工具也给直接生成一个er图,经查找验证发现并没有。因为现在表关系都是用的逻辑外键而非物理外键约束的,所以像navicat等工具就算生成了也没有描述关系的连接线。那么为了满足需求,这边就略微出手写了个代码......
  • 深入解析 Flink CDC 增量快照读取机制
    深入解析FlinkCDC增量快照读取机制一、Flink-CDC1.x痛点FlinkCDC1.x使用Debezium引擎集成来实现数据采集,支持全量加增量模式,确保数据的一致性。然而,这种集成存在一些痛点需要注意:一致性通过加锁保证:在保证数据一致性时,Debezium需要对读取的库或表加锁。全局锁可能导致数......
  • Java AQS
    AQS介绍AQS的全称为 AbstractQueuedSynchronizer ,翻译过来的意思就是抽象队列同步器。这个类在 java.util.concurrent.locks 包下面。AQS就是一个抽象类,主要用来构建锁和同步器。publicabstractclassAbstractQueuedSynchronizerextendsAbstractOwnableSynchronizer......
  • Java压缩文件为ZIP并加密
    1:引入jar包<dependency><groupId>net.lingala.zip4j</groupId><artifactId>zip4j</artifactId><version>1.3.1</version></dependency> 2:单文件压缩 importnet.lingala.z......
  • javaee平台技术
    3.spring和springboot3.3spring容器BeanFactory:创建Bean对象以及管理Bean对象。 ApplicationContext:BeanFactory的子接口,继承了其所有能力外,还追加了其他功能。  控制反转实现机制:将对象间的依赖交给容器去处理,利用set或构造函数的方法把依赖注射进来,告诉其关联关系,主......
  • 学习解析几何的启示——去掉直接联系,采用中心化标准
    目录引入案例1:找出三角形的外心案例2:证明两条线段垂直案例3:确定与一组点等距离的点的位置案例4:研究二次曲线的性质思想引入同样的几何体,不同阶段所使用的解题技巧:在初中,熟悉几何定理,需要添加辅助线在高中,需要建立坐标系,采用向量的方法,套对应的公式解析几何之所以强大,在于......
  • 几个高级 JavaScript 技巧
    赋值解构:赋值解构是一种从数组或对象中提取值并将其分配给变量的简洁方法。它简化了代码并提高了可读性。对于数组,您可以使用方括号表示法,对于对象可以使用大括号。扩展语法:可以使用扩展语法将数组的元素或对象的属性扩展到另一个数组或对象。这对于制作副本、合并对象以及将多个......
  • java直连mysql操作数据
    连接器importjava.sql.Connection;importjava.sql.DriverManager;importjava.sql.PreparedStatement;importjava.sql.ResultSet;importjava.sql.SQLException;/***@author:chenKeFeng*@date:2024/1/3010:21*/publicclassMySQLConnector{pri......