Java并发核心问题以及并发三特性原子性、可见性、有序性

news2025/1/21 22:02:13

这篇文章比较长,请耐心看完,相信会让你对并发三大特性有一个较深的理解。

1.原子性(Atomicity)

1.1 原子性定义以及理解

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

一个很经典的例子就是银行账户转账问题:

比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。

 所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。

 

1.2 java的原子性

在Java中,对基本数据类型的变量读取赋值操作是原子性操作。 

a = true;//1
a = 5;//2
a = b;//3
a = b + 2;//4
a ++;//5

上面的5个基本数据类型的操作,只有1和2是原子性的。

a = true:包含一个操作,1.将true的赋值给a。
a = 5:包含一个操作,1.将5的赋值给a。
a = b:包含两个操作,1.读取b的值;2.将b的值赋值给a。
a = b + 2:包含三个操作,1.读取b的值;2.计算b+2;3.将b+2的计算结果赋值给a。
a ++:即a = a + 1,包含三个操作,读取a的值;2计算a+1;3.将a+1的计算结果赋值给a

 

1.3 原子性问题

i ++是安全的吗? 

  在多线程的并发下, 此操作是不安全的, 它不是一个原子操作, i ++ 可分为三步三条 CPU 指令 :1.需要把变量 count 从内存加载到工作内存; 2.在工作内存执行 +1 操作;3.将结果写入内存;

 

线程A 读入count之后, 线程B也读入了count, 也就是说, 线程A的三条指令本应是不可分割的,但在这三步操作间又插入了线程B的操作, 致使我们最终得到的结果有误 

/**
 * <p>原子性示例:不是原子性</p>
 *
 * @author lbq 2024/6/05 10:58
 **/
static class Increment {
    private int count = 1;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

下面的代码展示了在多线程环境中,调用此自增器进行自增操作。 

int type = 0;//类型
int num = 50000;//自增次数
int sleepTime = 5000;//等待计算时间
int begin;//开始的值
Increment increment;
//不进行原子性保护的大范围操作
increment = new Increment();
begin = increment.getCount();
LOGGER.info("Java中普通的自增操作不是原子性操作。");
LOGGER.info("当前运行类:" +increment.getClass().getSimpleName() +  ",count的初始值是:" + increment.getCount());
for (int i = 0; i < num; i++) {
    new Thread(() -> {
        increment.increment();
    }).start();
}
//等待足够长的时间,以便所有的线程都能够运行完
Thread.sleep(sleepTime);
LOGGER.info("进过" + num + "次自增,count应该 = " + (begin + num) + ",实际count = " + increment.getCount());

 某次运行结果:

Java中普通的自增操作不是原子性操作。
当前运行类:Increment,count的初始值是:1
进过50000次自增,count应该 = 50001,实际count = 49999

 通过观察结果,发现程序确实存在原子性问题。

 

1.4 原子性保障技术 

在Java中提供了多种原子性保障措施,这里主要涉及三种:

  • 通过synchronized关键字定义同步代码块或者同步方法保障原子性。
  • 通过Lock接口保障原子性。
  • 通过Atomic类型保障原子性。

首先第一种方式就是加锁, Java提供的 synchronized 关键字,就是锁的一种实现 , 我们提供一把锁, 只有某一线程拿到这把锁才能对此变量进行操作, 其他线程进来后发现没有获得锁,只能在外等待, 所以synchronized锁能够很好的保持原子性 ,同样它还可以保持可见性和有序性(因为被锁住只能由一个线程进行操作)

因为篇幅有限,其他俩种不做过多解释,想要了解可以继续查阅资料学习 

 

2.可见性(Visibility) 

 2.1 可见性定义

可见性:当一个线程修改了共享变量的值,其他线程能够看到修改的值。 

 

2.2 java的可见性

场景说明:

  • 存在两个线程A、线程B和一个共享变量stop。
  • 如果stop变量的值是false,则线程A会一直运行。如果stop变量的值是true,则线程A会停止运行。
  • 线程B能够将共享变量stop的值修改为ture。
//普通情况下,多线程不能保证可见性
private static boolean stop;

new Thread(() -> {
    System.out.println("Ordinary A is running...");
    while (!stop) ;
    System.out.println("Ordinary A is terminated.");
}).start();
Thread.sleep(10);

new Thread(() -> {
    System.out.println("Ordinary B is running...");
    stop = true;
    System.out.println("Ordinary B is terminated.");
}).start();

运行结果:

Ordinary A is running...
Ordinary B is running...
Ordinary B is terminated.

 从结果观察,发现线程B运行结束了,也就是说已经修改了共享变量stop的值。但是线程A还在运行,也就是说线程A并没有用接收到stop=true这个修改。

 

2.3 可见性问题

在如今的多核CPU中 , 由于CPU与内存存在速度差异, 所以每个核都有自己的缓存, 缓存是私有的, 这就导致CPU缓存与内存中的数据可能会出现不一致的情况。CPU在自己的缓冲区拿到数据进行写操作之后, 并不会立刻将这个数据更新回内存中, 如果此时来了另一个线程, 它此时应该读入的是上一个线程修改后的值, 但由于不是即时更新的,此时第二个线程拿到的还是原本的值, 也就是说, 这两个线程之间对此变量操作是不可见的, 这就是可见性问题。

 

如上图, 内存中有一个共享变量a=0, 此时线程1 先进入, 操作之后 a=1, 但此时还没来及将这个值更新回内存, 线程2就将 a =0又读入了它里面, 此时两边最终更新的值都为1(原本应为2)。 

 

2.4 可见性保障技术 

在Java中提供了多种可见性保障措施,这里主要涉及四种:

  • 通过volatile关键字标记内存屏障保证可见性。
  • 通过synchronized关键字定义同步代码块或者同步方法保障可见性。
  • 通过Lock接口保障可见性。
  • 通过Atomic类型保障可见性。

 

3. 有序性(orderly)  

3.1 有序性定义

有序性:即程序执行的顺序按照代码的先后顺序执行。 

 

3.2 Java自带的有序性 

在Java中,由于happens-before原则,单线程内的代码是有序的,可以看做是串行(as-if-serial)执行的。但是在多线程环境下,多个线程的代码是交替的串行执行的,这就产生了有序性问题。 

先来看 happens-before 关系的定义:

  • 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果就会对第二个操作可见
  • 两个操作之间如果存在 happens-before 关系,并不意味着 Java 平台的具体实现就必须按照 happens-before 关系指定的顺序来执行.如果重排序之后的执行结果,与按照 happens-before 关系来执行的结果一直,那么 JMM 也允许这样的重排序。

备注: 

as-if-serial 语义保证的是单线程内重排序之后的执行结果和程序代码本身应该出现的结果是一致的, happens-before 关系保证的是正确同步的多线程程序的执行结果不会被重排序改变. 

(什么是as-if-serial ,别着急,最后会介绍重排序,再回来看就行)

一句话来总结就是:如果操作 A happens-before 操作 B ,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程. 

 

Java提供了happens-before原则保证程序基本的有序性,主要规则如下:

线程内部规则:在同一个线程内,前面操作的执行结果对后面的操作是可见的。
同步规则:如果一个操作x与另一个操作y在同步代码块/方法中,那么操作x的执行结果对操作y可见。
传递规则:如果操作x的执行结果对操作y可见,操作y的执行结果对操作z可见,则操作x的执行结果对操作z可见。
对象锁规则:如果线程1解锁了对象锁a,接着线程2锁定了a,那么,线程1解锁a之前的写操作的执行结果都对线程2可见。
volatile变量规则:如果线程1写入了volatile变量v,接着线程2读取了v,那么,线程1写入v及之前的写操作的执行结果都对线程2可见。
线程start原则:如果线程t在start()之前进行了一系列操作,接着进行了start()操作,那么线程t在start()之前的所有操作的执行结果对start()之后的所有操作都是可见的。
线程join规则:线程t1写入的所有变量,在任意其它线程t2调用t1.join()成功返回后,都对t2可见。
而有序性问题,都是发生在happens-before原则之外的状况。

 

3.3 有序性问题  

 编译器为了优化性能,有时候会改变程序中语句的先后顺序。

例如程序中:“a=6;b=7;” 编译器优化后可能变成 “b=7;a=6;”

在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的Bug。

这句红色字体的话在下面重排序会具体解释

 

3.4 有序性问题的解决

在Java中提供了多种有序性保障措施,这里主要涉及四种:

  • 通过volatile关键字标记内存屏障保证可见性。
  • 通过synchronized关键字定义同步代码块或者同步方法保障可见性。
  • 通过Lock接口保障可见性。

补充扩展-重排序问题

1. 重排序概念

“缓存不能及时刷新“(2.3 可见性问题)和“编译器为了优化性能而改变程序中语句的先后顺序”(3.3 有序性问题  )都是重排序的一种。

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

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

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

2.指令级并行的重排序。处理器将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3.内存系统的重排序。处理器使用缓存和读/写缓冲区,使得加载和存储操作看上去可能是在乱序执行。

举例:如下代码执行过程中,程序不一定按照先A后B的顺序执行,经重排序之后可能按照先B后A的顺序执行。

int a = 1;// A
int b = 2;// B 

2. 重排序规则 

重排序需要遵守一定规则,以保证程序正确执行。 

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。 

存在数据依赖性的三种情况: 

① 写后读:a = 1;b = a; 写一个变量之后,再读这个位置。
② 写后写:a = 1;a = 2; 写一个变量之后,再写这个变量。
③ 读后写:a = b;b = 1;读一个变量之后,再写这个变量。 

存在数据依赖关系的两个操作,不可以重排序。

数据依赖性只针对单个处理器中执行的指令序列和单个线程中执行的操作。

举例:

同一个线程中执行a=1;b=1; 不存在数据依赖性,可能重排序。
同一个线程中执行a=1;b=a; 存在数据依赖性,不可以重排序。

 

重排序遵守as-if-serial 语义

as-if-serial 语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。

 举例,以计算圆的面积为例:

double pi = 3.14; // A
double r  = 1.0;  // B
double area = pi * r * r; // C

 A和B重排序之后,程序的执行结果不会改变,所以允许A、B重排序。A和C重排序之后,程序的执行结果会改变,所以不允许A、C重排序。

所以在这里,我看来,遵守数据依赖性和as-if-serial 语义实质上是一回事。为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。

 

3. 重排序带来的问题 

 重排序可以提高程序执行的性能,但是代码的执行顺序改变,可能会导致多线程程序出现可见性问题和有序性问题

备注:指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

例如,在两个并发线程A和B中,如果线程A有三个操作A1、A2、A3,线程B有三个操作B1、B2、B3,并且线程A正确释放锁后线程B获取同一把锁,那么在顺序一致性模型下,两个线程会观察到一个整体有序的操作序列,如 A1->A2->A3->B1->B2->B3。 

 

然而,实际硬件和编译器并不遵循如此严格的顺序一致性模型,而是允许一定的指令重排序以提升性能。 

就可能出现下面的情况: 

 

 

4.volatile 关键字

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。 

当一个变量定义为 volatile 之后,将具备两种特性: 

 1.保证此变量对所有的线程的可见性,这里的“可见性”,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。

    2.禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)。

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C/C++学习笔记 C读取文本文件

1、简述 要读取文本文件&#xff0c;需要按照以下步骤操作&#xff1a; 首先&#xff0c;使用该函数打开文本文件fopen()。其次&#xff0c;使用fgets()或fgetc()函数从文件中读取文本。第三&#xff0c;使用函数关闭文件fclose()。 2、每次从文件中读取一个字符 要从文本文…

康谋技术 | 自动驾驶:揭秘高精度时间同步技术(一)

众所周知&#xff0c;在自动驾驶中&#xff0c;主要涵盖感知、规划、控制三个关键的技术层面。在感知层面&#xff0c;单一传感器采集外界信息&#xff0c;各有优劣&#xff0c;比如摄像头采集信息分辨率高&#xff0c;但是受外界条件影响较大&#xff0c;一般缺少深度信息&…

谢宁DOE培训的奇妙之旅:从陌生到熟练

在充满挑战与机遇的现代社会&#xff0c;不断提升自我&#xff0c;掌握新的技能和知识&#xff0c;成为了我们追求进步的重要途径。而对于我来说&#xff0c;参加谢宁DOE培训&#xff0c;无疑是我职业生涯中的一次重要抉择。这次培训让我从对谢宁DOE陌生到熟练&#xff0c;经历…

csrf漏洞与ssrf漏洞

环境&#xff1a;用kali搭建的pikachu靶场 一.CSRF 1.CSRF漏洞简介 跨站请求伪造&#xff08;CSRF&#xff09;漏洞是一种Web应用程序安全漏洞&#xff0c;攻击者通过伪装成受信任用户的请求来执行未经授权的操作。这可能导致用户在不知情的情况下执行某些敏感操作&#xff0…

【python深度学习】——torch.einsum|torch.bmm

【python深度学习】——torch.einsum|torch.bmm 1. 基本用法与示例2. torch.bmm 1. 基本用法与示例 基本用法: torch.einsum(equation, *operands)equation: 一个字符串&#xff0c;定义了张量操作的模式。 使用逗号来分隔输入张量的索引&#xff0c;然后是一个箭头&#xff…

38. 【Java教程】日期和时间处理

本小节我们将学习 Java 中的日期和时间&#xff0c;日期和时间在我们的实际开发中非常常用&#xff0c;例如用户的注册、数据的增删改、对敏感信息的操作等等都需要记录下日期和时间。通过本小节的学习&#xff0c;你将了解到什么是日期、什么是时间、什么是时区&#xff0c;Ja…

ru域名如何申请ssl证书

SSL证书是一种数字证书&#xff0c;通过它可以在客户端和服务器之间建立加密通道&#xff0c;保证数据在传输过程中的安全性。对于.ru域名来说&#xff0c;申请SSL证书可以有效提升网站的安全性&#xff0c;增强用户对网站的信任度&#xff0c;提高网站的排名和权重。今天就随S…

计算机网络 —— 数据链路层(VLAN)

计算机网络 —— 数据链路层&#xff08;VLAN&#xff09; 什么是VLAN为什么要有VLANVLAN如何实现IEEE 802.1Q 我们今天来看VLAN&#xff1a; 什么是VLAN VLAN&#xff08;Virtual Local Area Network&#xff0c;虚拟局域网&#xff09;是一种网络技术&#xff0c;它将一个物…

clickhouse学习笔记(一)入门与安装

目录 一 、入门 简介 核心特性包括 1.1 列式存储 1.2 原生压缩 1.3 向量化执行引擎 1.4 DBMS 功能 1.5 分布式处理 1.6 高吞吐写入能力 1.7 实时分析 1.8 SQL支持 1.9 高度可扩展 1.10 数据分区与线程级并行 1.11 应用场景 1.12 不适用场景 二、ClickHouse单机版…

LabVIEW中PID控制器系统的噪声与扰动抑制策略

在LabVIEW中处理PID控制器系统中的噪声和外部扰动&#xff0c;需要从信号处理、控制算法优化、硬件滤波和系统设计四个角度入手。采用滤波技术、调节PID参数、增加前馈控制和实施硬件滤波器等方法&#xff0c;可以有效减少噪声和扰动对系统性能的影响&#xff0c;提高控制系统的…

View->可拖拽滑动的ImageView + Fling惯性滑动效果 + 回弹效果

XML文件 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:o…

牛客热题:矩阵最长递增路径

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;力扣刷题日记 &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 文章目录 牛客热题&#xff1a;矩阵最长递增路径题目链接方法一…

铸铁机械5G智能工厂工业物联数字孪生平台,推进制造业数字化转型

铸铁机械5G智能工厂工业物联数字孪生平台&#xff0c;推进制造业数字化转型。工业物联数字孪生平台以5G技术为基础&#xff0c;通过工业物联网连接铸铁机械生产过程中的各个环节&#xff0c;运用数字孪生技术构建虚拟工厂&#xff0c;实现生产过程的实时监测、模拟与优化&#…

人工智能期末复习

&#x1f4cd;人工智能概论期末复习✔️ 知识表示与知识图谱⭐⭐ 知识的特性 相对正确性 不确定性 可表示性与可利用性 知识表示 将人类知识形式化或者模型化。 选择知识表示方法的原则 &#xff08;1&#xff09;充分表示领域知识。 &#xff08;2&#xff09;有利于对…

在线测宽仪的发展历程!

在线测宽仪的发展历程可以归纳为以下几个阶段&#xff1a; 光机扫描式测宽仪阶段&#xff1a; 时间&#xff1a;70年代以前 技术特点&#xff1a;通过机械旋转狭缝机构的扫描&#xff0c;由光电倍增管输出信号&#xff0c;经模拟信号处理得到测量结果。 国内应用&#xff1a;…

全光谱led灯的危害有哪些?曝光低质量全光谱led灯产生的四大风险

眼睛是人类获取信息最重要的感官器官之一&#xff0c;而近视则会导致视力模糊&#xff0c;进而影响学习效果和生活品质。因此&#xff0c;如何保护眼睛&#xff0c;尤其是在学习和使用电子设备时&#xff0c;成为了一个迫切需要解决的问题。然而在护眼领域上&#xff0c;护眼台…

Linux C语言: 数据类型

一、 为什么要引入数据类型 • 计算机中每个字节都有一个地址&#xff08;类似门牌号&#xff09; • CPU通过 地址 来访问这个字节的空间 0x20001103 1 0 0 1 0 0 1 1 0x20001102 1 1 1 0 1 1 1 0 0x20001101 1 1 1 1 0 1 0 1 0x20001100 0 …

边缘计算网关助力自动洗车机实现远程状态监测与即时报警

随着城市化进程的加快和人们生活水平的提高&#xff0c;自动洗车机作为一种高效、便捷的洗车设备&#xff0c;在市场上的需求日益增长。然而&#xff0c;自动洗车机作为一种高价值的自动化设备&#xff0c;其运行状态和安全性直接关系到洗车质量和顾客体验&#xff0c;因此对自…

Mitmproxy作为瑞士军刀可拦截、检查、修改和重放网络流量可用于渗透测试。

Mitmproxy是一个开源的中间人代理工具&#xff0c;用于拦截、修改和查看HTTP和HTTPS流量。它可以用于调试、测试和分析网络应用程序和移动应用程序的通信。 Mitmproxy可以在本地计算机上作为一个代理服务器运行&#xff0c;将所有流量导向到它&#xff0c;然后可以查看和修改这…

UIKit之图片轮播器Demo

需求 实现图片轮播器&#xff0c;搭配页面指示器、可以自动轮播。 注意计时器优先级问题 分析 需要UIScrollView组件、指示器UIPageControl。此外自定义类需要实现代理自动滚动需要监控当前屏幕的offsetx。防止拖拽自动滚动时一下子翻滚太多的BUG&#xff1a;拖拽时&#x…