内存模型的基础
Java线程线程之间是通过共享内存的方式实现通信的.
内存模型的抽象结构
共享变量手内存模型影响,线程会去主内存里去加载共享变量,当线程需要改变共享变量时,会将本地内存已更改的副本提交到主内存.
局部变量不会受内存模型的影响
线程之间通信
指令重排
什么是指令重排?
1 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语义。
happens-before
happens-before是JMM的最核心概念之一
JMM设计意图
JMM禁止:
禁止编译器和处理器会改变程序执行结果的重排序.
JMM允许:
允许编译器和处理器不会改变程序执行结果的重排序.
happens-before规则
在JMM中,如果⼀个操作执⾏的结果需要对另⼀个操作可⻅,那么这两个操作之间必须要存在happens-before关系.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 }
锁的内存语义:
volatile的作用
volatile内存语义
volatile内存语义的实现
是否能重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | Y | Y | N |
volatile读 | N | N | N |
volatile写 | Y | N | N |
内存屏障
屏障类型 | 指令示例 | 说明 |
LoadLoad Barriers | Load1;LoadLoad;Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令的装载 |
StoreStore Barriers | Store1;StoreStore;Store2 |
确保Store1数据对其他处理器可见(刷新达到内存)先于Store2及所有后续存储指令的存储 |
LoadStore Barriers | Load1;LoadStrore;Store2 | 确保Load1数据装载先于Store2及所有后续的存储指令刷新到内存 |
StoreLoad Barriers | Store;StoreLoad;Load2 | 确保Store1数据对其他处理器变得可见(指刷新到内存)先于Load2及所有后续装载指令的装载.StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令 |
Final的内存语义
写final域的重排序规则
读final域的重排序规则
写final域的重排序规则
多线程下的单例模式
双重检查锁定
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 }
我们来看看这段双重检查锁定的单例模式有什么问题?
线程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对象的初始化锁,从而确保只会发生一次初始化.