线程A-->本地内存-刷新->主内存
| x=1 x=0=>x=1
|
|线程A发送消息到线程B
|
线程B-取消息->主内存
| x=1
|更新
|1
本地内存
x=1
StoreStore Barriers:写写屏障
LoadStore Barriers:读写屏障
StoreLoad Barriers:写读屏障
无论编译器和处理器怎么重排序优化,必须要保证单线程情况下结果不能改变。
其实java程序并不是顺序执行的。
在不改变程序执行结果的前提下,尽可能提高并行度
public class Test{
int a;
boolean flag = false;
public void read(){
//控制依赖关系
if (flag){//操作1 flag可能读不到true 所以就不会执行下面的操作
// ===>分解
// 操作21 int temp = a+1;
// 操作22 int c = temp
int c = a+1;
}
}
public void write(){
a = 1;//操作3
flag = 0;//操作4
// 3,4的刷新到主内存的时机未知,有可能是2和4批量刷新至主内存,也有可能是分开刷。
}
public static void main(String[] args){
Thread t2;//执行write()方法 操作 3、4
Thread t1;//执行read()方法 操作1、2
}
}
t2的执行顺序: 3,4 4,3
t1的执行顺序 21 1 22 1 21 22
最终导致的结果:t2的flag可能会读到false,c的值可能是0,读取线程将会出现和期望不一致的情况
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial
语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操
作重排序,可能会改变程序的执行结果。
总线工作机制
多个处理器需要对内存进行读取操作的时候,都会向总线发起总线事务。这时候会出现竞争,总线仲裁保证了当前只会有一条总线事务会获取访问内存的权限。这样就保证了内存读写的原子性
JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保
证对所有的内存读/写操作都具有原子性。
在一些32位的处理器上,如果要求对64位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,Java语言规范鼓励但不强求JVM对64位的long型变量和double型变量的写操作具有原子性。当JVM在这种处理器上运行时,可能会把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行。这两个32位的写操作可能会被分配到不同的总线事务中执行,此时对这个64位变量的写操作将不具有原子
JDK 1.5之前,处理器读取long/double的操作流程
JDK 1.5之后,JMM要求读操作必须在单个读事务中完成,保证读操作的原子性;
仅仅允许把64位的long/double拆分成两个32位的写操作来执行,那么这两个写操作就破坏了写操作的原子性
锁释放和获取的内存定义
前提:事务总线,保证读写的原子性
释放:在释放锁的时候,JMM会将本地内存中的共享变量刷新到主内存
获取:在获取锁时,JMM会将本地内存的共享变量置为无效,从而被Monitor保护的临界区代码必须从主内存读取共享变量。
总结
1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用
变量,这两个操作之间不能重排序。
2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能
重排序
class A{
final int a;
public A(int a){
this.a = a;
}
}
//Thread 1 A a = new A(1);
//Thread2 if(a!=null){a.a++}这里可能会出现问题
//this.a = a;在构造方法中可能逃逸,在实例化对象之后才去执行该赋值语句
多线程情况下,取到的a的值可能是初始值0,造成线程不安全
final的重排序规则,禁止溢出构造方法之外
public class DoubleCheckLocking{
private static DoubleCheckLocking singleInstance;
public static DoubleCheckLocking getInstance(){
if(null == singleInstance){//1 第一次check
synchronized(singleInstance){//2 同步
if(null == singleInstance){//3 第二次check
singleInstance = new DoubleCheckLocking();//4 1.分配内存空间 2.初始化对象 3.将对象指向刚分配的内存地址//不是原子操作
}
}
}
//5 可能singleInstance 拥有一块内存地址,但是该内存空间还没有初始化完成,看起来singleInstance是!=null的,但是初始化还没结束
return singleInstance;
}
}
memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象
解决方案
1.不允许2、3重排序
2.允许2、3重排序,但是不允许其他线程看到这个重排序
jdk 1.5以上版本,将singleInstance加上volatile关键字,可以静止重排序自身重排序,之前、之后的操作不能在volatile之后或之前执行
public class DoubleCheckLocking{
private volatile static DoubleCheckLocking singleInstance;
public static DoubleCheckLocking getInstance(){
if(null == singleInstance){
synchronized(singleInstance){
if(null == singleInstance){
//不会重排序,分配内存 初始化对象 将对象指向分配的内存地址
singleInstance = new DoubleCheckLocking();
}
}
}
return singleInstance;
}
}
通过JVM初始化阶段(在Class被加载后,且在线程使用之前),会执行类的初始化,此时JVM会获取一把锁,会同步多个线程对类的初始化操作。
public class DoubleCheckLocking{
private static class DoubleCheckLockingHolder{
private static DoubleCheckLocking doubleCheckLocking = new DoubleCheckLocking();
}
public static DoubleCheckLocking getInstance(){
return DoubleCheckLockingHolder.doubleCheckLocking;
}
}
更详细内容请前往本人GitHub
尹忠政的GitHub