Loading...
墨滴

lht94

2021/09/07  阅读:35  主题:默认主题

volatile关键字

volatile的作用

Java的volatile关键字用于标记一个变量“应当存储在主存”。更确切地说,每次读取volatile变量,都应该从主存读取,而不是从CPU缓存读取。每次写入一个volatile变量,应该写到主存中,而不是仅仅写到CPU缓存。

  • 变量可见性和volatile的可见性保证

    如果变量a没有声明volitale,如图中的变量a,线程a更改a的值后,并不能保证刷入主内存,就会出现cpu缓存和主内存不一致的情况,此时线程B来读取,可能会读取到线程A更改前的b。而如果声明为volatile,在线程A更改b的值后,会立即刷入主内存,并且会清除掉线程B的内存空间中的b缓存,线程B直接读取内存中的b变量。

volatile的完整性保证

实际上,volatile的可见性保证并不是只对于volatile变量本身那么简单。可见性保证遵循以下规则:

如果线程A写入一个volatile变量,线程B随后读取了同样的volatile变量,则线程A在写入volatile变量之前的所有可见的变量值,在线程B读取volatile变量后也同样是可见的。

如果线程A读取一个volatile变量,那么线程A中所有可见的变量也会同样从主存重新读取。

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

完整的volatile可见性保证意味着,在写入days变量时,线程中所有可见变量也会写入到主存。也就是说,写入days变量时,years和months也会同时被写入到主存。

public class MyClass {
    private int years;
    private int months
    private volatile int days;
//volatile变量days读取放在最前,保证其他变量也从内存读取
    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

//volatile变量days写入放在最后,保证其他变量也写入内存
    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

注意totalDays()方法开始读取days变量值到total变量。在读取days变量值时,months和years的值也会同时从主存读取。因此,按上面所示的顺序读取时,可以保证读取到days、months、years变量的最新值。

volatile变量的读写会触发刷新操作,会影响其他变量的读取如上面所示

指令重排问题

一个经典的使用场景就是双重懒加载的单例模式了:

public class Singleton {
   //volatile修饰
   private static volatile Singleton singleton;
   private Singleton() {
   }
   public static Singleton getInstance() {
       if (singleton == null) {
           synchronized (Singleton.class{
               if (singleton == null) {
                   //防止指令重排
                   singleton = new Singleton();
               }
           }
       }
       return singleton;
   }
}

singleton=newSingleton();,这段代码其实是分为三步: (1)分配内存空间。 (2)初始化对象。 (3)将 singleton 对象指向分配的内存地址。

如果不用volatile修饰singleton ,可能会导致,指令重排,变成(1),(3),(2),另一个线程进来之后发现singleton不等于null,直接返回singleton,此时singleton并未初始化。

  • volatile的Happens-Before

    volatile变量在读写操作前后添加内存屏障,禁止了指令的重排序,保证了有序性。

    内存屏障有LoadLoad屏障,StoreStore屏障,LoadStore屏障,StoreLoad屏障。比如loadload屏障,加在Load1; Load2两个原子操作之间 ,保证在Load2及后续的读操作读取之前,Load1已经读取。其他同理。

    在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad在每个volatile读取之前,插入LoadLoad,之后插入LoadStore。

volatile的重排序规则表:

(1)保证,volatile之后的所有读操作都不能在volatile读操作之前。

(2)保证,volatile之前的所有写操作都不能在volatile写操作之后。

  • volatile无法保证原子性,只能保证自身读写为原子操作。

volatile并不完全可靠

多个线程都能写入共享的volatile变量,主存中也能存储正确的变量值,然而这有一个前提,变量新值的写入不能依赖于变量的旧值。换句话说,就是一个线程写入一个共享volatile变量值时,不需要先读取变量值,然后以此来计算出新的值。

如果线程需要先读取一个volatile变量的值,以此来计算出一个新的值,那么volatile变量就不足够保证正确的可见性。(线程间)读写volatile变量的时间间隔很短,这将导致一个竞态条件,多个线程同时读取了volatile变量相同的值,然后以此计算出了新的值,这时各个线程往主存中写回值,则会互相覆盖。

正确使用volatile

如果两个线程同时读写一个共享变量,仅仅使用volatile关键字是不够的。你应该使用 synchronized 来保证读写变量是原子的。(一个线程)读写volatile变量时,不会阻塞(其他)线程进行读写。你必须在关键的地方使用synchronized关键字来解决这个问题。

volatile的性能考量

读写volatile变量会导致变量从主存读写。从主存读写比从CPU缓存读写更加“昂贵”。访问一个volatile变量同样会禁止指令重排,而指令重排是一种提升性能的技术。因此,你应当只在需要保证变量可见性的情况下,才使用volatile变量。

lht94

2021/09/07  阅读:35  主题:默认主题

作者介绍

lht94