1、什么是内存模型,为什么需要它?
(1)内存模型的发展背景
近几年计算性能通过重排序实现了很大的提升,而且处理器也越来越朝着多核处理器发展以实现硬件的并行性。随着处理器的不断强大,编译器也在不断的改进:通过指令重排序来实现优化执行,使用成熟的全局寄存器分配
算法。这些都使得线程在内存内的操作更趋于复杂,如果没有正确的同步机制下,内存间的操作呈现乱序执行,从而不能保证计算结果的正确性。
编译器重排序:使得编译后的指令顺序可以和源代码的顺序不一样
处理器重排序:在编译器重排序的基础上再进行指令的优化重排序
处理器的并行性:多线程内线程之间的操作执行顺序不同,要实现线程间的数据共享在没有同步的机制下易产生不安全的数据共享。
处理器的多级缓存:如果缓存提交至主内存的顺序没有一定的同步机制下,就会出现提交顺序的乱序,使得共享数据在内存的不可见性。
1)对单线程程序执行的影响
在指令重排序的情况下,只要保证最终的计算结果和严格的串行执行环境下的结果一致下,重排序等优化措施是可以的。
2)对多线程程序执行的影响
多线程中线程间各自的操作执行顺序不同,强行保持程序的串行性,只会增加线程间的调度次数,而频繁的线程调度会引起不要的上下文操作使得线程的开销很大也降低了程序的执行速度。在只需要进行数据共享的操作内使
用同步机制协调线程间的操作,实现线程间的数据共享(保存线程间短暂的串行性)就能够减少不要的性能开销。
(2)Java内存模型是处理器架构内存模型的抽象
Java内存模型屏蔽了不同处理器架构内存模型之间的差异(JVM通过插入内存栅栏来屏蔽JMM与底层平台内存模型之间的差异)
(3)重排序
1)进行重排序的条件:操作之间不存在偏序关系和全序关系
2)禁止重排序的方法:锁,volatile,final
重排序会破外操作之间的偏序关系(Happens-Before),从而产生数据竞争问题
(4)内存模型简介
1)简介:
Java内存模型是通过各种操作来定义的,包括对变量的读写操作,监视器的加锁和释放操作,以及现场的启动和合并操作。
Java内存模型实质是为最终计算结果的正确性,而采取的实现内存操作间保存偏序关系即实现内存操作的有序性,实现内存操作的内存可见性和原子性的内存访问操作的模型
2)偏序关系(Happens-Before):
偏序关系(Happens-Before)的本质:保证操作之间的内存可见性
保持偏序关系(Happens-Before)的准则:
规则名称
内容
说明
程序顺序规则 程序代码顺序自然保持操作间的偏序关系 监视器锁规则 在同一个锁上锁的释放肯定在锁的获取之后 volatile规则 volatile变量的写操作肯定在volatile变量的读操作之前 线程启动规则 线程的启动操作start肯定在线程执行操作之前 线程结束规则 线程执行的任何操作肯定在线程检测到该线程结束之前执行,或者从join操作或者调用Thread.isAlive是返回 中断规则 interrupt中断操作必须在线程检测到线程中断之前执行 终结器规则 对象的构造函数必须在对象的终结器执行之前执行 传递性 操作顺序的传递性
2.发布
(1)不安全发布的本质
不安全发布操作的本质就是:对象的发布操作和对象的访问操作之间缺乏偏序关系(Happens-Before排序)
除了不可变对象外,使用被另外一个线程初始化的对象通常都是不安全的,除非对象的发布操作是在使用该对象的线程开始使用之前执行(即保存对象的发布操作在对象的加载操作之前也就是两个操作间保持偏序关系)
(2)安全发布的本质
安装发布对象的本质就是:使用锁或者volatile关键字来禁止操作之间的重排序,从而保持操作之间的偏序关系
(3)安全的初始化模式
初始化的几种模式:
初始化模式 | 优点 | 缺点 | 备注 |
synchronized加锁模式 | 禁止重排序,实现了线程安全的初始化 降低了初始化类或者创建实例的开销 | 数据竞争严重的情况下太耗性能 增加了访问被初始化延迟的字段的开销 | 不推荐使用 |
类初始化加锁模式 | 在类的静态初始化过程完成初始化,实现了线程安全的初始化 降低了初始化类或者创建实例的开销 | 仅适用于在构造时的状态,对于可变的对象读写操作之间仍然需要同步机制。仅限于静态字段的初始化 增加了访问被初始化延迟的字段的开销 | 类初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个类已经加载。类初始化(静态初始化)时类在初始化阶段执行,在类型加载后被线程使用之前执行。 |
双重检测锁定模式(DCL) | 降低了因synchronize锁带来的性能消耗 降低了初始化类或者创建实例的开销 | 没有实现线程安全的初始化 | 错误的不安全的延迟初始化方案 |
基于volatile改良后的双重检测锁定模式(VDCL) | 降低了因synchronize锁带来的性能消耗,实现了线程安全的初始化 除了可用于静态字段也可以用于实例字段 降低了初始化类或者创建实例的开销 | 增加了访问被初始化延迟的字段的开销 |
无论通过类初始化加锁模式还是volatile关键字都是实现了禁止重排序或者实现操作间的偏序关系从而保证了操作内存的可见性。
3、初始化过程中的安全性
初始化安全性只能保证final修饰的值从构造过程完成时开始的可见性,对于非final的值,或者在构造完成后可以改变的值,必须采用同步来确保可见性