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操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。

原文地址:https://www.cnblogs.com/bl123/p/14149906.html