Skip to content

《面渣逆袭》Java并发编程 篇 · 第 3/7 章。原版 PDF(下载 / 打印)

18.说一下你对Java内存模型(JMM)的理解?

Java 内存模型(JavaMemoryModel ,JMM ),是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。

JMM 定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(M ainMemory )

中,每个线程都有一个私有的本地内存(LocalMemory ),本地内存中存储了该线程以读/ 写共享变量的副本。

Java 内存模型的抽象图:

配图

本地内存是JMM 的 一个抽象概念,并不真实存在。它其实涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

配图

图里面的是一个双核 CPU 系统架构 ,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有自己的一级缓存,在有些架构里面还有一个所有

CPU 共享的二级缓存。 那么 Java 内存模型里面的工作内存,就对应这里的 Ll 缓存或者 L2 缓存或者 CPU 寄存器。

19.说说你对原子性、可见性、有序性的理解?

原子性、有序性、可见性是并发编程中非常重要的基础概念,JMM 的很多技术都是围绕着这三大特性展开。

原子性:原子性指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。

可见性:可见性指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。

有序性:有序性指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。

分析下面几行代码的原子性?

int i = 2;
int j = i;
i++;
i = i + 1;

第1 句是基本类型赋值,是原子性操作。

第2 句先读i 的值,再赋值到j ,两步操作,不能保证原子性。

第3 和第4 句其实是等效的,先读取i 的值,再+ 1 ,最后赋值到i ,三步操作了,不能保证原子性。

原子性、可见性、有序性都应该怎么保证呢?

原子性:JMM 只能保证基本的原子性,如果要保证一个代码块的原子性,需要使

用synchronized 。
可见性:Java是利用volatile 关键字来保证可见性的,除此之外,final 和synchronized 也能保

证可见性。

有序性:synchronized 或者volatile 都可以保证多线程之间操作的有序性。

20.那说说什么是指令重排?

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3 种类型。

  1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺

序。

  1. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism ,

ILP )来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应 机器指令的执行顺序。

  1. 内存系统的重排序。由于处理器使用缓存和读/ 写缓冲区,这使得加载和存储操作看上去可能是在

乱序执行。

从Java 源代码到最终实际执行的指令序列,会分别经历下面3 种重排序,如图:

配图

我们比较熟悉的双重校验单例模式就是一个经典的指令重排的例子,Singletoninstance=new

**Singleton() ;**对应的JVM 指令分为三步:分配内存空间- -> 初始化对象- --> 对象指向分配的内存空间,但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。

配图

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

21.指令重排有限制吗?happens-before了解吗?

指令重排也是有一些限制的,有两个规则happens-beforeas-if-serial来约束。

happens-before 的定义:

如果一个操作happens-before 另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

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

before 关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before 关系来执行的结果一致,那么这种重排序并不非法

happens-before 和我们息息相关的有六大规则:

配图

程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。

volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。

传递性:如果Ahappens-beforeB ,且Bhappens-beforeC ,那么Ahappens-beforeC 。

start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的
ThreadB.start()操作happens-before于线程B中的任意操作。
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-
before于线程A从ThreadB.join()操作成功返回。

22.as-if-serial又是什么?单线程的程序一定是顺序的吗?

as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行

结果不能被改变。编译器、runtime 和处理器都必须遵守a s-if-serial 语义。

为了遵守a s-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例。

doublepi=3.14; //A

doubler=1.0; //B

doublearea=pirr; //C上面3 个操作的数据依赖关系:

配图

A 和C 之间存在数据依赖关系,同时B 和C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A 和B 的前面(C 排到A 和B 的前面,程序的结果将会被改变)。但A 和B 之间没有数据依赖关系,编译器和处理器可以重排序A 和B 之间的执行顺序。

所以最终,程序可能会有两种执行顺序:

配图

as-if-serial 语义把单线程程序保护了起来,遵守a s-if-serial 语义的编译器、runtime 和处理器共同编织了这么一个“ 楚门的世界” :单线程程序是按程序的“ 顺序” 来执行的。a s-if-serial 语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。

23.volatile实现原理了解吗?

volatile有两个作用,保证可见性和有序性。
volatile怎么保证可见性的呢?

相比synchronized 的加锁方式来解决共享变量的内存可见性问题,volatile 就是更轻量的选择,它没有上下文切换的额外开销成本。

volatile 可以确保对某个变量的更新对其他线程马上可见,一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。

例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷

新回主内存,线程B 读取x 的时候,就会清空本地内存变量,然后再从主内存获取最新值。

配图

volatile怎么保证有序性的呢?

重排序可以分为编译器重排序和处理器重排序,valatile 保证有序性,就是通过分别限制这两种类型的重排序。

配图

为了实现volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

1.在每个volatile写操作的前面插入一个StoreStore 屏障
2.在每个volatile写操作的后面插入一个StoreLoad 屏障
3.在每个volatile读操作的后面插入一个LoadLoad 屏障
4.在每个volatile读操作的后面插入一个LoadStore 屏障

配图

配图

本文整理自三分恶《面渣逆袭》系列的公开内容,仅供个人学习使用

本站仅供个人学习使用,请勿外传