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

    转:https://www.cnblogs.com/selene/p/5972882.html

    volatile不能保证数据同步

    volatile关键字比较少用,原因无外乎两点,一是在Java1.5之前该关键字在不同的操作系统上有不同的表现,所带来的问题就是移植性较差;而且比较难设计,而且误用较多,这也导致它的"名誉" 受损。

      我们知道,每个线程都运行在栈内存中,每个线程都有自己的工作内存(Working Memory,比如寄存器Register、高速缓冲存储器Cache等),线程的计算一般是通过工作内存进行交互的,其示意图如下图所示:

      

      从示意图上我们可以看到,线程在初始化时从主内存中加载所需的变量值到工作内存中,然后在线程运行时,如果是读取,则直接从工作内存中读取,若是写入则先写到工作内存中,之后刷新到主内存中,这是JVM的一个简答的内存模型,但是这样的结构在多线程的情况下有可能会出现问题,比如:A线程修改变量的值,也刷新到了主内存,但B、C线程在此时间内读取的还是本线程的工作内存,也就是说它们读取的不是最"新鲜"的值,此时就出现了不同线程持有的公共资源不同步的情况。

      对于此类问题有很多解决办法,比如使用synchronized同步代码块,或者使用Lock锁来解决该问题,不过,Java可以使用volatile更简单地解决此类问题,比如在一个变量前加上volatile关键字,可以确保每个线程对本地变量的访问和修改都是直接与内存交互的,而不是与本线程的工作内存交互的,保证每个线程都能获得最"新鲜"的变量值,其示意图如下:

      

      明白了volatile变量的原理,那我们思考一下:volatile变量是否能够保证数据的同步性呢?两个线程同时修改一个volatile是否会产生脏数据呢?我们看看下面代码:

    class UnsafeThread implements Runnable {
        // 共享资源
        private volatile int count = 0;
    
        @Override
        public void run() {
            // 增加CPU的繁忙程度,不必关心其逻辑含义
            for (int i = 0; i < 1000; i++) {
                Math.hypot(Math.pow(92456789, i), Math.cos(i));
            }
            count++;
        }
    
        public int getCount() {
            return count;
        }
    }

    上面的代码定义了一个多线程类,run方法的主要逻辑是共享资源count的自加运算,而且我们还为count变量加上了volatile关键字,确保是从内存中读取和写入的,如果有多个线程运行,也就是多个线程执行count变量的自加操作,count变量会产生脏数据吗?想想看,我们已经为count加上了volatile关键字呀!模拟多线程的代码如下:

    public static void main(String[] args) throws InterruptedException {
            // 理想值,并作为最大循环次数
            int value = 1000;
            // 循环次数,防止造成无限循环或者死循环
            int loops = 0;
            // 主线程组,用于估计活动线程数
            ThreadGroup tg = Thread.currentThread().getThreadGroup();
            while (loops++ < value) {
                // 共享资源清零
                UnsafeThread ut = new UnsafeThread();
                for (int i = 0; i < value; i++) {
                    new Thread(ut).start();
                }
                // 先等15毫秒,等待活动线程为1
                do {
                    Thread.sleep(15);
                } while (tg.activeCount() != 1);
                // 检查实际值与理论值是否一致
                if (ut.getCount() != value) {
                    // 出现线程不安全的情况
                    System.out.println("循环到:" + loops + " 遍,出现线程不安全的情况");
                    System.out.println("此时,count= " + ut.getCount());
                    System.exit(0);
                }
            }
    
        }

    想让volatite变量"出点丑",还是需要花点功夫的。此段程序的运行逻辑如下:

    • 启动100个线程,修改共享资源count的值
    • 暂停15秒,观察活动线程数是否为1(即只剩下主线程再运行),若不为1,则再等待15秒。
    • 判断共享资源是否是不安全的,即实际值与理想值是否相同,若不相同,则发现目标,此时count的值为脏数据。
    • 如果没有找到,继续循环,直到达到最大循环为止。

    运行结果如下:

        循环到:40 遍,出现线程不安全的情况
        此时,count= 999
      这只是一种可能的结果,每次执行都有可能产生不同的结果。这也说明我们的count变量没有实现数据同步,在多个线程修改的情况下,count的实际值与理论值产生了偏差,直接说明了volatile关键字并不能保证线程的安全。
      在解释原因之前,我们先说一下自加操作。count++表示的是先取出count的值然后再加1,也就是count=count+1,所以,在某个紧邻时间片段内会发生如下神奇的事情:

    (1)、第一个时间片段

      A线程获得执行机会,因为有关键字volatile修饰,所以它从主内存中获得count的最新值为998,接下来的事情又分为两种类型:

    • 如果是单CPU,此时调度器暂停A线程执行,让出执行机会给B线程,于是B线程也获得了count的最新值998.
    • 如果是多CPU,此时线程A继续执行,而线程B也同时获得了count的最新值998.

    (2)、第二个片段

    • 如果是单CPU,B线程执行完+1操作(这是一个原子处理),count的值为999,由于是volatile类型的变量,所以直接写入主内存,然后A线程继续执行,计算的结果也是999,重新写入主内存中。
    • 如果是多CPU,A线程执行完加1动作后修改主内存的变量count为999,线程B执行完毕后也修改主内存中的变量为999

    这两个时间片段执行完毕后,原本期望的结果为1000,单运行后的值为999,这表示出现了线程不安全的情况。这也是我们要说明的:volatile关键字并不能保证线程安全,它只能保证当前线程需要该变量的值时能够获得最新的值,而不能保证线程修改的安全性。

    顺便说一下,在上面的代码中,UnsafeThread类的消耗CPU计算是必须的,其目的是加重线程的负荷,以便出现单个线程抢占整个CPU资源的情景,否则很难模拟出volatile线程不安全的情况,大家可以自行模拟测试。

  • 相关阅读:
    LintCode Python 简单级题目 488.快乐数
    LintCode Python 简单级题目 100.删除排序数组中的重复数字 101.删除排序数组中的重复数字II
    LintCode Python 简单级题目 373.奇偶分割数组
    LintCode Python 简单级题目 39.恢复旋转排序数组
    LintCode Python 简单级题目 35.翻转链表
    LintCode Python 简单级题目 451.两两交换链表中的节点
    LintCode Python 简单级题目 174.删除链表中倒数第n个节点
    aws查看官方centos镜像imageid
    linux shell脚本查找重复行/查找非重复行/去除重复行/重复行统计
    php配置优化-生产环境应用版
  • 原文地址:https://www.cnblogs.com/jvStarBlog/p/11796313.html
Copyright © 2011-2022 走看看