当前位置: 首页 > news >正文

【并发编程实战】内存模型--解决可见性和有序性的利器

一.前言

       在前面讲了三个问题,  缓存导致的可见性问题,编译优化带来的有序性问题,线程切换带来的原子性问题。既然存在问题,那么总要有解决方案的,这一章里主要就是解决这三个问题的关键点--内存模型

二.内存模型

      2.1 定义:规范了 JVM 如何提供按需禁用缓存和编译优化的方法。

      2.2 解释:缓存和编译优化 导致的可见性和有序性问题,如果把缓存和编译优化禁止掉,程序的性能就大大降低。为了避免这种情况,java提供了 内存模型 的概念来解决能够按照需要进行禁用缓存和编译优化。

      2.3 具体:这些方法包括 volatilesynchronized final 三个关键字,以及六项 Happens-Before 规则

三.Happens-Before 六项规则

   定义:Happens-Before 约束了编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 Happens-Before 规则。前面一个操作的结果对后续操作是可见的。

 1. 程序的顺序性规则

       volatile 变量 volatile boolean v = false,它表达的是:告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。

      happens-before并不是先发生,而是操作对后可见,因为如果不存在volatile的话,v=true有可能在x=42之前执行,而文中这句话的意思是,'x = 42' 的执行结果,在 'v=true' 的时候是可见的,也就是说在'v=true'的时候,我们可以认为此时x已经等于42,但只不过这段代码里,'v=true'并不依赖于'x=42'的结果,因此编译器认为做一次重排序没什么问题,所以有可能会做重排序。

      例如下面的示例代码,假设线程 A 执行 writer() 方法,按照 volatile 语义,会把变量 “v=true” 写入内存;假设线程 B 执行 reader() 方法,同样按照 volatile 语义,线程 B 会从内存中读取变量 v,如果线程 B 看到 “v == true” 时,那么线程 B 看到的变量 x 是多少呢?直觉上看,应该是 42,那实际应该是多少呢?这个要看 Java 的版本,如果在低于 1.5 版本上运行,x 可能是 42,也有可能是 0;如果在 1.5 以上的版本上运行,x 就是等于 42。1.5以上的版本根据顺序性规则做了优化

class VolatileExample { int x = 0; volatile boolean v = false; public void writer() {x = 42; v = true; } public void reader() { if (v == true) { // 这里x会是多少呢? } }
}

 2. volatile 变量规则

     volatile 关键字有两个主要作用:确保可见性和禁止指令重排序。为了实现这些特性,特别是在volatile写的操作中,Java内存模型会在写操作前后自动插入内存屏障。这个过程可以用以下步骤和示意图要点来概述:

  1. 前 Store 屏障(Store Store Barrier): 在volatile写操作执行前,会先插入一个Store Store屏障。这个屏障确保了在volatile写之前的所有普通内存写操作(非volatile的)都已经完成并刷新到主内存中。这样就确保了volatile变量的最新值不会被之前的写操作所覆盖。

  2. volatile 写操作: 执行volatile变量的写入操作,将更新后的值写入主内存中。

  3. 后 Store 屏障(Store Load Barrier): 在volatile写操作之后,会插入一个Store Load屏障。这个屏障有两个作用:首先,它确保当前的volatile写操作在任何后续的读操作(包括非volatile读)之前完成;其次,它也作为一个信号点,告知其他线程当前线程已经完成了volatile变量的写入,从而保证了写操作的可见性。

  4. volatile 写示意图

  5.volatile 读示意图

      

 3. 传递性

     这条规则是指如果 A 的操作结果对 B的操作是可知的,且 B 的操作结果对 C的后续操作也是可知的,那么 A 的操作结果则对 C后续操作也是可知的,这就是传递性

    还是以 1 规则里的代码来分析:

   

1.“x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;

2.写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容 。

3.再根据这个传递性规则,我们得到结果:“x=42” Happens-Before 读变量“v=true”。这意味着什么呢?如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是说,线程 B 能看到 “x == 42”

总结:

    顺序性规则+传递行规则+volatile规则来推断,边界就是只要给volatile赋值成功,那么这个赋值语句之前所有代码的执行结果都对其他线程可见 所以应该是将有volitile执行结果的cpu全部缓存强制刷新到内存,然后强制将其它cpu中的全部缓存和内存同步。所以这个赋值语句之前所有代码的执行结果都对其他线程可见

 4. 管程中锁的规则

     它指的是用于实现线程间同步与互斥的一套机制。管程的核心思想是将共享资源与访问这些资源的代码封装在一起,并通过一个锁(通常与一个条件变量相结合)来控制对资源的访问,以此来保证并发安全。Java中的管程机制主要是通过synchronized关键字和java.util.concurrent.locks包下的类(如ReentrantLock, Condition等)来实现的。

具体来说,Java中的管程特性包括:

  1. 互斥性:同一时刻只允许一个线程进入管程保护的代码区域,防止多个线程同时对共享资源进行修改,这是通过内置的监视器锁(monitor lock)或显式的Lock对象来实现的。

  2. 条件变量:允许线程在满足特定条件之前等待,并在条件满足时被唤醒。在Java中,Object类提供的wait(), notify(), 和 notifyAll()方法,以及Condition接口,都服务于这一目的。

  3. 线程安全:管程内部的代码是线程安全的,因为对共享资源的访问受到了严格的控制。

  4. 内存可见性:进入管程的线程可以看到管程中变量的最新值,这是因为监视器锁确保了对内存的写操作先行发生于后续的读操作,符合Java内存模型的规定。

通过使用管程,开发者可以构建更易于理解和维护的并发程序,减少因竞态条件导致的错误。实际上,当我们在Java中使用synchronized关键字标记方法或代码块时,就是在创建一个简单的管程结构,确保了同步和互斥的正确实施。

 5. 线程 start() 规则

Thread B = new Thread(()->{// 主线程调用B.start()之前// 所有对共享变量的修改,此处皆可见// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();

 6. 线程 join() 规则

Thread B = new Thread(()->{// 此处对共享变量var修改var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66

      

            

相关文章:

  • LoRA用于高效微调的基本原理
  • 【C语言】回调函数 和 部分库函数的用法以及模拟实现
  • 深入理解 MySQL 查询分析工具 EXPLAIN 的使用
  • 【ARMv8/ARMv9 硬件加速系列 4 -- 加解密 Cryptographic Extension 介绍】
  • 通过摄像头检测步频
  • 【C语言】数组参数和指针参数详解
  • MOS参数详解
  • nginx ws长连接配置
  • web端即时通信技术
  • Python for循环中的引用传递和值传递
  • Redis 面试热点(二)
  • 每日一练:攻防世界:Ditf
  • Golang并发控制的三种方案
  • 一文理清GO语言日志库实现开发项目中的日志功能(rotatelogs/zap分析)
  • 基于多头注意力机制卷积神经网络结合双向门控单元CNN-BIGRU-Mutilhead-Attention实现柴油机故障诊断附matlab代码
  • “寒冬”下的金三银四跳槽季来了,帮你客观分析一下局面
  • 10个确保微服务与容器安全的最佳实践
  • 10个最佳ES6特性 ES7与ES8的特性
  • ABAP的include关键字,Java的import, C的include和C4C ABSL 的import比较
  • es6
  • IDEA 插件开发入门教程
  • JavaScript HTML DOM
  • Java方法详解
  • Java新版本的开发已正式进入轨道,版本号18.3
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • node学习系列之简单文件上传
  • 第13期 DApp 榜单 :来,吃我这波安利
  • 高程读书笔记 第六章 面向对象程序设计
  • 基于 Babel 的 npm 包最小化设置
  • 前端面试之CSS3新特性
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 使用 Node.js 的 nodemailer 模块发送邮件(支持 QQ、163 等、支持附件)
  • 推荐一个React的管理后台框架
  • 小程序开发中的那些坑
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • - 转 Ext2.0 form使用实例
  • JavaScript 新语法详解:Class 的私有属性与私有方法 ...
  • ​卜东波研究员:高观点下的少儿计算思维
  • ​渐进式Web应用PWA的未来
  • # Apache SeaTunnel 究竟是什么?
  • #DBA杂记1
  • #etcd#安装时出错
  • #git 撤消对文件的更改
  • #include到底该写在哪
  • #Z0458. 树的中心2
  • #免费 苹果M系芯片Macbook电脑MacOS使用Bash脚本写入(读写)NTFS硬盘教程
  • (安卓)跳转应用市场APP详情页的方式
  • (附源码)spring boot公选课在线选课系统 毕业设计 142011
  • (排序详解之 堆排序)
  • (五)MySQL的备份及恢复
  • (一一四)第九章编程练习
  • *(长期更新)软考网络工程师学习笔记——Section 22 无线局域网
  • .axf 转化 .bin文件 的方法
  • .Net Core 笔试1
  • .NET Core中的时区转换问题