volatile关键字

线程安全问题

Java多线程带来的一个问题是数据安全问题,判断一段Java代码是否有线程安全问题可从以下几点入手:

  • 是否有多线程环境
  • 是否有共享数据
  • 是否对共享数据进行了非原子操作

而线程安全问题的造成原因,要从JVM内存模型讲起,下面简单讲一下JMM内存模型(Java Memory Model)。JMM本身并不真实存在,而是一种规范,通过这种规范约定了程序中各个变量的访问方式。

volatile关键字_第1张图片

如上图所示,JMM规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可访问,而线程对变量的操作须在工作内存中进行,过程是先从主内存中拷贝变量到自己的工作内存(私有数据区域)中,然后对变量进行操作,最后再将变量写回主内存。
问题:当线程A对一个共享变量进行了修改,在写回主内存之前,线程B正好从主内存中拷贝了该共享变量,这必然会导致数据的不一致。
举个简单的例子,假设在一个卖票平台,主内存中存储着剩余票数total = 1,此时线程A来买票,将total变量拷贝到自己的工作内存,买票成功修改total = 0,还没写回主内存,线程B也来买票,将total = 1k拷贝到自己的工作内存,发现total = 1,也成功买到了票,然而这显然是不合理的。

保证线程安全的三个性质:可见性原子性有序性。很显然,通过加锁操作可以保证线程同步,如synchronizedReentrantLock,还有CAS操作。
可见性:多个线程访问同一个变量时,一个线程修改这个变量的值,其它线程能够立即得到修改的值。
在Java中可以使用volatile关键字修饰变量以实现该变量的可见性。当一个变量被volatile修饰时,表示线程工作内存无效,当一个线程修改了该变量会立即更新到主内存,而其它线程读取该变量时直接从主内存中读取。同样,synchronized关键字也保证可见性。
原子性::即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
volatile关键字不能保证原子性。
有序性:程序执行按照代码顺序执行。
为了效率,编译器和处理器会进行指令重排,对于单线程来说结果不会受到影响,但多线程则会出现错误。
volatile能够禁止指令重排。

volatile关键字的实现原理

volatile通过内存屏障(一个CPU指令)禁止指令重排和保证可见性。
由于编译器和处理器执行指令重排优化,如果在指令间插入一条内存屏障,不管什么指令都不可与这条内存屏障指令重排。通过插入内存屏障禁止在内存屏障指令前后执行指令重排。其次,内存屏障能强制刷出CPU的缓存数据,任何CPU上的线程都能读取到这些数据的最新版本。
内存屏障的插入策略是

  • 在每一个volatile写操作前面插入一个StoreStore屏障
  • 在每一个volatile写操作后面插入一个StoreLoad屏障
  • 在每一个volatile读操作后面插入一个LoadLoad屏障
  • 在每一个volatile读操作后面插入一个LoadStore屏障

StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
StoreLoad屏障的作用是避免volatile写与后面可能有的volatile读/写操作重排序。
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
volatile写:

volatile关键字_第2张图片

volatile读:

volatile关键字_第3张图片

volatile原子性例子

class Data{
    volatile int number = 0;
    public void add(){
        number++;
    }
}
public  class  SynClass {
    public static void main(String[] args) {
        Data data = new Data();
        for(int i = 0;i < 10000;i++){
            new Thread(() -> data.add()).start();
        }
        while(Thread.activeCount() > 1){
            Thread.yield();
        }
        System.out.println("结果:" + data.number);

    }
}
运行结果:结果:9998

可见,volatile不能保证原子性,number++操作包含多条指令,期间会有其它线程加塞。volatile只是低配版的同步机制。
显然,在add方法加synchronized可以保证线程同步,但是这显得很笨重。

原子操作类

class Data{
    AtomicInteger number = new AtomicInteger(0);
    public void add(){
        number.getAndIncrement();
    }
}
public  class  SynClass {
    public static void main(String[] args) {
        Data data = new Data();
        for(int i = 0;i < 10000;i++){
            new Thread(() -> data.add()).start();
        }
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println("结果:" + data.number.get());
    }
}
运行结果:  结果:10000

volatile与单例模式

  • 懒汉模式:
public class Singleton {
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
    private Singleton() {
        System.out.println("实例化单例");
    }


    public static void main(String[] args) {
        for(int i = 0;i < 100;i++){
            new Thread(() -> {
                Singleton.getInstance();
            }).start();
        }
    }
}
结果:
实例化单例
实例化单例

结果显示被实例化了两次,显然这种写法在多线程模式下是不安全的。当然,在getInstance方法上加synchronized关键字可以保证线程安全,但是太重,每次有线程进来都加锁。

  • DCL双重校验
    既然在方法上加synchronized会比较重,那就换一个地方加。
public class Singleton {
    private static Singleton instance = null;
    public static Singleton getInstance() {
        if(instance == null){
        //可能有多个线程能够到达此处,因此下面还得判断一次
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
        System.out.println("实例化单例");
    }
}

这种写法貌似没问题,但是还是不正确。
正常顺序下,实例化对象得步骤是

  • 申请内存空间
  • 初始化对象
  • 设置instance指向申请的内存地址
    由于指令重排的原因,步骤3可能在步骤2之前执行。这会导致其它线程判断为instance != null,然而instance没有初始化完成,它拿到的instance是残缺的。

解决:volatile修饰instance禁止指令重排。

public class Singleton {
    private volatile static Singleton instance = null;
    public static Singleton getInstance() {
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    private Singleton() {
        System.out.println("实例化单例");
    }
}

你可能感兴趣的:(volatile关键字)