什么?面试官问我Java内存模型!这不得给我加薪?

内存模型的基础

  • 通信  线程之间以何种机制来交换信息
    • 共享内存  隐式通信
    • 消息传递  显示通信
  • 同步  程序中用于控制不同线程间操作,发生的相对顺序的机制
    • 共享内存  显式同步
    • 消息传递  隐式同步

Java线程线程之间是通过共享内存的方式实现通信的.

内存模型的抽象结构

image

  • 共享变量

共享变量手内存模型影响,线程会去主内存里去加载共享变量,当线程需要改变共享变量时,会将本地内存已更改的副本提交到主内存.

  • 局部变量

局部变量不会受内存模型的影响

线程之间通信

image

指令重排

  • 编译器优化的重排序
  • 指令级并行的重排序
  • 内存系统的重排序

image

什么是指令重排?

int i=0; 2 int j=1;

按照我们的认知,程序是一行一行往下执行的,但是由于编译器或运行时环境为了优化程序性能,采取对指令进行重新排序执行,也就是说在计算机执行上面两句话的时候,有可能第二条语句会优先于第一条语句执行.

然而并不是所有的指令都能重排,重排需要基于数据依赖性.

数据依赖性

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

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

上面的情况,如果重排序了两个操作的执行顺序,程序的执行结果将会跟预期完全不一样.

所以说,虽然编译器和处理器可能会对操作做重排序,但是编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial

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

image

happens-before

happens-before是JMM的最核心概念之一

JMM设计意图

  • 程序员对内存模型的使用
    • 为程序员提供足够强的内存可见性保证
  • 编译器和处理器对内存模型的实现
    • 对编译器和处理器的限制要尽可能的放松

JMM禁止:

禁止编译器和处理器会改变程序执行结果的重排序.

JMM允许:

允许编译器和处理器不会改变程序执行结果的重排序.

happens-before规则

在JMM中,如果⼀个操作执⾏的结果需要对另⼀个操作可⻅,那么这两个操作之间必须要存在happens-before关系.

  • 程序顺序规则  ⼀个线程中的每个操作,happens-before于该线程中的任意后续操作.
  • 监视器锁规则  对⼀个锁的解锁,happens-before于随后对这个锁的加锁.
  • volatile变量规则  对⼀个volatile域的写,happens-before于任意后续对这个volatile域的读.
  • 传递性  如果A happens-before B,且B happens-before C,那么A happens-before C.
  • start()规则  如果线程A执⾏操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  • join()规则  如果线程A执⾏操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()返回
  • 线程中断规则  对线程interrupt⽅法的调⽤happens-before于被中断线程的代码检测到中断事件的发⽣.
  • 对象终结规则  ⼀个对象的初始化的完成,也就是构造函数执⾏的结束⼀定 happens-before它的finalize()⽅法.

JMM向程序员提供的happens-before规则能满⾜程序员的需求.

JMM对编译器和处理器的束缚已经尽可能少.

JMM对程序员的承诺

如果⼀个操作happens-before另⼀个操作,那么第⼀个操作的执⾏结果将对第⼆个操作 可⻅,⽽且第⼀个操作的执⾏顺序排在第⼆个操作之前.

JMM对编译器和处理器重排序的约束原则

两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照 happens-before关系指定的顺序来执⾏.

例子:

1 public class Demo29 { 
2     int a=0;
3     boolean flag=false;
4     public void writer(){ 
5         a=1;                //1
6         flag=true;          //2
7     }
8     public void reader(){ 
9         if(flag){           //3
10             int i=a * a;    //4
11 } 
12 } 
13 }

假如线程B在进行操作4时,能否看到线程A在操作1对共享变量a的写入呢? 不一定

| 时刻 | 线程A | 线程B |
| T1 | flag=true |   |
| T2 |   | if(flag) |
| T3 |   | int i=a*a |
| T4 | a=1 |   |

当线程A在执行writer方法时,因为指令重排序,会先执行flag=true,再执行a=1.而线程B在执行操作4时就会读不到线程A对共享变量a的写入,导致运行结果超出预期.

解决方案1:

通过加锁的方式来解决

1 public class Demo29 { 
2     int a=0;
3     boolean flag=false;
4     public synchronized void writer(){ 
5         a=1;                //1
6         flag=true;          //2
7     }
8     public synchronized void reader(){ 
9         if(flag){           //3
10             int i=a * a;    //4
11 } 
12 } 
13 }

image

锁的内存语义:

image

  • 线程A释放⼀个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。
  • 线程B获取⼀个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。
  • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发 送消息。

volatile的作用

  1. volatile原理:被volatile关键字修饰的变量,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
  2. 在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
  3. volatile在Java并发编程中常用于保持内存可见性和防止指令重排序。内存可见性(Memory Visibility):所有线程都能看到共享内存的最新状态;防止指令重排:在基于偏序关系的Happens-Before内存模型中,指令重排技术大大提高了程序执行效率,但同时也引入了一些问题。
  4. 可见性:volatile保持内存可见性的特殊规则:read、load、use动作必须连续出现;assign、store、write动作必须连续出现;每次读取前必须先从主内存刷新最新的值;每次写入后必须立即同步回主内存当中。也就是说,volatile关键字修饰的变量看到的随时是自己的最新值。在线程1中对变量v的最新修改,对线程2是可见的。
  5. 内存屏障:volatile防止指令重排的策略:在每个volatile写操作的前面插入一个StoreStore屏障;在每个volatile写操作的后面插入一个StoreLoad屏障;在每个volatile读操作的后面插入一个LoadLoad屏障;在每个volatile读操作的后面插入一个LoadStore屏障。
  6. volatile 性能:volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

volatile内存语义

image

  • 线程A写⼀个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程 发出了(其对共享变量所做修改的)消息。
  • 线程B读⼀个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile 变量之前对共享变量所做修改的)消息。
  • 线程A写⼀个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过 主内存向线程B发送消息。

volatile内存语义的实现

是否能重排序第二个操作
第一个操作普通读/写
普通读/写Y
volatile读N
volatile写Y
  • 当第⼆个操作是volatile写时,不管第⼀个操作是什么,都不能重排序。

  • 当第⼀个操作是volatile读时,不管第⼆个操作是什么,都不能重排序。

  • 当第⼀个操作是volatile写,第⼆个操作是volatile读时,不能重排序。

内存屏障

屏障类型指令示例说明
LoadLoad BarriersLoad1;LoadLoad;Load2确保Load1数据的装载先于Load2及所有后续装载指令的装载
StoreStore BarriersStore1;StoreStore;Store2确保Store1数据对其他处理器可见(刷新达到内存)先于Store2及所有后续存储指令的存储
LoadStore BarriersLoad1;LoadStrore;Store2确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存
StoreLoad BarriersStore;StoreLoad;Load2确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载.StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令
  • 在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障
  • 在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障
  • 在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障
  • 在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障

image

image

Final的内存语义

写final域的重排序规则

  • JMM禁止编译器把final域的写重排序到构造函数之外.
  • 编译器会在final域的写之后,构造函数return之前插入一个StoreStore屏障

读final域的重排序规则

  • 在⼀个线程中,初次读对象引⽤与初次读该对象包含的final域,JMM禁⽌处理器重排序这两个操作
  • 在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

image

写final域的重排序规则

  • 在构造函数内对⼀个final引⽤的对象的成员域 的写⼊,与随后在构造函数外把这个被构造对象的引⽤赋值给⼀个引⽤变量,这两个操作之 间不能重排序。

image

多线程下的单例模式

双重检查锁定

1 public class DoubleCheckedLocking {2     private static DoubleCheckedLocking doubleCheckedLocking;3 4     private DoubleCheckedLocking() {5 6     }7 8     public static DoubleCheckedLocking getInstance() { 9         if (doubleCheckedLocking == null) { 
10             synchronized (DoubleCheckedLocking.class) { 
11                 if (doubleCheckedLocking == null) { 
12                     doubleCheckedLocking = new DoubleCheckedLocking();//问题出现在这里
13 } 
14 } 
15 } 
16         return doubleCheckedLocking; 
17 } 
18 }

我们来看看这段双重检查锁定的单例模式有什么问题?

image

线程A设置指向刚分配的内存地址后,线程B就判断doubleCheckedLocking对象是否为空,然后直接返回未初始化的doubleCheckedLocking对象,这样会引发出很严重的问题.

解决方案1:

使用volatile,禁止2和3重排序

1 public class DoubleCheckedLocking { 
2     private volatile static DoubleCheckedLocking doubleCheckedLocking; 3 
4     private DoubleCheckedLocking() { 
5 
6     }
7 
8     public static DoubleCheckedLocking getInstance() { 
9         if (doubleCheckedLocking == null) { 
10             synchronized (DoubleCheckedLocking.class) { 
11                 if (doubleCheckedLocking == null) { 
12                     doubleCheckedLocking = new DoubleCheckedLocking();//问题出现在这里
13 } 
14 } 
15 } 
16         return doubleCheckedLocking; 
17 } 
18 }

解决方案2:

基于类初始化,允许2和3重排序,但不允许其他线程"看到这个重排序"

1 public class InstanceFactory { 
2     private static class InstanceHolder { 
3         public static DoubleCheckedLocking doubleCheckedLocking = new DoubleCheckedLocking(); 
4 } 
5 
6     public static DoubleCheckedLocking getInstance() { 
7         return InstanceHolder.doubleCheckedLocking; 
8 } 
9 }

这里使用到了静态内部类的静态属性,类的静态属性只会在第一次调用的时候初始化,而且会有一个Class对象的初始化锁,从而确保只会发生一次初始化.

最后,祝大家早日学有所成,拿到满意offer

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

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

相关文章

使用 Blazor 开发内部后台(四):基于Card组件快速搭建导航首页

James: 本系列为大家介绍如何使用 Blazor 来开发管理后台,有兴趣的朋友欢迎跟着实验,体验 Blazor 开发的高效与乐趣。本系列目录:使用 Blazor 开发内部后台(一):认识Blazor使用 Blazor 开发内部后台&#x…

2020年,朋友圈的正确打开方式!

全世界只有3.14 % 的人关注了青少年数学之旅各位读者朋友们大家好 我是你们的小伙伴上流君我们开始不定期做互推了不是广告,根据兴趣关注感谢理解与支持ღ( ・ᴗ・ )比心有些人,生活离不开朋友圈。朋友圈是他们展示自我、观察世界的…

因为我把JMM原理讲解了一遍,这给足了我涨薪的底气!

一、什么是JMM? JMM指的是Java内存模型,即 Java Memory Model Java内存模型并不是一种实际存在的东西,而是一种人为形成的约定,是一种概念。 关于JMM,我们需要了解一些相关的同步约定 : 线程在解锁前&…

防腐投加器需要加盐吗_果干、果脯、蜜钱、水果脆片,哪个有营养?你吃对了吗?...

时下,追求健美瘦身的年轻人,常把水果千作为零食、加餐的选择之一。大家认为水果干不仅味道好,富含营养素,而且热量低。还有一些人会把果脯、蜜钱、水果脆片等当作新鲜水果的替代品,用以补充营养一认为果 脯、蜜钱的原料…

.NET6正式版将近,70%开发者或面临技术断层!

过往一周,VS2019 发布了最新V16.11,正式支持热重载;.NET6发布了最后一个预览版Preview7,各大新功能宣告完成;随之C#10的新特性也基本成型,好东西很多!各种密集的版本更新,佐证了行业…

一只蝙蝠的自述在朋友圈火了:千万不要再吃野味了!

全世界只有3.14 % 的人关注了青少年数学之旅来源:少女兔(ID:iiilass)、人民日报(ID:rmrbwx)从营养价值上来看野生动物和家养动物的差别微乎其微既不能延年益寿,也不能青春永驻它们不…

掌握JVM 运行时数据区,其实不是很难,加薪也是要技巧可言的!!!

一、概念 Java 内存区域和内存模型是不一样的东西,内存区域是指 Jvm 运行时将数据分区域存储,强调对内存空间的划分。 而内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 J…

不要被约束的意思_俗话说:“娶妻不娶颧骨高,嫁汉不嫁连眉梢”,到底什么意思?...

“世有伯乐,然后有千里马。千里马常有,而伯乐不常有。”——《杂说》 唐韩愈传说春秋时期,有个叫伯乐的人很会选马,他选马的时候,并不需要骑着马真刀真枪地跑几圈,只是看看马的骨相、听听马的嘶鸣就能判断出…

终于来了!微软正式推出 VS Code 测试 API

随着 Visual Studio Code 1.59.0 正式版本的发布,全新的插件测试 API 也从试验版本进入了正式版本。自 2020 年 9 月起至今,经过了将近一年及数次版本迭代的打磨,不管是 API 的功能性还是易用性,与首个预览版本相比都获得了极大的…

MariaDB 10的复制 集群 高可用搭建 大表拆分【持续更新中】

视频地址 http://edu.51cto.com/course/course_id-1691.htmlMariaDB 10培训课程第一部分 复制1.基于GTID方式复制配置及注意事项2.基于表的并行复制3.多源复制(MariaDB 10支持多主一从)4.从库延迟如何找到那条执行慢的SQL5.复制异常处理1062,1032错误处理…

c++歌手大赛系统_计人即讯|第十届程序设计大赛

“第十届程序设计大赛”为激发学生学习计算机语言和科学使用计算机的热情,培养独立思考、勇于创新的探索精神和敢闯会创的意志品格,我院于12月5日13时30分在中主阶教201室成功举办了“第十届程序设计大赛”。本次大赛主要考察选手们应用计算机解决生活中…

限时9.9元 | 快速领取数学建模竞赛备战必备技巧与论文详解!

全世界只有3.14 % 的人关注了青少年数学之旅大家晚上好,随着美赛时间的公布以及大大小小的数学建模竞赛的进行,小天经常可以收到来自很多小伙伴们提出的问题,“竞赛中如何去考虑选题?”、“如何进行思路分析?”、“参考…

项目经理问我Java内存区域模型!急急急

也就这么点吧 Java内存区域主要分为:堆,栈,方法区三大部分 1、堆:是内存区域中最大的部分,所有线程共享,其中存放创建的对象和数组。由于堆是垃圾收集器管理的主要区域,因此也叫GC堆。 2、栈…

设计模式之中介者

中介者模式介绍中介者模式中介者模式是一种行为设计模式,能让你减少对象之间混乱无序的依赖关系,该模式会限制对象之间的直接交互,迫使它们通过一个中介者对象进行合作。中介者模式顾名思义,可以用我们平时生活中的房产中介去理解…

你的网页加载太慢了怎么办?

客户反映,“网页速度太慢了!!”,怎么办? 这个问题似乎有些难以解决,慢了就是慢了,也许用户使用就是一个56K猫,也许服务器只有256K的带宽,也许是网页设计有问题&#xff…

看了这个逻辑关系图,才更清晰为何不让你随便外出了

全世界只有3.14 % 的人关注了青少年数学之旅看了这个逻辑关系图,才更清晰为何不让你随便外出了。非常时期,爱国、爱家、爱自己,请近期减少不必要的外出,直到官方宣布疫情解除!转载来源:刘小兔爱画画部分素材…

ef 关联依赖属性_基础巩固之:xmlns属性梳理

转载自&#xff1a;https://www.cnblogs.com/osttwz/p/6892999.html<?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/beans"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&q…

高德面试官问我:JVM内存溢出后服务还能运行吗,我一顿操作行云流水

文章开篇问一个问题吧&#xff0c;一个java程序&#xff0c;如果其中一个线程发生了OOM&#xff0c;那进程中的其他线程还能运行吗&#xff1f; 接下来做实验&#xff0c;看看JVM的六种OOM之后程序还能不能访问。 在这里我用的是一个springboot程序。 /*** author &#xff…

架构 | 聊聊我心中的架构设计观

【架构设计】| 总结/Edison Zhou在各种面试场合&#xff0c;可能都会被问到“你对架构设计的理解”&#xff0c;我也在最近的转正答辩中被技术委员会负责人问到&#xff0c;这里我重新整理一下思绪&#xff0c;聊聊我心中的的架构设计观。1系统的本质是什么&#xff1f;作为一个…

GeneralUpdate 2021.08.14更新公告

GeneralUpdate是基于.net standard2.0开发的一款&#xff08;c/s应用&#xff09;自动升级程序。该组件将更新的核心部分抽离出来方便应用于多种项目当中目前适用于wpf&#xff0c;控制台应用&#xff0c;winfrom。1.Notice预计会使用Blazor开发GeneralUpdate官网&#xff0c;介…