java并发编程六 共享模型之内存

news2024/11/24 15:01:52

文章目录

    • Java 内存模型
    • 可见性
      • 解决方法
    • 有序性
      • 解决方法

Java 内存模型

JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等。

JMM 体现在以下几个方面

  • 原子性 - 保证指令不会受到线程上下文切换的影响
  • 可见性 - 保证指令不会受 cpu 缓存的影响
  • 有序性 - 保证指令不会受 cpu 指令并行优化的影响

可见性

退不出的循环
先来看一个现象,main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止:

static boolean run = true;
 public static void main(String[] args) throws InterruptedException {
 Thread t = new Thread(()->{
 while(run){
 // ....
        }
    });
 t.start();
 }
 sleep(1);
 run = false; // 线程t不会如预想的停下来

为什么呢?分析一下:

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
    在这里插入图片描述

  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
    在这里插入图片描述

  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述

解决方法

volatile(易变关键字)
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
可见性 vs 原子性
前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对 volatile 变量的修改对另一个线程可见, 不能保证原子性,仅用在一个写线程,多个读线程的情况: 上例从字节码理解是这样的:

getstatic     run   // 线程 t 获取 run true 
getstatic     run   // 线程 t 获取 run true 
getstatic     run   // 线程 t 获取 run true 
getstatic     run   // 线程 t 获取 run true 
putstatic     run  //  线程 main 修改 run 为 false, 仅此一次 
getstatic     run   // 线程 t 获取 run false 

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,只能保证看到最新值,不能解决指令交错

// 假设i的初始值为0 
getstatic     i  // 线程2-获取静态变量i的值 线程内i=0 
getstatic     i  // 线程1-获取静态变量i的值 线程内i=0 
iconst_1         // 线程1-准备常量1 
iadd             // 线程1-自增 线程内i=1 
putstatic     i  // 线程1-将修改后的值存入静态变量i 静态变量i=1 
iconst_1         // 线程2-准备常量1 
isub             // 线程2-自减 线程内i=-1 
putstatic     i  // 线程2-将修改后的值存入静态变量i 静态变量i=-1 

注意 synchronized 语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized 是属于重量级操作,性能相对更低如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到

有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
 // 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...; 
j = ...;

也可以是

j = ...;
i = ...; 

这种特性称之为『指令重排』,多线程下『指令重排』会影响正确性。为什么要有重排指令这项优化呢?从 CPU执行指令的原理来理解一下吧

  • 原理之指令级并行
    诡异的结果
int num = 0;
boolean ready = false;
 // 线程1 执行此方法
public void actor1(I_Result r) {
 if(ready) {
 r.r1 = num + num;
    } 
else {
 r.r1 = 1;
    }
 }
 // 线程2 执行此方法
public void actor2(I_Result r) {        
num = 2;
 ready = true;    
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?
有同学这么分析
情况1:线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
情况2:线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
情况3:线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
但我告诉你,结果还有可能是 0 😁😁😁,信不信吧!
这种情况下是:线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2
相信很多人已经晕了 😵😵😵
这种现象叫做指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现:
借助 java 并发压测工具 jcstress https://wiki.openjdk.java.net/display/CodeTools/jcstress

mvn archetype:generate  -DinteractiveMode=false -DarchetypeGroupId=org.openjdk.jcstress - DarchetypeArtifactId=jcstress-java-test-archetype -DarchetypeVersion=0.5 -DgroupId=cn.onenewcode DartifactId=ordering -Dversion=1.0 

创建 maven 项目,提供如下测试类

@JCStressTest
 @Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
 @Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
 @State
 public class ConcurrencyTest {
 int num = 0;
 boolean ready = false;
@Actor
 public void actor1(I_Result r) {
 if(ready) {
 r.r1 = num + num;
        } 
else {
 r.r1 = 1;
        }
    }
 @Actor
 public void actor2(I_Result r) {
 num = 2;
 ready = true;
    }
 }

执行

mvn clean install 
java -jar target/jcstress.jar 

会输出我们感兴趣的结果,摘录其中一次结果:

*** INTERESTING tests 
  Some interesting behaviors observed. This is for the plain curiosity. 
  2 matching test results. 
      [OK] test.ConcurrencyTest 
    (JVM args: [-XX:-TieredCompilation]) 
  Observed state   Occurrences              Expectation  Interpretation 
               0         1,729   ACCEPTABLE_INTERESTING  !!!! 
               1    42,617,915               ACCEPTABLE  ok 
               4     5,146,627               ACCEPTABLE  ok 
      [OK] test.ConcurrencyTest 
    (JVM args: []) 
  Observed state   Occurrences              Expectation  Interpretation 
               0         1,652   ACCEPTABLE_INTERESTING  !!!! 
               1    46,460,657               ACCEPTABLE  ok 
               4     4,571,072               ACCEPTABLE  ok 

可以看到,出现结果为 0 的情况有 638 次,虽然次数相对很少,但毕竟是出现了。

解决方法

volatile 修饰的变量,可以禁用指令重排

@JCStressTest
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok")
 @Outcome(id = "0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "!!!!")
 @State
 public class ConcurrencyTest {
 int num = 0;
 volatile boolean ready = false;
 @Actor
 public void actor1(I_Result r) {
 if(ready) {
 r.r1 = num + num;
        } 
else {
 r.r1 = 1;
        }
    }
 @Actor
 public void actor2(I_Result r) {
 num = 2;
 ready = true;
    }
 }

结果为:

*** INTERESTING tests 
  Some interesting behaviors observed. This is for the plain curiosity. 
  0 matching test results. 
  • 原理之 volatile
    happens-before
    happens-before 规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套规则总结,抛开以下 happens-before 规则,JMM 并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见
  • 线程解锁 m 之前对变量的写,对于接下来对 m 加锁的其它线程对该变量的读可见
static int x;
 static Object m = new Object();
 new Thread(()->{
 synchronized(m) {
 x = 10;
    }
 },"t1").start();
 new Thread(()->{
 synchronized(m) {
 System.out.println(x);
    }
 },"t2").start();
  • 线程对 volatile 变量的写,对接下来其它线程对该变量的读可见
volatile static int x;
 new Thread(()->{
 x = 10;
 },"t1").start();
 new Thread(()->{
 System.out.println(x);
 },"t2").start();
  • 线程 start 前对变量的写,对该线程开始后对该变量的读可见
static int x;
 x = 10;
 new Thread(()->{
 System.out.println(x);
 },"t2").start();
  • 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用 t1.isAlive() 或 t1.join()等待它结束)
static int x;
 Thread t1 = new Thread(()->{
 x = 10;
 },"t1");
 t1.start();
 t1.join();
 System.out.println(x);
  • 线程 t1 打断 t2(interrupt)前对变量的写,对于其他线程得知 t2 被打断后对变量的读可见(通过
    t2.interrupted 或 t2.isInterrupted)
static int x;
 public static void main(String[] args) {
 Thread t2 = new Thread(()->{
 while(true) {
 if(Thread.currentThread().isInterrupted()) {
 System.out.println(x);
 break;
            }
        }
    },"t2");
 t2.start();
 new Thread(()->{
 sleep(1);
 x = 10;
 t2.interrupt();
    },"t1").start();
     while(!t2.isInterrupted()) {
 Thread.yield();
    }
 System.out.println(x);
 }
 
  • 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见
  • 具有传递性,如果 x hb-> y 并且y hb-> z 那么有x hb-> z ,配合 volatile 的防指令重排,有下面的例子
volatile static int x;
 static int y;
 new Thread(()->{    
y = 10;
 x = 20;
 },"t1").start();
 new Thread(()->{
 // x=20 对 t2 可见, 同时 y=10 也对 t2 可见
System.out.println(x); 
},"t2").start();

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

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

相关文章

算法与数据结构--哈夫曼树与哈夫曼编码

演示视频: 【1】数据结构——五分钟搞定哈夫曼树,会求WPL值,不会你打我_哔哩哔哩_bilibili 【2】哈夫曼树和哈夫曼编码_哔哩哔哩_bilibili 【3】哈夫曼树的构造的做题三步骤_哔哩哔哩_bilibili 求哈夫曼编码的步骤: 1.根据字符及…

Linux(二)常用命令

文章目录 一、文件管理命令1.1 chmod1.2 chown1.3 cat1.4 cp1.5 find1.6 head1.7 tail1.8 less1.9 more1.10 mv1.11 rm1.12 touch1.13 vim1.14 >和>>1.15 scp1.16 ln1.17 怎么用命令查看日志 二、文档管理命令2.1 grep2.2 wc2.3 echo 三、磁盘管理命令3.1 cd3.2 df3.3…

lamda表达式(史上最全)

一、函数式接口 在jdk8中什么是函数式接口: 被FunctionalInterface注解修饰的。接口里边只有一个非default的方法。 满足以上2个条件的即为函数式接口,ps:即使一个接口没有FunctionalInterface修饰,但是满足2,那么这…

​TrustZone之可信固件

Trusted Firmware是Armv8-A设备的安全世界软件的开源参考实现。Trusted Firmware为SoC开发人员和OEM提供了一个符合相关Arm规格(包括TBBR和SMCC)的参考Trusted代码库。 以下图表显示了Trusted Firmware的结构: SMC调度程序处理传入的SMC。SMC…

LIGA-Stereo:为基于立体 3D 检测器的学习 LiDAR 几何感知表示

论文地址:https://openaccess.thecvf.com/content/ICCV2021/papers/Guo_LIGA-Stereo_Learning_LiDAR_Geometry_Aware_Representations_for_Stereo-Based_3D_Detector_ICCV_2021_paper.pdf 论文代码:https://github.com/xy-guo/LIGA-Stereo 摘要 基于立…

解决Maven找不到依赖的问题

如果经过Reload Maven项目,清除Idea缓存,甚至重启Idea等方法都解决不了Dependency xxx not found的问题,不妨试试手动安装。 1. 进入maven仓库,搜索自己需要的对应版本的依赖。 2. 点击下图红框jar图标下载对应的jar包&#xff0c…

CGAL的3D Alpha Shapes

假设我们给定一个二维或三维的点集S,我们希望得到类似“这些点形成的形状”的东西。这是一个相当模糊的概念,可能有许多可能的解释,阿尔法形状就是其中之一。阿尔法形状可用于从密集的无组织数据点集进行形状重建。事实上,阿尔法形…

基于比较的排序算法总结(java实现版)

目录 什么是基于比较的排序算法 什么是排序算法的稳定性 基础排序算法的稳定性 插入排序法 希尔排序法 冒泡排序法 总结 高级算法的稳定性 快速排序法 堆排序法 归并排序法 总结 注意 什么是基于比较的排序算法 基于比较的排序算法定义:之所以能给元素…

【银行测试】银行金融测试+金融项目测试点汇总...

目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 1、银行金融测试是…

案例101:基于微信小程序的停车共享小程序

文末获取源码 开发语言:Java 框架:SSM JDK版本:JDK1.8 数据库:mysql 5.7 开发软件:eclipse/myeclipse/idea Maven包:Maven3.5.4 小程序框架:uniapp 小程序开发软件:HBuilder X 小程序…

微信小程序 动态设置状态栏样式

onLoad(options) {//修改状态栏标题wx.setNavigationBarTitle({title: 页面标题, //页面标题success: () > {}, //接口调用成功的回调函数fail: () > {}, //接口调用失败的回调函数complete: () > {} //接口调用结束的回调函数(调用成功、失败…

CentOS 7 Tomcat服务的安装

前提 安装ava https://blog.csdn.net/qq_36940806/article/details/134945175?spm1001.2014.3001.5501 1. 下载 wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.84/bin/apache-tomcat-9.0.84.tar.gzps: 可选择自己需要的版本下载安装https://mirr…

Centos7在安装Graylog时新安装MongoDB报错端口不监听服务不启动无法运行启动失败

由于虚拟机服务器上需要安装Graylog需要安装MongoDB,尝试官网下载安装包,和yum安装均无法正常启动,折腾了好几天,重装了十几次,网上搜索了很多很多资料,均无法正常运行,百度上搜索各种文档&…

python三大开发框架django、 flask 和 fastapi 对比

本文讲述了什么启发了 FastAPI 的诞生,它与其他替代框架的对比,以及从中汲取的经验。 如果不是基于前人的成果,FastAPI 将不会存在。在 FastAPI 之前,前人已经创建了许多工具 。 几年来,我一直在避免创建新框架。首先&…

【python笔记】并发编程

前言 菜某的笔记总结分享。有错误请指正。 并发编程的意义 并发编程是用来提升代码执行的效率的。 名词理解 进程和线程 我们可以这样理解进程和线程。进程是一个工厂,线程是工厂里的一条流水线。 我们要让我们产品的生产效率提高,我们可以多开工…

基于多反应堆的高并发服务器【C/C++/Reactor】(上)

(一)初始化服务器端用于监听的套接字 Server.h #pragma once // 初始化监听的套接字 int initListenFd(unsigned short port); Server.c int initListenFd(unsigned short port) {// 1.创建监听的fdint lfd socket(AF_INET, SOCK_STREAM, 0);if(lf…

HTTP:HTTP报文

HTTP:HTTP报文 1. 报文流1.1 报文流入源端服务器1.2 报文向下游流动1.3 报文的组成部分1.3.1 报文的语法1.3.2 起始行1. 请求行2. 响应行3. 方法4. 状态码 如果说HTTP是因特网的信使,那么HTTP报文就是用它来搬东西的包裹了。 1. 报文流 HTTP报文是在…

最新国内免费使用GPT4教程,GPT语音对话使用,Midjourney绘画

一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画,相信对大家应该不感到陌生吧?简单来说,GPT-4技术比之前的GPT-3.5相对来说更加智能,会根据用户的要求生成多种内容甚至也可以和用户进行创作交流。 然而,GP…

PHP字符串解析特性绕过WAF

[RoarCTF 2019]Easy Calc 题目的突破点: 只能传入数字和运算符号,不能传入字符(想办法绕过waf) 方法1. php解析规则:当php进行解析时,如果变量名前面有空格,php会自动去掉前面的空格再进行…

轻松搭建知识付费小程序:让知识传播更便捷

明理信息科技saas知识付费平台 在当今数字化时代,知识付费已经成为一种趋势,越来越多的人愿意为有价值的知识付费。然而,公共知识付费平台虽然内容丰富,但难以满足个人或企业个性化的需求和品牌打造。同时,开发和维护…