java指令重排以及volatile 为什么禁止指令重排
指令重排序
什么是指令重排序
处理器对写入的代码进行乱序执行优化, 并保证 优化后的执行
结果和正常执行结果(顺序执行结果)是一致的。
例 写入代码 int a=1; int b=2; 但是指令重排后的真正执
行时他的顺序 有可能是 int b=2 然后 int a =1;
指令重排不是任意重排 重排可以 但是你得 通过处理依赖情况得
出正确的结果
例 int a=1; int b=a+1; int c=2;
这种情况 因为 b 的需要依赖 a 的值所以b不能排到a前面 c的
话就无所谓了 因为c不依赖 他们任何一个。
听到这 问题来了 指令重排这费劲 有什么卵用?···
为什么要进行指令重排序 指令重排干吗的
指令重排是为了提高在多处理器环境下程序的运行效率,怎么提高?
比如 两个处理器同时处理两个共享变量
处理器1 变量a++ 变量b++
处理器2 变量a++ 变量b++
这个时候两个处理器都先执行a++操作 为了保证数据的安全 肯定要一
个一个来,一个一个来的话那么势必要有一个处理器处于闲置状态等
待另一个处理器操作共享变量完成它才能运行,这样的话就造成了cpu
资源的浪费。
当进行指令重排优化后 可能会变成这样啦
处理器1 变量a++ 变量b++
处理器2 变量b++ 变量a++
当处理器1操作a的时候 处理器2在操作b
通过重排了指令的执行顺序 避免了cpu资源的浪费
ps(我这里只是简单的举个例子,真实情况比这个要复杂多哈哈哈)
volatile
volatile 是干什么的?
不知道从哪下手码,直接说的话 可能太直接会有点蒙哈哈哈 想了半天还是决定过渡一下从内存模型说吧。
了解java内存模型先从 计算机的内存模型聊起 (别着急很快= = )
计算机的内存模型
现在计算机中 存储设备和处理器的运算速度查了几个数量级的差距(内存最大读写速度 跟不上处理器的处理速度) 简直了 所以不得不加入一层读写速度尽可能接近处理器处理速度的高速缓存 作为内存与处理器之间的缓冲区,运算时把需要使用到的数据复制到缓冲区 运算结束后 再写回主存中,每个处理器都有自己的独立的高速缓存区,基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是 但是 但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)。
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。
多处理器的运算任务都涉及到一个内存区域 同步会主内存时 会导致 各个缓存中的数据不一致 这个时候按谁的来 ?所以说每个处理器访问缓存时都要遵守一些协议 MSI MESI MOSI…等等等等
看图 应该是这样的
java 中的内存模型
jmm(JavaMemoryModel)
java 虚拟机定义的自己的内存模型 和上面的计算机内存模型差不多 主要是为了屏蔽掉各种硬件和操作系统的底层差异
java 内存模型规定了
1,所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
2,每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的从主存中拷贝的变量副本。
3,线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。
4,不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成
可见性问题
什么是可见性? 当一个线程修改了变量的值 其他线程会立即得知,普通变量做不到这一点。
例如: 线程a 修改了普通变量的值 然后回写入主存中 线程b在线程a回写到主内存前读取了 变量值 当线程a回写到主存后 线程b已经读到的变量数据是不会改变的
public class MyTestThread extends Thread {
public boolean testMark=false; //默认false
public MyTestThread(){}
@Override
public void run() {
while (true){
if(testMark){ //如果testMark是true 打印
System.out.println("我的头可不是面团捏的!");
break;
}
}
}
}
public static void main(String[] args){
MyTestThread myTestThread=new MyTestThread();
myTestThread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//把myTestThread的testMark设置为true
myTestThread.testMark=true;
}
运行这个main 函数 你会发现“我的头可不是面团捏的!” 永远打印不出来
因为当 run 方法 别调用的时候从主存中copy下来的数据是 mark=false 当主线程把mark修改为true并同步会主从中后 myTestThread 线程并没有重新去主存读取变量mark值 所以myTestThread线程中的mark 一直都是false 这就是线程的可见性问题。
解决方案: 把testMark变量用volatile 修饰
public volatile boolean testMark=false; //默认false
好啦好啦 它来了
volatile
volatile 可以说是 java 中提供的一种最轻量级的同步机制,它有两个特性
1,保证被他修饰的变量在所有线程中的可见性 。
2,禁止指令重排序
一 可见性
首先说第一个 它是怎么保证 变量在线程中的可见性的 简单来说 就是每次使用到值的时候 去主存中重新读取最新的变量值 这里就用到了上面所说的 一致性协议。
写操作: 当一个线程对共享变量发生写操作的时候 会把其它线程中的对应的共享变量副本置为无效状态。
读操作: 当一个线程使用共享变量的时候它会先判断当前副本变量的状态 如果是无效状态的话 会想总线发送read 消息读取变量最新数据 总线贯穿这个变量用到的所有缓存以及主存 读取到的最新数据可能来自主存 也可能来自其他线程
代码方面看 在修改共享变量后会有一个 lock 指令 这个指令会将本线程缓存写入内存 并 使得其他线程中的缓存失效(缓存锁)
二 禁止指令重排序
顾名思义 就是禁止上面说到的指令重排 处理器不会对代码进行乱序优化 就按写入的顺序执行,在操作变量后会有一个 内存屏障 内存屏障后的指令不能重排序到内存屏障之前的位置 。
为什么禁止指令重排序 指令重排不是会提高运行效率吗?
假设volatile 没有指令重排功能 变量只会保证该方法执行过程中所有依赖值结果的地方都能获得到正确的结果 但不能保证该方法的执行顺序 与写入代码的顺序一样 这样在多线程环境下就会出现问题
一个经典例子(来自 深入理解java 虚拟机): 解释这个问题
volatile 没有指令重排功能
public volatile boolean testMark=false; // 共享变量
Map configOptions;
char[] configText;
//线程1 逻辑
{
//模拟读取配置信息, 当读取完成后把 testMark 设置为ture 作为通知 其他线程 配置可以读取了
configOptions=new HashMap<>();
//读取配置文件信息
configText=readConfigFile(fileName);
//把配置文件信息放入configOptions
processConfigOptions(configText,configOptions);
//标志位设为ture
testMark=true;
}
//线程2逻辑
{
//循环判断testMark 等待testMark为true
while(!testMark ){
}
//读取配置信息
doSomethingWithConfig();
}
如果不禁止 指令重排 线程1 中 把 testMark=true; 重排到了 加载配置信息前面
这个时候线程2 会立即发现 testMark=true; 并进行 配置信息的读取 但是 线程1 仅仅是先执行了testMark=true; 配置文件加载 还没执行,configOptions没有数据 这个时候你如果进行这个中操作 configOptions.get(“token”);就会报错。
好啦 讲完啦! 如果哪里写的有问题 大家可以提出来 纠错一波哈哈!感谢大家 !这里是活泼乐观的潇洒李