java并发编程之volatile

  首先是一段简单的多线程代码

public class VolatileTest {

    private boolean flag = true;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public static void main(String[] args) throws InterruptedException, IOException {
        VolatileTest volatileTest = new VolatileTest();
        Thread thread =new Thread(() -> {
            int i = 1;
            while (volatileTest.isFlag()){
                i++;
            }
            System.out.println(i);
        });
        thread.start();
        Thread.sleep(2000);
        volatileTest.setFlag(false);
        System.out.println("设置结束标示");

    }

}

  运行main方法后,我们发现线程并不能被有效的终止,这其中有两个原因:1、cpu高速缓存2、JIT优化

  在java中,我们通常把线程独享的区域称为工作内存(栈),而把线程共享的区域称为主内存(堆),现代cpu为了提升处理效率,会在cpu和内存之间添加三级cpu高速缓存,因为cpu高速缓存,导致线程中不能读取到共享变量的变化(短时间内),导致可见性问题,而JIT编译器则会对我们的代码进行优化,查看JIT优化后的汇编代码,可以使用JITwatch(当然前提是看得懂汇编),优化后的伪代码大概是这样

boolean f = demo1.flag;
if(f){
  while(true) {
    i++;
  }
}

  这两个原因导致demo中的线程不能正确终止,除了给flag变量添加volatile关键字,我们还可以使用以下手段

  1、如果是32的jdk,则可以在运行参数上加上-client,客户端模式中jdk不会对我们的代码进行JIT优化

  2、64位jdk没有client运行模式,client和server的主要区别在于JIT优化,server模式下可以通过-Djava.compier=NONE关闭JIT优化

  3、使用synchronized关键字,所有同步操作都必须保证原子性和可见性,所以在同步块中发生的变化会立马写回主存,我们需要对修改flag值和读取flag值的代码进行同步

  4、while循环中,调用sleep()方法或者ew byte[1024*1024*1],让出cpu,JVM针对现在的硬件水平已经做了很大程度的优化,基本上很大程度的保障了工作内存和主内存的及时同步,相当于默认使用了volatile。但只是最大程度!在CPU资源一直被占用的时候,工作内存与主内存中间的同步,也就是变量的可见性就会不那么及时。CPU一直被占用的时候,数据的可见性得不到很好的保证,就像上面的程序一直循环做i++;运算占用CPU,而如果调用sleep()或者new byte[1024*1024*1]操作来说,CPU已经不是主要占时间的操作,真正的耗时应该在内存的分配上(因为CPU的处理速度明显快过内存,不然也不会有CPU的寄存器了),所以CPU空闲后会遵循JVM优化基准,尽可能快的保证数据的可见性,从而从主存同步is变量到工作内存,最终导致程序结束。

  JVM指令重排序遵从if-as-serial ,即保证单个cpu中最终结果不变,多cpu多线程时,多个内存区间进行交互势必会有问题。为了解决这个问题,java语言提出内存模型的相关规范,定义了程序在每个点上可以读取到什么值,官网文档地址:https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4

  java内存模型的定义

  happen-before原则(先发生先生效)

  JVM规范对volatile的定义,官网文档地址:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html,通过javap命令反编译字节码文件,反编译后可以看到volatile变量有 ACC_VOLATILE访问控制符,在JVM规范中,可以看到对ACC_VOLATILE访问控制符修饰的变量是禁用缓存的。

  综合下来,volatile关键字,根据java内存模型规范,对volatile变量v的写操作,对其它线程后续对v的读操作保持可见,为了满足规范,JIT有时候就不会对volatile修饰的变量进行优化,也就是不进行指令重排序;根据jvm规范,volatile变量v读操作总是能及时读取到主内存的最新值。

  

原文地址:https://www.cnblogs.com/hhhshct/p/11477652.html