1.JMM
主要定义了对于一个共享变量,另一个线程对这个共享变量执行写操作后,这个线程对这个共享变量的可见性。
2.JMM和JVM的区别:
JMM是用于高并发的,抽象了线程和主存之间的关系,比如线程之间共享的变量必须存储在主内存中,规定了java从源代码到CPU可执行指令的转化过程需要遵守哪些和并发相关的原则和规范,主要目的是简化多线程编程,增强程序的可移植性。
JVM是用于java虚拟机的运行时区域相关,定义了jvm在运行时如何分区存储数据,比如说堆主要用于存放对象实例。
3. CPU缓存模型:
CPU缓存的目的是为了平衡CPU的高速处理和内存之间速度不匹配的问题,而内存缓存的是硬盘数据,用于解决硬盘访问速度过慢的问题。
- L1 cache为CPU私有,相较于其他两种缓存更加接近CPU,访问速度差不多2-4个时钟周期。
- L2 cache为CPU私有,访问速度差不多10-20个时钟周期。
- L3 Cache,所有物理核共享,是最慢的一级,访问速度差不多20-60个时钟周期。
3.1 CPU Cache工作方式:
首先复制一份数据到达CPU Cache中,然后在CPU需要用到的时候直接从CPU Cache中取读取数据,运算结束后,在将数据刷回到Main Memory中,但是这就存在一个内存缓存不一致的问题。为了解决内存缓存不一致的问题,需要指定注入MESI协议这种缓存协议或者其他手段解决。
4.指令重排序
指令重排序简单地说就是系统在执行代码的时候并不一定是按照程序员写代码的顺序来执行,可能会重新安排语句的顺序。
- 编译器重排优化:编译器(JVM)在不改变单线程程序语义的情况下来对顺序重排
- 指令重排优化: 处理器采用了指令级并行技术来将多条指令重叠执行。入股噢不存在数据依赖性,则处理器可以改变语句的的执行顺序
另外内存系统也会有重排序,但并不是真正意义上的重排序,在JMM中表现为主存和本地内存的内容可能不一致,进而大道至程序在多线程的额执行下出现可能的问题。
java 源代码会经历 编译器优化重排序 -->指令并行重排序 --> 内存系统重排序
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致
编译器和处理器的指令重排序的处理方式不一样。对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。指令并行重排和内存系统重排都属于是处理器级别的指令重排序。
5.为什么需要JMM
一般来说,编程语言也可以直接复用操作系统的内存,但是java作为跨平台语言,需要有一套自己的内存模型
可以把JMM看做是java定义的并发编程相关的一些规定,除了抽象了线程和主内存之间的关系外,还规定了java从源代码到CPU可执行指令的这个转换过程需要遵守哪些和并发相关的原则和规范
5.1 JMM如何抽象线程和主存之间的关系?
在 JDK1.2 之前,Java 的内存模型实现总是从 主存 (即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存 本地内存 (比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
5.2 什么是主内存?什么是本地内存?
- 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)
- 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。
从上图来看,线程 1 与线程 2 之间如果要进行通信的话,必须要经历下面 2 个步骤:
- 线程 1 把本地内存中修改过的共享变量副本的值同步到主内存中去。
- 线程 2 到主存中读取对应的共享变量的值。
也就是说,JMM 为共享变量提供了可见性的保障。
不过,多线程下,对主内存中的一个共享变量进行操作有可能诱发线程安全问题。举个例子:
- 线程 1 和线程 2 分别对同一个共享变量进行操作,一个执行修改,一个执行读取。
- 线程 2 读取到的是线程 1 修改之前的值还是修改后的值并不确定,都有可能,因为线程 1 和线程 2 都是先将共享变量从主内存拷贝到对应线程的工作内存中。
关于主内存与工作内存直接的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,Java 内存模型定义来以下八种同步操作(了解即可,无需死记硬背):
- 锁定(lock): 作用于主内存中的变量,将他标记为一个线程独享变量。
- 解锁(unlock): 作用于主内存中的变量,解除变量的锁定状态,被解除锁定状态的变量才能被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
- load(载入):把 read 操作从主内存中得到的变量值放入工作内存的变量的副本中。
- use(使用):把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write 操作使用。
- write(写入):作用于主内存的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。
除了这 8 种同步操作之外,还规定了下面这些同步规则来保证这些同步操作的正确执行(了解即可,无需死记硬背):
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中 “诞生”,不允许在工作内存中直接使用一个未被初始化(load 或 assign)的变量,换句话说就是对一个变量实施 use 和 store 操作之前,必须先执行过了 assign 和 load 操作。
- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。
- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量
6. happens-before原则
1.设计思想:
- 为了对编译器和处理器都尽可能少的约束,只要不改变程序执行的结果.
- 对于会改变程序执行结果的重排序,jvm必须禁止
2.原则定义:
- 如果一个操作 happens-before另一个操作,那么第一个操作的结果对第二个操作是可见的,并且第一个操作的执行顺序在第二个之前,
- 两个操作之间存在 happens-before关系并不意味着java平台的具体实现必须按照 happens-before关系指定的顺序进行,如果重排序之后的执行结果,与按照 happens-before关系来执行的结果一致,那么JMM也允许这样的操作.
happens-before原则更想表达的含义是前一个操作的结果对后一个操作可见,无论两个操作结果是否在同一个线程中.
1 happens-before 2, 即时1,2不在同一个线程,JMM也抱枕1->2
7.happens-before 常见规则有哪些?谈谈你的理解?
- 程序顺序原则: 按照代码的书写顺序,前面happens-before后面的代码
- 锁: 加锁happens-before解锁
- volatile变量原则: 对于volatile修饰的变量写操作happens-before读操作,也就是说,写操作对于后面的任何操作都是可见的
- 传递规则: A happens-before B B happens-before C --> A happens-before C
- 线程启动规则: Thread对象的start 方法 happens-before 此线程的其他每一个动作.
8.再看并发编程三个重要特性
原子性
一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。
在 Java 中,可以借助synchronized
、各种 Lock
以及各种原子类实现原子性。
synchronized
和各种 Lock
可以保证任一时刻只有一个线程访问该代码块,因此可以保障原子性。各种原子类是利用 CAS (compare and swap) 操作(可能也会用到 volatile
或者final
关键字)来保证原子操作。
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
在 Java 中,可以借助synchronized
、volatile
以及各种 Lock
实现可见性。
如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。
有序性
由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。
我们上面讲重排序的时候也提到过:
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
在 Java 中,volatile
关键字可以禁止指令进行重排序优化。