zoukankan      html  css  js  c++  java
  • Java的volatile关键字

    简要说明它的作用:

    一:线程可见性

    一个变量在某个线程里修改了它的值,如果使用了volatile关键字,那么别的线程可以马上读到修改后的值。

    二:指令重排序

    没加之前,指令是并发执行的,第一个线程执行到一半另一个线程可能开始执行了。加了volatile关键字后,不同线程是按照顺序一步一步执行的。

    线程可见性原理

    用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。

    如果要深入了解volatile关键字的作用,就必须先来了解一下JVM在运行时候的内存分配过程。

    在 java 垃圾回收整理一文中,描述了jvm运行时刻内存的分配。其中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,

    线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存

    变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,

    在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。下面一幅图

    描述这写交互!

     

    那么在了解完JVM在运行时候的内存分配过程以后,我们开始真正深入的讨论volatile的具体作用

    请看代码:

    复制代码
    复制代码
     1 public class VolatileTest extends Thread {
     2     
     3     boolean flag = false;
     4     int i = 0;
     5     
     6     public void run() {
     7         while (!flag) {
     8             i++;
     9         }
    10     }
    11     
    12     public static void main(String[] args) throws Exception {
    13         VolatileTest vt = new VolatileTest();
    14         vt.start();
    15         Thread.sleep(2000);
    16         vt.flag = true;
    17         System.out.println("stope" + vt.i);
    18     }
    19 }
    复制代码
    复制代码

    上面的代码是通过标记flag来控制VolatileTest线程while循环退出的例子!

    下面让我用伪代码来描述一下我们的程序

    • 首先创建 VolatileTest vt = new VolatileTest();
    • 然后启动线程 vt.start();
    • 暂停主线程2秒(Main) Thread.sleep(2000);
    • 这时的vt线程已经开始执行,进行i++;
    • 主线程暂停2秒结束以后将 vt.flag = true;
    • 打印语句 System.out.println("stope" + vt.i); 在此同时由于vt.flag被设置为true,所以vt线程在进行下一次while判断 while (!flag) 返回假 结束循环 vt线程方法结束退出!
    • 主线程结束

    上面的叙述看似并没有什么问题,“似乎”完全正确。那就让我们把程序运行起来看看效果吧,执行mian方法。2秒钟以后控制台打印stope-202753974。

    可是奇怪的事情发生了 程序并没有退出。vt线程仍然在运行,也就是说我们在主线程设置的 vt.flag = true;没有起作用。

    在这里我需要说明一下,有的同学可能在测试上面代码的时候程序可以正常退出。那是因为你的JVM没有优化造成的!在DOC下面输入 java -version 查看 如果显示Java HotSpot(TM) ... Server 则JVM会进行优化。

    如果显示Java HotSpot(TM) ... Client 为客户端模式,需要设置成Server模式  设置方法问Google

     

    问题出现了,为什么我在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false?

     那么按照我们上面所讲的 “JVM在运行时候的内存分配过程” 就很好解释上面的问题了。

     首先 vt线程在运行的时候会把 变量 flag 与 i (代码3,4行)从“主内存”  拷贝到 线程栈内存(上图的线程工作内存)

     然后 vt线程开始执行while循环 

     7         while (!flag) {
     8             i++;
     9         }

    while (!flag)进行判断的flag 是在线程工作内存当中获取,而不是从 “主内存”中获取。

    i++; 将线程内存中的i++; 加完以后将结果写回至 "主内存",如此重复。

    然后再说说主线程的执行过程。 我只说明关键的地方 

    vt.flag = true;

    主线程将vt.flag的值同样 从主内存中拷贝到自己的线程工作内存 然后修改flag=true. 然后再将新值回到主内存。

    这就解释了为什么在主线程(main)中设置了vt.flag = true; 而vt线程在进行判断flag的时候拿到的仍然是false。那就是因为vt线程每次判断flag标记的时候是从它自己的“工作内存中”取值,而并非从主内存中取值!

    这也是JVM为了提供性能而做的优化。那我们如何能让vt线程每次判断flag的时候都强制它去主内存中取值呢。这就是volatile关键字的作用。

    再次修改我们的代码

    复制代码
    复制代码
    public class VolatileTest extends Thread {
        
        volatile boolean flag = false;
        int i = 0;
        
        public void run() {
            while (!flag) {
                i++;
            }
        }
        
        public static void main(String[] args) throws Exception {
            VolatileTest vt = new VolatileTest();
            vt.start();
            Thread.sleep(2000);
            vt.flag = true;
            System.out.println("stope" + vt.i);
        }
    }

    指令重排序原理

    计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:

    源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令

    单线程环境里面确保最终执行结果和代码顺序的结果一致

    处理器在进行重排序时,必须要考虑指令之间的数据依赖性

    多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

    Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

    首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

    • 保证特定操作的顺序
    • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

    由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。

    也就是过在Volatile的写 和 读的时候,加入屏障,防止出现指令重排的

    屏障类型指令示例说明
    LoadLoad Load1;LoadLoad;Load2 保证Load1的读取操作在Load2及后续读取操作之前执行
    StoreStore Store1;StoreStore;Store2 在Store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存
    LoadStore Load1;LoadStore;Store2 在Store2及其后的写操作执行前,保证Load1的读操作已读取结束
    StoreLoad Store1;StoreLoad;Load2 保证load1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

    工作内存与主内存同步延迟现象导致的可见性问题

    • 可通过synchronized或volatile关键字解决,他们都可以使一个线程修改后的变量立即对其它线程可见

    对于指令重排导致的可见性问题和有序性问题

    • 可以使用volatile关键字解决,因为volatile关键字的另一个作用就是禁止重排序优化

    部分来源于:https://www.cnblogs.com/bbgs-xc/p/12731769.html和https://www.cnblogs.com/daxin/p/3364014.html

  • 相关阅读:
    程序员面试中有几大法则
    css 页面重绘和回流(重排)以及优化
    (一)Windows下安装RabbitMQ服务
    NOT IN 和 OR 的SQL语句优化
    java更改图片格式,,更改图片大小,并实现多线程上传图片。
    数据库根据坐标查找附近的店铺
    Spring MVC接收提交文件图片的两种方式
    搜索引擎选择: Elasticsearch与Solr
    redis
    浅谈分布式事务
  • 原文地址:https://www.cnblogs.com/yjyxzzz/p/14686459.html
Copyright © 2011-2022 走看看