【Java 并发】三大特性

在 Java 的高并发中,对于线程并发问题的分析通常可以通过 2 个主核心进行分析

  1. JMM 抽象内存模型和 Happens-Before 规则
  2. 三大特性: 原子性, 有序性和可见性

JMM 抽象内存模型和 Happens-Before 规则, 前面我们讨论过了。这里讨论一下三大特性。

1 原子性

定义: 一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断要么就都不执行

简单地说就是一个操作被定义为原子性了, 那么不管这个操作里面包含多少个步骤, 他们都是一个整体的, 这个整体只有执行成功, 或不执行, 不存在任何的中间态, 比如只执行一半, 或者一半成功, 一半失败。

而回到 Java 中, 很多操作看起来就一个操作, 好像是具备原子性, 但是实际中却不具备原子性。

猜猜下面的操作哪些是原子操作?

// 1
int a = 1;

// 2
a++;

// 3
int b = a + 1;

// 4
a = a + 1;

答案是: 只有第一个。

a++ 操作可以拆分为下面 3 步:

  1. 读取 a 的值
  2. a 的值加 1
  3. 将计算后的值重新赋值给 a

其他 2 个的分析类似。

1.1 原子操作

在 Java 内存模型中定义了 8 种原子操作

操作作用对象说明
lock (锁定)主内存中的变量把一个变量标识为一个线程独占的状态
unlock (解锁)主内存中的变量把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取)主内存的变量把一个变量的值从主内存传输到线程的工作内存中,以便后面的 load 动作使用
load (载入)工作内存中的变量把 read 操作从主内存中得到的变量值放入工作内存中的变量副本
use (使用)工作内存中的变量把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作
assign (赋值)工作内存中的变量把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store (存储)工作内存的变量把工作内存中一个变量的值传送给主内存中以便随后的write操作使用
write (操作)主内存的变量把 store 操作从工作内存中得到的变量的值放入主内存的变量中

上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。

需要注意的一点就是: 指令与指令之间组合起来达到某个效果。
但是 Java 内存模型只要求这些组合指令之间是顺序执行的,不强制他们一定是连续执行的。

比如把一个变量从主内存中复制到工作内存中就需要执行 read, load 操作,将工作内存同步到主内存中就需要执行 store, write 操作。
也就是说 read 和 load 之间可以插入其他指令,store 和 writer 可以插入其他指令。
比如对主内存中的 a, b 进行访问就可以出现这样的操作顺序: read a, read b, load b, load a

支持变量操作的原子操作的有 read, load, use, assign, store, write。 基础数据类型比较简单, 基本只有使用到其中的一条, 所以可以看为基本数据类型的访问读写具备原子性 (long 和 double 的操作不具备操作性), 如上面的 int a = 1;

1.2 synchronized 和 volatile 对原子性的支持

synchronized

上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下 lock 和 unlock 两条原子操作。这 2 个可以用于支持更大范围的原子性操作。
尽管 JVM 没有把 lock 和 unlock 开放给我们使用,但 JVM 以更高层次的指令 monitorenter 和 monitorexit 指令开放给我们使用,
映射到 Java 代码中就是— synchronized 关键字, 也就是说 synchronized 满足原子性。

public void test() {
    int b = 0;
    synchronized(Test.class) {
        int a = 1;
        b = a;
    }
}

volatile

而说到关键字, Java 中另一个和并发相关的高频关键字 volatile, 是否可以保证原子性了。
先举一个例子。

public class VolatileExample {

    private static volatile int counter = 0;
 
    public static void main(String[] args) {

        // 启动 10 个线程
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

如上: counter 是 volatile 修饰的。
开启 10 个线程,每个线程都自加 10000 次,如果不出现线程安全的问题最终的结果应该就是:10 * 10000 = 100000。
但是运行多次都是小于 100000 的结果, 也就是说明: volatile 不能保证原子性

从上面的说明可以知道 counter++ 不是原子性操作。如果线程 A 读取 counter 到工作内存后,其他线程对这个值已经做了自增操作后,那么线程 A 的这个值自然而然就是一个过期的值,因此,总结果必然会是小于 100000 的。

如果让 volatile 保证原子性,必须符合以下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值
  2. 变量不需要与其他的状态变量共同参与不变约束 (volatile 变量的变化不会与其它变量的变化有任何联系)

2 有序性

定义: 程序执行的顺序按照代码的先后顺序执行。

例如:

int i = 0;              
boolean flag = false;
i = 1;                // 语句1  
flag = true;          // 语句2

先给变量 i 赋值,然后给 flag 赋值,语句 1 在 语句 2 的前面。
但是在编译器和处理器可能会为了性能对其进行重排序 (指令重排序), 因为语句 1 和 语句 2 之间没有依赖关系,所以语句 2 可能被重排序到语句 1 的前面。

2.1 synchronized 和 volatile 对有序性的支持

synchronized

synchronized 语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。
因此, synchronized 语义就要求线程在访问读写共享变量时只能 “串行” 执行, 因此 synchronized 具有有序性。

但是在 synchronized 内部的代码块的逻辑, JVM 没有禁止重排序, 也就是支持处理器为了性能, 在不影响结果的情况下, 调整执行顺序。

例子:

public void test() {
    int b = 0;
    synchronized(Test.class) {
        int a = 1;
        // 1
        b = a;
        // 2
        int c = a;
    }
}

上面 1,2 2 个操作没有存在结果的依赖, 如果为了性能, 处理器仍然可以对他们进行重排序, 变为 2, 1 的执行顺序。

volatile

在 Java 内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序。
也就是说 Java 程序天然的有序性可以总结为:如果在本线程内观察,所有的操作都是有序的, 如果在一个线程观察另一个线程,所有的操作都是无序的。那么 volatile 具备有序性吗?

先看一个例子:volatile 和 双重检验锁定的方式(Double-checked Locking)的关系:

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() { }

    public Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){

                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这里的 instance 为什么要加 volatile 修饰?
创建一个对象,实际是经过 3 步实现的

  1. 分配对象内存空间
  2. 初始化对象
  3. 将对象指向我们刚刚分配的内存

但是不加 volatile 在重排序的作用下, 可能会出现下面的执行顺序:

Alt 'ObjectInitWithoutVolatile'

如果 2 和 3 进行了重排序的话,线程 B 进行判断 if( instance == null) 时就会为 true,而实际上这个 instance 并没有初始化成功,显而易见对线程 B 来说之后的操作就会是错的。
而用 volatile 修饰的话,可以禁止 2 和 3 操作重排序,从而避免这种情况。volatile 包含禁止指令重排序的语义, 其具有有序性。

3 可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

// 线程 1 执行的代码
int i = 0;
i = 10;
 
// 线程 2 执行的代码
j = i;

假若执行线程 1 的是 CPU1,执行线程 2 的是 CPU2。由上面的分析可知:
当线程 1 执行 i = 10 这句时,会先把 i 的初始值加载到 CPU1 的本地缓存,然后赋值为 10,那么在 CPU1 的本地缓存中把 i 的值变为 10,却没有立即写入到主存当中。

此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到 CPU2 的本地缓存当中,此时内存当中 i 的值还是 0,那么就会使得 j 的值为 0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。

3.1 synchronized 和 volatile 对有序性的支持

synchronized
通过对 synchronized 的内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。所以 synchronized 具有可见性 (Happens-Before 的 Monitor 规则保证)。

volatile

volatile 修饰的变量在写的时候,底层会在后面在添加 lock 指令,确保将修改的值刷新到主内存,所以 volatile 具备可见性。

4 总结

synchronized 具有: 原子性, 有序性, 可见性。

volatile 具有:有序性,可见性。

synchronized 是否保证有序性呢? 从上面的双重检测看起来, synchronized 貌似不保证有序性, 但是 synchronized 还是保证有序性的, 只是和 volatile 的有序性不一样。

volatile 关键字禁止 JVM 编译器和处理器对其进行重排序, 而 synchronized 保证的有序性是只有单线程可以获取锁, 串行地执行同步代码的结果, 但是同步代码里的语句是会发生指令重排序。
进入 synchronized 代码块前, 底层先添加一个 acquire barrier, 在最后添加一个 release barrier, 保证同步代码块中的代码不能和同步代码块外面的代码进行指令重排, 在其内部还是会发生指令重排但基本不会影响结果。


public void testSynchronized() {

    // 1
    int a = 1;

    // 2
    synchronized(TestDemo.class) {
        // 2.1
        a = 2;
        // 2.2
        int b = 3;
        // 2.3
        int c = 4;
    }

    // 3
    a = 3;
}

将 synchronized 内的代码块看着整个整体, 在 synchronized 的作用下, 1, 2, 3 是有序的,
但是 synchronized 不保证代码块内的代码是有序的, 在没有数据依赖的条件下, 运行指令重排序, 也就是可能存在 2.1 - 2.3 - 2.2 等情况。

5 参考

三大性质总结:原子性、可见性以及有序性

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

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

相关文章

Docker部署MinIO对象存储服务器结合内网穿透实现远程访问

文章目录 前言1. Docker 部署MinIO2. 本地访问MinIO3. Linux安装Cpolar4. 配置MinIO公网地址5. 远程访问MinIO管理界面6. 固定MinIO公网地址 前言 MinIO是一个开源的对象存储服务器&#xff0c;可以在各种环境中运行&#xff0c;例如本地、Docker容器、Kubernetes集群等。它兼…

飞天使-jumpserver-docker跳板机安装

文章目录 jumpserverdocker 更新到最新下载安装包mysql启动mysql 命令 验证字符集,创建数据库使用jumpserver 进行连接测试 redis部署jumpserver 写入变量建jumpserver 容器正确输出登录验证 jumpserver 基础要求 硬件配置: 2 个 CPU 核心, 4G 内存, 50G 硬盘&#xff08;最低…

2023.12.18Linux部署项目

动态查看最新内容 防火墙不能杀毒&#xff0c;只能限制服务器的哪些端口可以被访问 哪些主机可以访问本服务器 防火墙开启之后默认封闭所有端口&#xff0c;自己再用策略声明把哪些端口放开 ksh jdk&#xff1a;二进制包 MySQL&#xff1a;rpm包 Redis&#xff1a;源码…

PyQt6 QFrame分割线控件

锋哥原创的PyQt6视频教程&#xff1a; 2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 PyQt6 Python桌面开发 视频教程(无废话版) 玩命更新中~共计46条视频&#xff0c;包括&#xff1a;2024版 PyQt6 Python桌面开发 视频教程(无废话版…

hping3

Hping3 Hping3的介绍&#xff1a; 是一款网络的测试工具&#xff0c;一般用于网络安全员用来进行防火墙的测试等抗压测试。 Hping3的帮助面板: -h –help显示帮助 -v –version显示版本信息 -c –count 限制发包数 -i –interval nterval 指定发包间隔为多少毫秒&#…

Uniapp上传下载文件-不限制文件类型-附详细代码解析

Uniapp上传下载文件&#xff0c;不限制文件类型 1 知识小课堂1.1 Uniapp简介1.2 文件上传1.3 文件下载 2 Uniapp上传文件3 Uniapp 下载文件 1 知识小课堂 1.1 Uniapp简介 UniApp是一款跨平台应用程序开发框架&#xff0c;它允许开发者使用同一套代码来构建基于多个操作系统的应…

以低成本实现高转化:搭建年入百万的知识付费网站的技巧与方法

明理信息科技知识付费平台 一、引言 随着知识经济的崛起&#xff0c;越来越多的知识提供者希望搭建自己的知识付费平台。然而&#xff0c;对于新手来说&#xff0c;如何以低成本、高效率地实现这一目标&#xff0c;同时满足自身需求并提高客户转化率&#xff0c;是一大挑战。…

Ubuntu 常用命令之 cat 命令用法介绍

cat是一个常用的命令行工具&#xff0c;它用于连接和显示文件的内容。cat这个名字来源于它的功能 - concatenate(连接)。 以下是cat命令的一些基本用法 &#x1f447;显示文件内容&#xff1a;cat后面跟上文件名&#xff0c;就可以在终端显示出文件的内容。例如&#xff0c;c…

3分钟让你学会axios在vue项目中的基本用法(建议收藏)

目录 Axios Axios简介 一、axios是干啥的 二、安装使用 三、Axios请求方式 1、axios可以请求的方法&#xff1a; 2、get请求 3、post请求 4、put和patch请求 5、delete请求 6、并发请求 四、Axios实例 1、创建axios实例 2、axios全局配置 3、axios实例配置 4、…

Swift爬虫采集唯品会商品详情

我有个朋友之前在唯品会开的店&#xff0c;现在想转战其他平台&#xff0c;想要店铺信息商品信息全部迁移过去&#xff0c;如果想要人工手动操作就有点麻烦了&#xff0c;然后有天找到我 &#xff0c;让我看看能不能通过技术手段实现商品信息迁移。嫌来无事&#xff0c;写了下面…

【华为】文档中命令行约定格式规范(命令行格式规范、命令行行为规范、命令行参数格式、命令行规范)

文章目录 命令行约定格式**粗体&#xff1a;命令行关键字***斜体&#xff1a;命令行参数*[ ]&#xff1a;可选配置{ x | y | ... } 和 [ x | y | ... ]&#xff1a;选项{ x | y | ... }* 和 [ x | y | ... ]*&#xff1a;多选项&<1-n>&#xff1a;重复参数#&#xff…

ROS-tf2功能包安装

首先使用 rospack find tf2_tools 查看是否安装了 tf2_tools&#xff0c;如果没有则安装 但直接采用 sudo apt install tf2_tools 是无法安装成功的&#xff0c;会显示 E: 无法定位软件包 tf2_tools 使用下面的命令安装 sudo apt install ros-melodic-tf2-tools&#xff08;…

区域和检索算法(leetcode第303题)

题目描述&#xff1a; 给定一个整数数组 nums&#xff0c;处理以下类型的多个查询:计算索引 left 和 right &#xff08;包含 left 和 right&#xff09;之间的 nums 元素的 和 &#xff0c;其中 left < right 实现 NumArray 类&#xff1a;NumArray(int[] nums) 使用数组…

关于车轮螺母的拧紧力矩——SunTorque智能扭矩系统

车轮螺母是汽车的重要部件之一&#xff0c;其拧紧力矩的大小直接影响到车辆的安全性和稳定性。因此&#xff0c;开发一种准确、可靠的车轮螺母拧紧力矩计算方法对于提高汽车制造质量具有重要意义。SunTorque智能扭矩系统将从车轮螺母拧紧力矩的开发和计算两个方面进行探讨。 一…

单元测试计划、用例、报告、评审编制模板

单元测试支撑文档编制模板&#xff0c;具体文档如下&#xff1a; 1. 单元测试计划 2. 单元测试用例 3. 单元测试报告 4. 编码及测试评审报告 软件项目相关资料全套获取&#xff1a;软件项目开发全套文档下载-CSDN博客 1、单元测试计划 2、单元测试用例 3、单元测试报告 4、编码…

售前解决方案工程师在项目到底有多大价值?

售前解决方案工程师在项目中的价值是非常重要的&#xff0c;虽然其具体价值难以量化&#xff0c;但可以从以下几个方面来体现&#xff1a; 1、提升项目成功率&#xff1a;售前工程师在项目初期就能够深入了解客户需求&#xff0c;通过技术交流、解决方案设计等方式&#xff0c;…

数字滤波器设计——Matlab实现数字信号处理<1>

目录 一.实验内容 二.代码分析 1.信号产生部分 2.利用傅立叶级数展开的方法&#xff0c;自由生成所需的x(t) 3.通过选择不同的采样间隔T&#xff08;分别选T>或<1/2fc&#xff09;&#xff0c;从x(t)获得相应的x(n) 3.对获得的不同x(n)分别作傅立叶变换&#xff0c…

LeedCode刷题---二分查找类问题

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C/C》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、二分查找 题目链接&#xff1a;二分查找 题目描述 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一…

刚入行的嵌入式新人是否值得坚持嵌入式方向?

今日话题&#xff0c;刚入行的嵌入式新人是否值得坚持嵌入式方向&#xff1f;如果你正在学习C语言或者嵌入式方向&#xff0c;坚持下去是一个明智的选择。嵌入式行业涉及硬件&#xff0c;技术更新相对较慢&#xff0c;但这为你积累宝贵的经验提供了机会&#xff0c;与纯软件相比…

JVM日常故障排查小结

前置知识 jstack简介 jstack是JVM自带的工具&#xff0c;用于追踪Java进程线程id的堆栈信息、锁信息&#xff0c;或者打印core file&#xff0c;远程调试Java堆栈信息等。 而我们常用的指令则是下面这条: # 打印对应java进程的堆栈信息 jstack [ option ] pid option常见选…