1.两个关键问题
1)线程之间如何通信
共享内存
程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
消息传递
程之间没有公共状态,线程之间必须通过发送消息来显式进行通信
2)线程之间如何同步
线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信
总结:Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
2.抽象结构
1)本地内存
每个线程都有一个私有的本地内存(LocalMemory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化
2)主内存
线程之间的共享变量存储在主内存
附图:
注:所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享(本章用“共享变量”这个术语代指实例域,静态域和数组元素)。局部变量(Local Variables),方法定义参数(Java语法规范称之为Formal Method Parameters)和异常处理器参数(Exception HandlerParameters)不会在线程之间共享,它们不会有内存可⻅性问题,也不受内存模型的影响。
3.重排序
定义:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
1) 3种类型
编译器优化的重排序(处理器重排序)
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
指令级并行的重排序(处理器重排序)
现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
内存系统的重排序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行
2)数据依赖性
说明:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
存在三种类型(只要重排两个操作执行顺序,结果便会被改变)
写后读:写一个变量之后,再读这个变量
写后写:写一个变量之后,在写这个变量
读后写:读一个变量之后,再写这个变量
3)as-if-serial语义
说明:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
顺序规则:A happens-before B,B happens-before C,happens-before C
那么实际执行是B可以排在A前,JMM允许这种排序。
4)对多线程的影响
对于存证控制依赖的操作重排序,可能会改变程序的执行结果。
5)DCL问题(double check lock)
public class DoubleCheckedLocking { // 1 private static Instance instance; // 2 public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次检查 synchronized (DoubleCheckedLocking.class) { // 5:加锁 if (instance == null) // 6:第二次检查 instance = new Instance(); // 7:问题的根源出在这里 } // 8 } // 9 return instance; // 10 } // 11 }
那么问题就出现在线程执行到第4行,代码读取到instance不为null时,instance引用的对象有可能还
没有完成初始化。
__根源:
memory = allocate(); //1:分配对象的内存空间 ctorInstance(memory); //2:初始化对象 instance = memory; //3:设置instance指向刚分配的内存地址JMM允许上述命令的执行顺序调整为
memory = allocate(); //1:分配对象的内存空间 instance = memory; //3:设置instance指向刚分配的内存地址 //注意,此时对象还没有被初始化! ctorInstance(memory); //2:初始化对象问题:为什么要调整这个顺序呢?
原因:这个重排序在没有改变单线程程序执行结果的前提下,可以提高程序的执行性能。
解决方案
不允许2和3重排序([volatile]);
允许2和3重排序,但不允许其他线程“看到”这个重排序。