Java 三大并大特性-有序性介绍(结合代码、分析源码)

目录

一、概念解析

二、 有序性代码例子

2.1 代码

2.2 执行结果

三、 指令重排序机制

3.1 为什么要引入指令重排序

3.2 指令重排序的分类

3.2.1 编译器优化重排序

3.2.2 指令级并行的重排序

3.2.3 内存系统的重排

3.3 指令重排序规范

3.3.1 as-if-serial 规范

3.3.2 happens-before 规范

3.3.2.1 先行发生原则适用场景

四、 Java 中保证有序性手段

4.1 volatile

4.1.1 DCL单例对象代码

4.1.2 测试结果

4.1.3 volatile有序性原理分析

4.1.3.1 使用volatile修饰DCL单例的原因

4.1.3.2 查看字节码

4.1.3.3 hotspot 层面

4.1.3.4 volatile 有序性原理总结


一、概念解析

是指程序中代码的执行顺序,Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

二、 有序性代码例子

2.1 代码

package com.ningzhaosheng.thread.concurrency.features.order;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 20:01:16
 * @description 测试有序性
 */
public class TestOrder {
    static int a, b, x, y;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            Thread t1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                b = 1;
                y = a;
            });

            t1.start();
            t2.start();
            t1.join();
            t2.join();

            if (x == 0 && y == 0) {
                System.out.println("第" + i + "次,x = " + x + ",y = " + y);
            }
        }
    }

}

2.2 执行结果

从以上截图中我们可以看到,其实程序并没有按照我们编写的代码顺序去执行,为了提高性能,一直在执行for循环体和打印结果操作,没有执行线程t1、t2里面的内容。那么为什么会出现这样的情况呢,要解析这个问题,还得介绍下指令重排序机制。

三、 指令重排序机制

刚才我们在解析有序性概念的时候指出了并发编程有序性问题产生的原因是指令重排序,通过代码例子,我们也验证了结果。既然指令重排序会造成并发线程安全问题,那么为什么还要引入这个指令重排序呢?它解决什么问题呢?下面我们就来分析下。

3.1 为什么要引入指令重排序

其实说到底引入指令从排序都是源于对性能的优化,我们都知道CPU运行效率相比缓存、内存、硬盘IO之间效率有着指数级的差别,CPU作为系统的宝贵资源,如能更好的优化和利用这个资源就能提升整个计算机系统的性能。

其实指令重排序就是一种来源于生活的优化思想,这种思想在生活中处处可见,就像平常咱们做菜,咱们会选择在炒第一个菜的同时就在洗第二个菜了, 咱们会把熟得最慢的菜放到最开始(比如煲汤),因为在等待这些菜熟的过程中(类似计算机中的IO等待)咱们(类似计算机中的CPU)还可以做其它事情,这就是一种时间上的优化,在计算机领域也是一样,它也会根据指令的类别做一些优化,目的就是把CPU的资源利用起来,这样就能提升整个计计算机的效率。

3.2 指令重排序的分类

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。

3.2.1 编译器优化重排序

编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

3.2.2 指令级并行的重排序

现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3.2.3 内存系统的重排

由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

3.3 指令重排序规范

3.3.1 as-if-serial 规范

我们可能会想,我写代码的时候,就是按照我的思路顺序写下来的,但是编译执行的时候,由于指令重排序的存在,优化了执行顺序,那它是怎么保证我程序相关计算逻辑的正确性呢?这个问题,指令重排序机制是做了处理的,对于那些存在数据依赖的两个操作,是不会被做重排序的。而保障这个的规范,就是我们的as-if-serial 规范。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的。

as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题

可以重排的情况 : 对于下面代码 , 两条指令顺序颠倒 , 执行结果相同 , 可以进行指令重排 ;

int a = 0;
int b = 10;

不可以进行重排的情况 : 对于下面的代码 , 两条指令如果上下颠倒 , 结果不同 , 不可以进行指令重排 ;

int a = 0;
int b = a+10;

不管怎么重排序,程序的执行结果不能被改变,编译器,runtime 和处理器都必须遵守as-if-serial语义。即编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

3.3.2 happens-before 规范

先行发生原则 。

happens-before 先行发生原则 :A happens-before B,A 先于 B 发生 , 先 A 后 B ;

Java 虚拟机在编译时和运行时 , 会对 JVM 指令进行重排优化 , 很明显 , 指令重排会对线程并发产生影响 ;

为了保证并发编程的安全性,这里规定了一些场景下 , 禁止在这些场景中 使用 指令重排 ;

3.3.2.1 先行发生原则适用场景
  • 程序次序原则 :在程序内 , 按照代码书写的执行顺序 , 前面的代码先执行 , 后面的代码后执行 ; 时间上靠前 的操作先于时间上靠后的操作执行。
  • 管程锁规则 : 不论是单线程还是多线程 , 线程 A 解锁后 , 线程 B 获取该锁 , 可以看到线程 A 的操作结果 ; 解锁的操作先于加锁的操作 ; 线程 B 要加锁 , 必须等待线程 A 解锁完毕才可以 ;
  • volatile 规则 : volatile 关键字修饰的变量 , 线程 A 对该变量的写操作 先于 线程 B 读取该变量的操作 , 线程 A 对该变量的写操作的结果对于线程 B 一定可见 ;
  • 线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
  • 线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
  • 传递性: happens-before 规则具有传递性 ;如果 A happens-before B 和 B happens-before C ,则 A happens-before C ;
  • 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
  • 对象终结规则:对象的创建先于对象的终结,, 创建就是调用构造函数 , 终结就是调用finalize()方法

只要符合上述规则 , 不需要进行同步 , 就可以成立 ;

通过 " happens-before 先行发生原则 " 可以判定两个线程的操作 , 是否有发生冲突的可能 ;

四、 Java 中保证有序性手段

4.1 volatile

4.1.1 DCL单例对象代码

我们实现DCL单例对象,通过多线程获取单例对象,比较两个对象是否相等,在线程安全的情况下,我们的测试结果应该是相等的,下面我们来实现下。

package com.ningzhaosheng.thread.concurrency.features.order.singleton;

/**
 * @author ningzhaosheng
 * @date 2024/2/6 16:14:45
 * @description DCL实现单例
 */
public class OrderSingleton {
    private static volatile OrderSingleton test;

    private OrderSingleton() {

    }
    public static OrderSingleton getInstance(){
        // B
        if(test  == null){
            synchronized (OrderSingleton.class){

                if(test == null){
                    // A   ,  开辟空间,test指向地址,初始化
                    test = new OrderSingleton();
                }
            }
        }
        return test;
    }
}

package com.ningzhaosheng.thread.concurrency.features.order.singleton;

import java.util.concurrent.Callable;

/**
 * @author ningzhaosheng
 * @date 2024/2/6 16:16:20
 * @description 模拟多线程获取DCL单例
 */
public class ThreadSingleton implements Callable<OrderSingleton> {


    @Override
    public OrderSingleton call() throws Exception {
        return OrderSingleton.getInstance();
    }
}

package com.ningzhaosheng.thread.concurrency.features.order.singleton;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author ningzhaosheng
 * @date 2024/2/6 12:08:07
 * @description 测试多线程获取DCL单例对象,是否存在线程安全问题
 */
public class TestOrderSingleton {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程回调对象
        ThreadSingleton threadSingletonA = new ThreadSingleton();
        ThreadSingleton threadSingletonB = new ThreadSingleton();

        // 创建异步任务
        FutureTask<OrderSingleton> taskA = new FutureTask<OrderSingleton>(threadSingletonA);
        FutureTask<OrderSingleton> taskB = new FutureTask<OrderSingleton>(threadSingletonB);

        // 创建执行线程
        Thread threadA = new Thread(taskA);
        Thread threadB = new Thread(taskB);

        // 调用start()方法
        threadA.start();
        threadB.start();

        // 获取回调单例对象
        OrderSingleton orderSingletonA = taskA.get();
        OrderSingleton orderSingletonB = taskB.get();

        // 判断两个单例对象是否相等
       System.out.println("两个实例对象是否相等:"+orderSingletonA.equals(orderSingletonB));
    }
}


4.1.2 测试结果

从检测结果可以看出,DCL单例中,通过多线程获取对象,比较两个对象,他们相等。这都要归功于使用volatile修饰。那么volatile是如何实现有序性的呢,接下来我们分析下。

4.1.3 volatile有序性原理分析

4.1.3.1 使用volatile修饰DCL单例的原因

对象的初始化一般经历一下三个步骤:

1、为对象分配内存

2、初始化实例对象

3、为对象的引用分配内存

以上步骤全部完成才算完成了对象的初始化。

DCL单例中,懒汉模式下,如果不使用volatile修饰,由于JVM为了优化指令,提高程序运行效率,允许指令重排序,如果JVM优化指令为1、3、2顺序执行,那么多线程同时执行任务时,如果指令3刚好执行完,指令2未来及执行,其他线程调getInstance()执行任务时就会抛出对象未被初始化的异常。所以我们要使用volatile修饰单例实例。(很难复现出来问题,好尴尬,一直尝试复现没复现出来.............)

4.1.3.2 查看字节码

从以上截图我们可以看到,使用volatile修饰的变量,会多一个ACC_VOLATILE 指令关键字。我们接着去hotspot 查看c++源码,分析ACC_VOLATILE做了些什么操作。

4.1.3.3 hotspot 层面

根据ACC_VOLATILE指令关键字,我们可以在hotspot 源码中,找到他的内容:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/utilities/accessFlags.hpp

 

接着,我们找下is_volatile:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/interpreter/bytecodeInterpreter.cpp (openjdk.org)

从以上截图的这段代码中可以看到,会先判断tos_type(volatile变量类型),后面有不同的基础类型的调用,比如int类型就调用release_int_field_put,byte就调用release_byte_field_put等等。
判断完类型之后,我们可以看到代码后面执行的语句是:

我们可以在以下代码位置找到该源码:

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/runtime/orderAccess.hpp (openjdk.org)

实际上storeload() 这个方法,针对不同CPU有不同的实现,它的具体实现在src/os_cpu下,我们可以去看一下:

这里我们以linux_x86架构的CPU实现为例,我们去看下storeload()方法做了些什么操作。

jdk8u/jdk8u/hotspot: 69087d08d473 src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp (openjdk.org)

接着看下fence()函数:

通过这面代码可以看到lock;add1,其实这个就是内存屏障。lock;add1 $0,0(%%esp)作为cpu的一个内存屏障。
add1 $0,0(%%rsp)表示:将数值0加到rsp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,rsp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用add1指令来配合lock指令,用作cpu的内存屏障。

内存屏障:

这四个分别对应了经常在书中看到的JSR规范中的读写屏障

  • LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

对于volatile操作而言,其操作步骤如下:

  • 每个volatile写入之前,插入一个 StoreStore ,写入以后插入一个StoreLoad,JMM会将当前线程对应的CPU缓存及时的刷新到主内存中。
  • 每个volatile读取之前,插入一个 LoadLoad ,读取之后插入一个LoadStore,JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量。
4.1.3.4 volatile 有序性原理总结

通过分析编译代码可知,使用volatile修饰的变量,会生成ACC_VOLATILE 指令关键字,通过分析hotspot源码可知,ACC_VOLATILE 关键字最终会使用到内存屏障机制,保证使用volatile修饰的内容不被指令重排序,这是保障程序有序性的机制。具体点来说,就是指令重排序的happens-before规范,里面有一条volatile规则,它规定volatile 关键字修饰的变量 , 线程 A 对该变量的写操作 先于 线程 B 读取该变量的操作 , 线程 A 对该变量的写操作的结果对于线程 B 一定可见。

好了,本次内容就分享到这,欢迎关注本博主。如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/449152.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于boost库的搜索引擎项目

文章目录 一、项目背景二、什么样的搜索引擎三、搜索引擎的宏观图原理四、Parse模块4.1下载boost库源代码4.2提取boost库中以.html为结尾的文件4.2.1 boost库的简单使用 4.3数据清洗(去标签化)4.3.1数据清洗的具体实现 4.4将清洗后的数据写入到raw.txt文件中 五、正排索引 vs 倒…

最新android icon和splashScreen适配兼容至2024android

android在12做了splashScreen的变动&#xff0c;即&#xff0c;android12有自带的screenSplash过渡&#xff0c;不论你是否自己有变化&#xff0c;都会插入该动画。 android8做了icon的巨大变动。13做了图标的主题兼容。 一、icon制作 制作 使用android自带的工具&#xff0…

甜甜圈和贪吃蛇的后续

代码复现-项目复现 代码复现 云课五分钟-02第一个代码复现-终端甜甜圈C-CSDN博客 项目复现 云课五分钟-03第一个开源游戏复现-贪吃蛇-CSDN博客 不同的地图 加入班级和标识 循序渐进 这些案例都是来源网络&#xff0c;只是方便熟悉一下云课使用过程。 此部分学生掌握情况非…

阿里云数据湖存储加速套件JindoData

计算存储分离已经成为云计算的一种发展趋势。在计算存储分离之前&#xff0c;普遍采用的是传统的计算存储相互融合的架构&#xff0c;但是这种架构存在一定的问题&#xff0c;比如在集群扩容的时候会面临计算能力和存储能力相互不匹配的问题。用户在某些情况下只需要扩容计算能…

颜色检测python项目

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 什么是颜色检测&#xff1f; 颜色检测是检测任何颜色名称的过程。很简单不是吗&#xff1f;嗯&#xff0c;对于人类来说&#xff0c;这是一项极…

Raspberry Pi树莓派CODESYS PLC控制器解决方案,提供license和实时系统

我们提供正版codesys license和实时系统集成服务。 使用树莓派制作一个CODESYS的PLC控制器 树莓派上实现控制器 1. 安装CodeSys编程环境 1.1 下载CODESYS 1.2 安装CODESYS开发环境 1.3 运行CODESYS开发环境 2. 运行CODESYS开发环境 2.1 创建项目 2.2 编辑程序 2.3 编译程序 2.4…

如何避免MYSQL主从延迟带来的读写问题?

在MYSQL 部署架构选型上&#xff0c;许多公司都会用到主从读写分离的架构&#xff0c;如下是一个一主一从的架构&#xff0c;主库master负责写入&#xff0c;从库slave进行读取。 但是既然是读写分离&#xff0c;必然会面临这样一个问题&#xff0c;当在主库上进行更新后&#…

跨平台大小端判断与主机节序转网络字节序使用

1.macOS : 默认使用小端 ,高位使用高地址,转换为网络字节序成大端 #include <iostream> #include <arpa/inet.h> int main() {//大小端判断union{short s;char c[sizeof(short)];}un;un.s = 0x0102;printf("低地址:%d,高地址:%d\n",un.c[0],un.c[1]);if …

排序类算法

目录 一、交换类排序 1.冒泡排序 2.快速排序 二、 插入排序 1.直接插入排序 2.折半插入排序 3.希尔排序 三、选择排序 1.简单选择排序 2.堆排序 完整代码 四、归并排序 完整代码 五、汇总 六、OJ练习 1.冒泡排序&#xff1a;正确表示前一个数和后一个数 2.选…

线下活动线上同步直播?媒体同步直播的好处

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 线下活动线上同步直播&#xff0c;即利用互联网技术将线下活动的实时内容传输到线上平台&#xff0c;供无法到场的观众在线观看。这种直播方式的好处主要体现在以下几个方面&#xff1a;…

010Editor汉化版+下载+注册码+模板bug

项目场景&#xff1a; 这天我想使用我的不知名的一个破解版本的010Edit来查看一个EXE程序&#xff0c;并想使用模板功能&#xff0c;但是发现没有该模板还无法下载最新模板 问题描述 010Edit联网后需要注册码&#xff1a; 010 Editor 激活码生成器 使用方法 参照教程使用0…

AI新晋王者Claude3完胜ChatGPT4?

1 引言 新王即位&#xff1f;ChatGPT 4跌下神坛? 3月4日&#xff0c;OpenAI 的主要竞争对手之一Anthropic发布了最新的Claude 3系列模型&#xff0c;并宣称claude3系列在推理、数学、编程、多语言理解和视觉方面都树立了新的行业基准。Claude家族包括了Claude 3 Haiku, Clau…

pycharm中连接远程服务器

文章目录 概要文件进行映射将本地的project和远程的project进行映射&#xff0c;一定要使用sftp本地文件和远程的位置配置不想将远程的文件同步时候&#xff0c;可以进行下面设置 配置远程服务器的python解释器需要setting-->python interpreter-->add---> on ssh选择…

车规芯片为什么需要信息安全(1)

目录 1.汽车出现过被黑客攻击事件吗&#xff1f; 2.汽车信息安全标准汇总 2.1 国际标准 2.2 国内标准 3.车规芯片的信息安全应该从什么地方考虑 3.1 芯片硬件安全防护能力 3.2 车规芯片的信息安全服务 3.3 芯片厂如何证明芯片的信息安全能力 4.小结 这个来自家里人的灵…

Spring Cloud Gateway自定义断言

问题&#xff1a;Spring Cloud Gateway自带的断言&#xff08;Predicate&#xff09;不满足业务怎么办&#xff1f;可以自定义断言&#xff01; 先看Spring Cloud Gateway是如何实现断言的 Gateway中断言的整体架构如下&#xff1a; public abstract class AbstractRoutePred…

【pycharm使用ssh连接服务器】

2、pycharm使用ssh连接服务器 1、具体流程2、一些需要注意的小问题2.1 更改代码地址2.2 本地代码上传到服务器2.3 在服务器的环境中上新安装库&#xff0c;但是pycharm检测不到 1、具体流程 打开pycharm – File – Setting 输入服务器的IP地址&#xff0c;端口号、登录账号名…

指令调用模板

也就是这边指令通过id和map会定位到一个结构体&#xff0c;然后这个结构再赋值两个成员&#xff0c;一个是函数一个是指令类型&#xff0c;然后这个函数是模板的实例化 使用的时候就传进去&#xff0c;这只是参数&#xff0c;最开始初始化的时候模板就已经实例化了。然后关于模…

嵌入式软件开发工程师如何提高C语言编码技能?

嵌入式软件开发工程师如何提高C语言编码技能&#xff1f; 在开始前我分享下我的经历&#xff0c;我刚入行时遇到一个好公司和师父&#xff0c;给了我机会&#xff0c;一年时间从3k薪资涨到18k的&#xff0c; 我师父给了一些 电气工程师学习方法和资料&#xff0c;让我不断提升…

[Unity3D]--更换天空盒子

我们原来的天空盒子是这样的。 感觉不是特别满意&#xff0c;想换一个更好看的。 去资源商店找个好看的 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 例如这个 然后在Window>Rendering>Lighting里的环境选项里更换材质 更换&#xff1a; ​ …