• volatile修饰符


    1.简述

      volatile是Java提供的一种轻量级的同步机制。Java语言包含两种内在的同步机制:同步块(或方法)和volatile变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

      volatile的缺点

    • 频繁更改、改变、写入volatile字段 有可能导致性能问题(volatile字段未修改时,读取没有性能问题的)。
    • 限制现代JVM的JIT编译器对这个字段优化(volatile字段必须遵守一定顺序,放置顺序指令重拍导致bug例如单例双检测bug)。

      volatile适用场景

    • 适用于对变量的写操作不依赖于当前值,对变量的读取操作不依赖于非volatile变量。
    • 适用于读多写少的场景。
    • 可用作状态标志。
    • JDK中ConcurrentHashMap的Entry的value和next被声明为volatile,AtomicLong中的value被声明为volatile。AtomicLong通过CAS原理(也可以理解为乐观锁)保证了原子性。

      volatile的特性

    • volatile具有可见性、有序性,不具备原子性。
    • volatile不具备原子性,这是volatile与synchronized、Lock最大的功能差异。

      synchronized和volatile比较

    • volatile不需要加锁,比synchronized更轻量级,不会阻塞线程。
    • 从内存可见性角度讲,volatile读相当于加锁,volatile写相当于解锁。
    • synchronized既能保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。

    2.volatile的作用

    (1)可见性

      当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

      通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

      非volatile修饰变量示例

    public class Test {
        static boolean bool = false;//非volatile变量
        public static void main(String[] args) throws Exception{
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     while(!bool){
                     };
                     System.out.println("子线程运行完毕");
                 }
             }).start();
             Thread.sleep(100);
             bool = true;
             System.out.println("主线程运行完毕");
        }
    }
    View Code

      运行上面的示例可以看出,主线程修改了bool为true,但子线程却一直不退出循环,因为主线程的修改子线程是不可见的(子线程中获取的bool一直为false,因为子线程的变量副本一直未更新)。

      volatile修饰变量示例

    public class Test {
        static volatile boolean bool = false;//非volatile变量
        public static void main(String[] args) throws Exception{
             new Thread(new Runnable() {
                 @Override
                 public void run() {
                     while(!bool){
                     };
                     System.out.println("子线程运行完毕");
                 }
             }).start();
             Thread.sleep(100);
             bool = true;
             System.out.println("主线程运行完毕");
        }
    }
    View Code

      运行上面的示例可以看出,使用volatile修饰变量后,主线程修改了bool为true,子线程可见(子线程可以退出循环,因为使用volatile修饰后主线程修改了变量会导致子线程的变量副本失效,从而子线程会从主内存中拿去最新值)。

    (2)有序性(禁止指令重排序)

      即程序执行的顺序按照代码的先后顺序执行。当代码执行顺序不同时,多线程就会出现问题。

      在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

      指令重排序遵循的规则

    • 重排序不会对存在依赖关系的操作进行重排。
    • 重排序目的是优化性能,不管怎样重排,单线程下的程序执行结果不会变。

      指令重排序示例

    public class Test {
        public void get(){
            int a = 1;        //1
            int b = a + 1;    //2  因为依赖 1指令的结果,因此1和2都不会排序
        }
        
        public void get2(){
            int a = 1;        //1
            int b = 2;        //2 1和2没有依赖关系,可能发生重排
            int c = a + b;    //3 单线程情况下运行的结果永远是3
        }
    }
    View Code

      double-check(懒汉式)单例模式示例

    class Singleton {
        private volatile static Singleton singleton;
        /**构造函数私有,禁止外部实例化
         */
        private Singleton() {};
        public static Singleton getInstance() {
            if (singleton == null) {                //1.第一次检查是否为空
                synchronized (Singleton.class) {    //2.singleton对象上锁,初始化是由三步操作组成的复合操作,为了保障这三个步骤不可中断,使用synchronized加锁。
                    if (singleton == null) {        //3.第二次检查是否为空,防止二次初始化
                        singleton = new Singleton();//4.实例化
                    }
                }
            }
            return singleton;
        }
    }
    View Code

      singleton = new Singleton();由三步操作组合而成,如果不使用volatile修饰,可能发生指令重排序。步骤3在步骤2之前执行,singleton引用的是还没有被初始化的内存空间,别的线程调用单例的方法就会引发未被初始化的错误。

    (3)原子性

      volatile只能保证单次读、写操作的原子性,多线程就会出现问题。

      不能保证原子性示例

    public class Test {
        public static volatile int num = 0;
        
        public static void main(String[] args) throws Exception {
            //开启30个线程进行累加操作
            for(int i=0;i<30;i++){
                new Thread(){
                    public void run(){
                        for(int j=0;j<10000;j++)
                            num++;//自加操作
                    }
                }.start();
            }
            
            while(Thread.activeCount() > 1)  //线程是否执行完成
                Thread.yield();
            System.out.println(num);
        }
    }
    View Code

      在复合操作的情景下,原子性的功能是维持不了。但是volatile对于读、写操作都是单步的情景,所以还是能保证原子性的。

      要想保证原子性,只能借助于synchronized、Lock以及并发包下的atomic的原子操作类了,即对基本数据类型的自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。

  • 相关阅读:
    codevs 1432 总数统计
    codevs3500 快速幂入门题解
    #163. 【清华集训2015】新式计算机
    2989:糖果
    191:钉子和小球
    java 删除所有HTML工具类
    DateTools时间插件
    新的开始
    springBoot---端口,路径数据配置
    springBoot---多个配置文件读取
  • 原文地址:https://www.cnblogs.com/bl123/p/14149906.html
Copyright © 2020-2023  润新知