首页 > 编程语言 >java中的重排序和volatile关键字

java中的重排序和volatile关键字

时间:2023-11-06 11:58:08浏览次数:52  
标签:语句 java 变量 关键字 线程 内存 操作 volatile

一、内存模型基础

1、内存模型描述的是程序中各变量(线程共享变量)的访问规则,以及在实际计算机系统中将变量存储到内存和从内存读取出变量这样的低层细节。
2、Jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。
3、每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。

Example:

线程A与线程B的通信过程如下:

  • 首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去
  • 然后,线程B到主内存中去读取线程A之前已更新过的共享变量

从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM(Java Memory Model)通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

可见性:一个线程对共享变量的修改能够及时的被其他线程看见

二、重排序

为什么要重排序

现在的CPU一般采用流水线来执行指令。一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段。然后,多条指令可以同时存在于流水线中,同时被执行。

指令流水线并不是串行的,并不会因为一个耗时很长的指令在“执行”阶段呆很长时间,而导致后续的指令都卡在“执行”之前的阶段上。

重排序的目的是为了性能。

Example:
理想情况下:
过程A:cpu0—写入1—> bank0;
过程B:cpu0—写入2—> bank1;
如果bank0状态为busy, 则A过程需要等待
如果进行重排序,则直接可以先执行B过程。

数据依赖性

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

分为下面三种情况:

名称示例说明
写后读 a = 1; b = a; 写一个变量后再读这个位置
写后写 a = 1; a = 2; 写一个变量后再写这个变量
读后写 a = b; b = 1; 读一个变量后再写这个变量

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

  • 所以有数据依赖性的语句不能进行重排序。

as-if-serial

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

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

Example:

Ⓐ -> Ⓑ -> Ⓒ 按程序顺序的执行结果:area = 3.14
Ⓑ -> Ⓐ -> Ⓒ 按重排序后的执行结果:area = 3.14

as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,写单线程的程序员有一个幻觉:单线程程序是按程序写的顺序来执行的。

happens-before 规则

语义:如果A先发生于B,那么A所做的所有改变都能被B看到

遵循的规则

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。

重排序对多线程的影响

flag为标志位,表示a有没有被写入,当A线程执行 writer 方法,B线程执行 reader 方法,线程B在执行4操作的时候,能否看到线程A对a的写入操作?

答案是: 不一定!

由于操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序。

如果操作1和操作2做了重排序,程序执行时,线程A首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

三、volatile 关键字

两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile 修饰之后,那么就具备了两层语义:

1、保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

Example:

先看这段代码会完全运行正确么?即一定会将线程中断么?
答案是:不一定!

线程1在运行的时候,会将 stop 变量的值拷贝一份放在自己的工作内存当中。那么当线程2更改了 stop 变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对 stop 变量的更改,因此还会一直循环下去。

如果加上 volatile 则不一样:

  • 使用 volatile 关键字会强制将修改的值立即写入主存。
  • 使用 volatile 关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量 stop 的缓存行无效。
  • 由于线程1的工作内存中缓存变量 stop 的缓存行无效,所以线程1再次读取变量 stop 的值时会去主存读取。

2、禁止进行指令重排序。

  • 当程序执行到 volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

  • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

Example:

由于 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

并且 volatile 关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。

volatile 能保证原子性吗?

不能

Example:

这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

原因在于,自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行,就有可能导致下面这种情况出现:

线程1对变量进行自增操作:线程1先读取变量inc的原始值,然后线程1被阻塞了(还没有 inc 的值);

然后线程2对变量进行自增操作:线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。

然后线程1接着进行加1操作,由于已经读取了inc的值,此时线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。

那么两个线程分别进行了一次自增操作后,inc只增加了1。

根源就在这里,自增操作不是原子性操作。

 

标签:语句,java,变量,关键字,线程,内存,操作,volatile
From: https://www.cnblogs.com/cdlyy/p/17812316.html

相关文章

  • javascript中的时间格式化的方法
     javascript中的时间格式化的方法 Date.prototype.format=function(format){varo={"M+":this.getMonth()+1,//month"d+":this.getDate(),//day"h+":this.getHours(),//hour&quo......
  • 探索 Java 8 中的 Stream 流:构建流的多种方式
    当我们处理集合数据时,往往需要对其进行各种操作,如过滤、映射、排序、归约等。在Java8中引入的Stream流为我们提供了一种更加简洁和灵活的方式来处理数据。上述情况都是流对集合进行操作的,但是对于流的创建操作还是不太了解,其实流的创建不止是使用集合进行创建,还可以基于值、数......
  • 带你理解 Java 8 的函数式接口使用和自定义
    函数式接口是Java8引入的一种接口,用于支持函数式编程。函数式接口通常包含一个抽象方法,可以被Lambda表达式或方法引用所实现。在本文中,我们将深入探讨函数式接口的概念、用途以及如何创建和使用函数式接口。什么是函数式接口函数式接口是只包含一个抽象方法的接口。但是默认方......
  • 解锁多核处理器的力量:探索数据并行化在 Java 8 Stream 中的应用
    在Java8中引入的Stream为集合数据的处理带来了现代化的方式,而数据并行化则进一步提升了处理速度,充分发挥了多核处理器的优势。本篇博客将详细介绍数据并行化在Java8Stream中的应用,以及如何利用并行流处理大量数据。什么是数据并行化数据并行化是指将任务分解成多个子任务,......
  • Java 获取自定义注解 字段值 及 注解值
    自定义注解packagecom.jianmu.bean.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;/**[email protected]......
  • volatile如何防止指令重排和保证有序性
    在多线程的世界里,一共有三个问题:原子性问题、可见性问题、有序性问题。整个java并发体系也是围绕着如何解决这三个问题来设计的。volatile关键字也不例外,我们都知道它解决了可见性和有序性,但是不能保证原子性。这篇文章也主要基于其中一个特性,也就是研究一下volatile是如何保证有......
  • java随机数
    在Java中生成随机数可以使用java.util.Random类或者java.lang.Math类的静态方法,也可以使用Java8引入的java.util.concurrent.ThreadLocalRandom类。参考文章:java怎么产生随机数使用java.util.Random类:importjava.util.Random;publicclassMain{publicstaticvoid......
  • java数组最大值
    参考文章:java数组求最大值在Java中,你可以通过遍历数组元素来找到数组中的最大值。以下是两种常见的方法:使用循环遍历数组publicclassMain{publicstaticvoidmain(String[]args){int[]array={10,5,8,2,7};//假设数组的第一个元素是最大......
  • Shell系列---【常用脚本之---java启动脚本:run.sh】
    run.sh#!/bin/sh-lapplication=node_exporterPORT=1888CUR_IP=${hostname-I|awk'{print$1}'}command="/opt/app/middles/node_exporter/node_exporter--web.listen-address=:$PORT"#停止stop(){ echo"=============================......
  • Java设计模式之桥接模式
    桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。假如你有一个几何形状(Shape)类,从它能扩展出两个子类:圆形(Circle)和方形(Square)。你希望对这样的类层次结构进行扩展以使其包含颜色,所......