究竟什么是内存模型?

在多处理系统中,每个 CPU 通常都包含一层或者多层内存缓存,这样设计的原因是为了加快数据访问速度(因为数据会更靠近处理器) 并且能够减少共享内存总线上的流量(因为可以满足许多内存操作)来提高性能。内存缓存能够极大的提高性能。

但是同时,这种设计方式也带来了许多挑战。

比如,当两个 CPU 同时对同一内存位置进行操作时会发生什么?在什么情况下这两个 CPU 会看到同一个内存值?

现在,内存模型登场了!!!在处理器层面,内存模型明确定义了其他处理器的写入是如何对当前处理器保持可见的,以及当前处理器写入内存的值是如何使其他处理器可见的,这种特性被称为可见性,这是官方定义的一种说法。

然而,可见性也分为强可见性弱可见性,强可见性说的是任何 CPU 都能够看到指定内存位置具有相同的值;弱可见性说的是需要一种被称为内存屏障的特殊指令来刷新缓存或者使本地处理器缓存无效,才能看到其他 CPU 对指定内存位置写入的值,写入后的值就是内存值。这些特殊的内存屏障是被封装之后的,我们不研究源码的话是不知道内存屏障这个概念的。

内存模型还规定了另外一种特性,这种特性能够使编译器对代码进行重新排序(其实重新排序不只是编译器所具有的特性),这种特性被称为有序性。如果两行代码彼此没有相关性,那么编译器是能够改变这两行代码的编译顺序的,只要代码不会改变程序的语义,那么编译器就会这样做。

我们上面刚提到了,重新排序不只是编译器所特有的功能,编译器的这种重排序只是一种静态重排序,其实在运行时或者硬件执行指令的过程中也会发生重排序,重排序是一种提高程序运行效率的一种方式。

比如下面这段代码

Class Reordering {int x = 0, y = 0;public void writer() {x = 1;y = 2;}public void reader() {int r1 = y;int r2 = x;}
}
复制代码

当两个线程并行执行上面这段代码时,可能会发生重排序现象,因为 x 、 y 是两个互不相关的变量,所以当线程一执行到 writer 中时,发生重排序,y = 2 先被编译,然后线程切换,执行 r1 的写入,紧接着执行 r2 的写入,注意此时 x 的值是 0 ,因为 x = 1 没有编译。这时候线程切换到 writer ,编译 x = 1,所以最后的值为 r1 = 2,r2 = 0,这就是重排序可能导致的后果。

所以 Java 内存模型为我们带来了什么?

Java 内存模型描述了多线程中哪些行为是合法的,以及线程之间是如何通过内存进行交互的。Java 内存模型提供了两种特性,即变量之间的可见性和有序性,这些特性是需要我们在日常开发中所注意到的点。Java 中也提供了一些关键字比如 volatile、final 和 synchronized 来帮助我们应对 Java 内存模型带来的问题,同时 Java 内存模型也定义了 volatile 和 synchronized 的行为。

其他语言,比如 C++ 会有内存模型吗?

其他语言比如 C 和 C++ 在设计时并未直接支持多线程,这些语言针对编译器和硬件发生的重排序是依靠线程库(比如 pthread )、所使用的编译器以及运行代码的平台提供的保证。

JSR - 133 是关于啥的?

在 1997 年,在此时 Java 版本中的内存模型中发现了几个严重的缺陷,这个缺陷经常会出现诡异的问题,比如字段的值经常会发生改变,并且非常容易削弱编译器的优化能力。

所以,Java 提出了一项雄心勃勃的畅想:合并内存模型,这是编程语言规范第一次尝试合并一个内存模型,这个模型能够为跨各种架构的并发性提供一致的语义,但是实际操作起来要比畅想困难很多。

最终,JSR-133 为 Java 语言定义了一个新的内存模型,它修复了早期内存模型的缺陷。

所以,我们说的 JSR - 133 是关于内存模型的一种规范和定义。

JSR - 133 的设计目标主要包括:

  • 保留 Java 现有的安全性保证,比如类型安全,并加强其他安全性保证,比如线程观察到的每个变量的值都必须是某个线程对变量进行修改之后的。
  • 程序的同步语义应该尽可能简单和直观。
  • 将多线程如何交互的细节交给程序员进行处理。
  • 在广泛、流行的硬件架构上设计正确、高性能的 JVM 实现。
  • 应提供初始化安全的保证,如果一个对象被正确构造后,那么所有看到对象构造的线程都能够看到构造函数中设置其最终字段的值,而不用进行任何的同步操作。
  • 对现有的代码影响要尽可能的小。

重排序是什么?

在很多情况下,访问程序变量,比如对象实例字段、类静态字段和数组元素的执行顺序与程序员编写的程序指定的执行顺序不同。编译器可以以优化的名义任意调整指令的执行顺序。在这种情况下,数据可以按照不同于程序指定的顺序在寄存器、处理器缓存和内存之间移动。

有许多潜在的重新排序来源,例如编译器、JIT(即时编译)和缓存。

重排序是硬件、编译器一起制造出来的一种错觉,在单线程程序中不会发生重排序的现象,重排序往往发生在未正确同步的多线程程序中。

旧的内存模型有什么错误?

新内存模型的提出是为了弥补旧内存模型的不足,所以旧内存模型有哪些不足,我相信读者也能大致猜到了。

首先,旧的内存模型不允许发生重排序。再一点,旧的内存模型没有保证 final 的真正 不可变性,这是一个非常令人大跌眼睛的结论,旧的内存模型没有把 final 和其他不用 final 修饰的字段区别对待,这也就意味着,String 并非是真正不可变,这确实是一个非常严重的问题。

其次,旧的内存模型允许 volatile 写入与非 volatile 读取和写入重新排序,这与大多数开发人员对 volatile 的直觉不一致,因此引起了混乱。

什么是不正确同步?

当我们讨论不正确同步的时候,我们指的是任何代码

  • 一个线程对一个变量执行写操作,
  • 另一个线程读取了相同的变量,
  • 并且读写之间并没有正确的同步

当违反这些规则时,我们说在这个变量上发生了数据竞争现象。 具有数据竞争现象的程序是不正确同步的程序。

同步(synchronization)都做了哪些事情?

同步有几个方面,最容易理解的是互斥,也就是说一次只有一个线程可以持有一个监视器(monitor),所以在 monitor 上的同步意味着一旦一个线程进入一个受 monitor 保护的同步代码块,其他线程就不能进入受该 monitor 保护的块直到第一个线程退出同步代码块。

但是同步不仅仅只有互斥,它还有可见,同步能够确保线程在进入同步代码块之前和同步代码块执行期间,线程写入内存的值对在同一 monitor 上同步的其他线程可见。

在进入同步块之前,会获取 monitor ,它具有使本地处理器缓存失效的效果,以便变量将从主内存中重新读取。 在退出一个同步代码块后,会释放 monitor ,它具有将缓存刷新到主存的功能,以便其他线程可以看到该线程所写入的值

新的内存模型语义在内存操作上面制定了一些特定的顺序,这些内存操作包含(read、write、lock、unlock)和一些线程操作(start 、join),这些特定的顺序保证了第一个动作在执行之前对第二个动作可见,这就是 happens-before 原则,这些特定的顺序有

  • 线程中的每个操作都 happens - before 按照程序定义的线程操作之前。
  • Monitor 中的每个 unlock 操作都 happens-before 相同 monitor 的后续 lock 操作之前。
  • 对 volatile 字段的写入都 happens-before 在每次后续读取同一 volatile 变量之前。
  • 对线程的 start() 调用都 happens-before 在已启动线程的任何操作之前。
  • 线程中的所有操作都 happens-before 在任何其他线程从该线程上的 join() 成功返回之前。

需要注意非常重要的一点:两个线程在同一个 monitor 之间的同步非常重要。并不是线程 A 在对象 X 上同步时可见的所有内容在对象 Y 上同步后对线程 B 可见。释放和获取必须进行匹配(即,在同一个 monitor 上执行)才能有正确的内存语义,否则就会发生数据竞争现象。

final 在新的 JMM 下是如何工作的?

通过上面的讲述,你现在已经知道,final 在旧的 JMM 下是无法正常工作的,在旧的 JMM 下,final 的语义就和普通的字段一样,没什么其他区别,但是在新的 JMM 下,final 的这种内存语义发生了质的改变,下面我们就来探讨一下 final 在新的 JMM 下是如何工作的。

对象的 final 字段在构造函数中设置,一旦对象被正确的构造出来,那么在构造函数中的 final 的值将对其他所有线程可见,无需进行同步操作。

什么是正确的构造呢?

正确的构造意味着在构造的过程中不允许对正在构造的对象的引用发生 逃逸,也就是说,不要将正在构造的对象的引用放在另外一个线程能够看到它的地方。下面是一个正确构造的示例:

class FinalFieldExample {final int x;int y;static FinalFieldExample f;public FinalFieldExample() {x = 3;y = 4;}static void writer() {f = new FinalFieldExample();}static void reader() {if (f != null) {int i = f.x;int j = f.y;}}
}
复制代码

执行读取器的线程一定会看到 f.x 的值 3,因为它是 final 的。 不能保证看到 y 的值 4,因为它不是 final 的。 如果 FinalFieldExample 的构造函数如下所示:

public FinalFieldExample() { x = 3;y = 4;// 错误的构造,可能会发生逃逸global.obj = this;
}
复制代码

这样就不会保证读取 x 的值一定是 3 了。

这也就说是,如果在一个线程构造了一个不可变对象(即一个只包含 final 字段的对象)之后,你想要确保它被所有其他线程正确地看到,通常仍然需要正确的使用同步。

新的内存模型修复了双重检查锁的问题吗?

也许我们大家都见过多线程单例模式双重检查锁的写法,这是一种支持延迟初始化同时避免同步开销的技巧。

class DoubleCheckSync{private static DoubleCheckSync instance = null;public DoubleCheckSync getInstance() {if (instance == null) {synchronized (this) {if (instance == null)instance = new DoubleCheckSync();}}return instance;}
}
复制代码

这样的代码看起来在程序定义的顺序上看起来很聪明,但是这段代码却有一个致命的问题:它不起作用

双重检查锁不起作用?

原因就是初始化实例的写入和对实例字段的写入可以由编译器或缓存重新排序,看起来我们可能读取了初始化了 instance 对象,但其实你可能只是读取了一个未初始化的 instance 对象。

有很多小伙伴认为使用 volatile 能够解决这个问题,但是在 1.5 之前的 JVM 中,volatile 不能保证。在新的内存模型下,使用 volatile 会修复双重检查锁定的问题,因为这样在构造线程初始化 DoubleCheckSync 和返回其值之间将存在 happens-before 关系读取它的线程。

需要相关资料的可以扫一扫

p8大佬告诉你JSR - 133 都解决了哪些问题?相关推荐

  1. P8大佬出书了!送送送!

    P8 大佬冰河的分布式书籍终于出版了,我特意跟他要了五本书,作为福利送给大家,送书活动见文末,下面是书籍介绍: 随着互联网的不断发展,互联网企业的业务在飞速变化,推动着系统架构也在不断地发生变化.总体 ...

  2. JSR 133 Java内存模型以及并发编程的最权威论文汇总

    Java内存模型 先看官方文档: https://docs.oracle.com/javase/specs/ JSR 133:Java TM内存模型和线程规范修订版:https://www.jcp.o ...

  3. 阿里P8大佬带你深入解析JVM与java

    阿里P8大佬带你深入解析JVM与java 什么是Java 经过了多年的发展,Java早已由一门单纯的计算机编程语言,演变为了一套强大的技术体系.是的,什么是Java,我想技术体系四个字应该是最好的概括 ...

  4. 被P8大佬面试2小时,差点干趴下了!

    「 关注"石杉的架构笔记",大厂架构经验倾囊相授 」  儒猿技术团队最新出品 <基于ShardingSphere的分库分表实战> 文章来源:http://u6.gg/k ...

  5. 自学python需要下载什么软件-一篇告诉你为什么人人都应该学点Python?

    一篇告诉你为什么人人都应该学点Python? 2018-06-28 20:39:45 333点赞 3620收藏 203评论 小编注:想获得更多专属福利吗?金币加成.尊享众测.专属勋章.达人福利任务你想 ...

  6. ubuntu11.10设置了pppoe(自动拨号后),开机速度慢和无法连接无线网都解决办法...

    开机速度慢解决办法 sudo gedit /etc/network/interfaces 用#号注释掉中间四行 auto lo iface lo inet loopback #auto dsl-pro ...

  7. 除了缓存,Redis 都解决了哪些问题?

    先看一下Redis是一个什么东西.官方简介解释到:Redis是一个基于BSD开源的项目,是一个把结构化的数据放在内存中的一个存储系统,你可以把它作为数据库,缓存和消息中间件来使用.同时支持string ...

  8. python125免费教程,125 个视频成就千万级网红,Python 告诉你李子柒都在拍些什么?...

    原标题:125 个视频成就千万级网红,Python 告诉你李子柒都在拍些什么? 作者 |Mika,数据 |真达 后期 |Mika.泽龙 责编 | 郭芮 来源 | CDA数据分析师 今天我们来聊聊把生活 ...

  9. 前端学习与“IT界大佬告诉你,程序员接私活的7大平台利器”

    mark一下,博主名称"前端入门到精通",博客标题:"IT界大佬告诉你,程序员接私活的7大平台利器" https://blog.csdn.net/zwjweb/ ...

最新文章

  1. 技术人生:本周改进计划
  2. 永成科技C++笔试题
  3. cat在MATLAB中的应用
  4. 用python自动化做ecshop测试_Python-自动化测试面试
  5. Mysql 数据库学习笔记03 存储过程
  6. 学习笔记-----fputs与printf
  7. 【opencv学习】【图像金字塔】
  8. Linux学习笔记(四)之查看登录用户
  9. python 输入列表 返回每个元素出现的次数
  10. SqlConnection,SqlCommand,SqldataReader的用法总结
  11. gis属性表怎么导成excel_将Excel数据导入到ArcGIS属性表
  12. C# .NET与数据结构
  13. 如何用70行Java代码实现神经网络算法
  14. 为什么 String hashCode 方法选择数字31作为乘子?
  15. python os 的坑_Python踩坑之旅其二裸用os.system的原罪
  16. 软件工程专业的论文答辩_软件工程论文答辩开场白范例
  17. cck8graphpad作图_新经验 | CCK8 实验心得
  18. 微pe工具箱 系统安装教程_微pe工具箱怎么装系统
  19. java教学视频平台系统计算机毕业设计MyBatis+系统+LW文档+源码+调试部署
  20. WYSE S10、WYSE V90、HP T5740瘦客机评测

热门文章

  1. R语言使用plotly绘制3D散点图实战
  2. plotly可视化绘制多坐标轴图
  3. python判断二叉树是否为平衡二叉树
  4. 巴菲特+查理芒格+投资理论+经典语录+给韭菜的启示
  5. 面向对象一类与对象的概念与特性
  6. Nanopore sequencing technology and tools for genome assembly: computational analysis of the current
  7. 用计算机怎么算个人所得税,个税计算器2021
  8. php 返回字符串给aja,解决ajax异步请求返回的是字符串问题
  9. numpy 数组 填充 0、1和各种值
  10. tf.io.gfile.glob 遍历文件