• 深入理解Volitile关键字


    Volatile

    概念:JVM提供的一个轻量级的同步机制

    作用:

    1. 防止JVM对 Long/Double 等64位的非原子性协议进行的 误操作(读取半个数据);
    2. 可以使某一个变量对所有的线程立即可见(某一个线程如果修改了工作内存中的变量副本,那么加上Volatile关键字之后,该变量就会立即同步到其他线程的工作内存当中)。
    3. 禁止指令 "重排序" 优化。

    前面两点在之前的稳文章中都有提到,下面我们来看什么是指令"重排序"。看指令重排序之前,首先要理解什么是原子性!

    原子性

    原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。

    • 原子性

      num = 10; 就是一个原子操作,这段代码在程序的底层就这么一句话,不会再拆开了!

    • 非原子性

      int num = 10; 如果现在先定义变量,再赋值。这个操作就是非原子性的。

      这段代码在程序底层会拆分成两步,这两步已经不能再拆分了,所以是原子性的。

      1、int num;

      2、num = 10;

    重排序

    为了性能优化,编译器和处理器会进行指令重排序。排序的对象就是 原子性 操作!

    比如上面的例子,int num = 10不是原子性操作。所以程序会在底层将它变成 int num 和 num = 10,把它变成原子性后在进行重排序。

    下面通过一个例子理解重排序,有一个直观的映像:

    int a = 10;		//1  int a; a= 10;
    int b;			//2
    b = 20;			//3
    int c = a * b;	//4
    

    重排序不会影响单线程的执行规则,因此以上程序在经过重排序后,可能的执行过程为1234或者2314,1234就是按照上面正常的执行流程,2314为

    int b;			//2
    b = 20;			//3
    int a = 10;		//1
    int c = a * b;	//4
    

    在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下:

    /**
     * @author leizige
     */
    public class Singleton {
    
        private Singleton(){
    
        }
    
        public  static Singleton instance = null;
    
        public static Singleton getInstance() {
            if(instance == null){
                synchronized (Singleton.class){
                    if(instance == null){
                        /* 不是一个原子性操作 */
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    以上代码在并发环境中会出现问题,原因是 instance = new Singleton()不是一个原子性操作,在执行过程中会拆分为一下几步:

    1. JVM会为 instance 分配内存地址以及内存空间。
    2. 在执行时通过构造方法实例化对象。
    3. 将 instance 指向在第一步分配好的内存地址 。

    根据我们前面重排序的知识,以上代码在真正执行时可能是 1、2、3,也可能是 1、3、2。

    如果在多线程环境下,使用1、3、2可能会出现问题:

    假设线程A刚执行了1、3步骤,但还没有执行2,此时 instance 已经指向了JVM分配的内存地址。如果现在线程B进入 if(instance == null) ,会直接拿到 instance 的对象(此instance是刚才线程A并没有new的对象)。这时拿到的 instance 对象是null,如果直接使用必然会报错!

    解决方案就是添加 Volatile 关键字来禁止 程序使用1、3、2的重排序顺序。

    public volatile static Singleton instance = null;
    

    Volatile 是否能保证变量的原子性、 线程安全

    不能!

    下面通过一段代码来验证一下:

    /**
     * @author leizige
     */
    public class TestVolatile {
    
        private volatile static int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            /**
             * 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
             */
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 30000; j++) {
                        /* 不是一个原子性操作 */
                        num++;
                    }
                }).start();
            }
    
            /**
             * 这里不能直接打印num,代码当中开了100个线程,main也是一个线程。
             * 假如100个线程,每个线程执行需要5ms
             * 但是从main方法开始到打印num,可能只需要花2ms
             * 如果main线程执行完,子线程还没执行完,所以会发生错误
             * 所以需要先暂停1s,让子线程执行完
             */
            Thread.sleep(1000);
    
            System.err.println(num);
        }
    
    }
    

    以上代码执行结果与预期的300w不符,下面我们分析一下线程不安全的原因:

    其实造成原因的代码还是 num++,这句代码不是一个原子性操作。

    num++ 等价与 num = num +1;

    num = num +1 还可以拆分为以下两步:

    1. num + 1;
    2. num = 第一步的结果;

    假设两个线程在执行时通过执行 num + 1;(假设此时num的值为10)

    线程A执行 10 +1 = 11;

    线程B执行 10 +1 = 11;

    正常执行完线程A和B之后num的值应该为12,在并发环境下可能出现两个线程同时+1,就造成了漏加的情况,所以结果与预期不符合。

    如何将 num 变成原子性的呢,只要使用 java.util.concurrent.atomic包下的 AtomicInteger。该类能够保证原子性的核心是因为提供了compareAndSet()方法,该方法提供了 CAS算法(无锁算法)。

    /**
     * @author leizige
     */
    public class TestVolatile {
    
        //    private volatile static int num = 0;
        private static AtomicInteger num = new AtomicInteger(0);
    
        public static void main(String[] args) throws InterruptedException {
    
            /**
             * 每个线程num++300次,100个线程在线程安全时,结果应该为300w;
             */
            for (int i = 0; i < 100; i++) {
                new Thread(() -> {
                    for (int j = 0; j < 30000; j++) {
    //                    num++
                        /* 一个原子性操作 */
                        num.incrementAndGet();
                    }
                }).start();
            }
    
            System.err.println(num);
        }
    
    }
    
  • 相关阅读:
    一, 认识一下Python
    VUE
    排序算法6---归并排序算法
    排序算法5---堆排序算法,改进的简单选择排序
    排序算法4---希尔排序算法,改进的直接插入排序
    排序算法3---直接插入排序算法
    排序算法2---简单选择排序
    排序算法1---冒泡排序及其改进版
    10.17写一函数,实现两个字符串的比较。即自己写一个strcmp函数,函数原型为: int strcmp(char * p1,char * p2) 设p1指向字符串s1,p2指向字符串s2。要求:当s1=s2时,返回值为0。当s1≠s2时,返回它们两者的第一个不同字符的ASCII码差值(如”BOY”与”BAD”,第二个字母不同,”O”与”A”之差为79-65=14);如果s1>s2,则输出正值;
    10.16输入一个字符串,内有数字和非数字字符,如: a123x456 17960? 302tab5876 将其中连续的数字作为一个整数,依次存放到一数组num中。例如123放在num[0]中,456放在num[1]中……统计共有多少个整数,并输出这些数。
  • 原文地址:https://www.cnblogs.com/leizzige/p/14165338.html
Copyright © 2020-2023  润新知