乐趣区

关于java:谈谈你对volatile的理解

缓存不一致性问题

为了解决缓存不一致性问题,通常来说有以下 2 种解决办法:

1)通过在总线加 LOCK# 锁的形式

2)通过缓存一致性协定

这 2 种形式都是硬件层面上提供的形式。

在晚期的 CPU 当中,是通过在总线上加 LOCK#锁的模式来解决缓存不统一的问题。因为 CPU 和其余部件进行通信都是通过总线来进行的,如果对总线加 LOCK# 锁的话,也就是说阻塞了其余 CPU 对其余部件拜访(如内存),从而使得只能有一个 CPU 能应用这个变量的内存。比方如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上收回了 LCOK# 锁的信号,那么只有期待这段代码齐全执行结束之后,其余 CPU 能力从变量 i 所在的内存读取变量,而后进行相应的操作。这样就解决了缓存不统一的问题。

然而下面的形式会有一个问题,因为在锁住总线期间,其余 CPU 无法访问内存,导致效率低下。

所以就呈现了缓存一致性协定。最闻名的就是 Intel 的 MESI 协定,MESI 协定保障了每个缓存中应用的共享变量的正本是统一的。它外围的思维是:当 CPU 写数据时,如果发现操作的变量是共享变量,即在其余 CPU 中也存在该变量的正本,会发出信号告诉其余 CPU 将该变量的缓存行置为有效状态,因而当其余 CPU 须要读取这个变量时,发现自己缓存中缓存该变量的缓存行是有效的,那么它就会从内存从新读取。

volatile 的内存语义

volatile 能够保障线程可见性且提供了肯定的有序性,然而无奈保障原子性。在 JVM 底层 volatile 是采纳“内存屏障”来实现的。

1.volatile 关键字的两层语义

一旦一个共享变量(类的成员变量、类的动态成员变量)被 volatile 润饰之后,那么就具备了两层语义:

1)保障了不同线程对这个变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。

2)禁止进行指令重排序。

先看一段代码,如果线程 1 先执行,线程 2 后执行:

// 线程 1
boolean stop = false;
while(!stop){doSomething();
}
 
// 线程 2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采纳这种标记方法。然而事实上,这段代码会齐全运行正确么?即肯定会将线程中断么?不肯定,兴许在大多数时候,这个代码可能把线程中断,然而也有可能会导致无奈中断线程(尽管这个可能性很小,然而只有一旦产生这种状况就会造成死循环了)。

上面解释一下这段代码为何有可能导致无奈中断线程。在后面曾经解释过,每个线程在运行过程中都有本人的工作内存,那么线程 1 在运行的时候,会将 stop 变量的值拷贝一份放在本人的工作内存当中。

那么当线程 2 更改了 stop 变量的值之后,然而还没来得及写入主存当中,线程 2 转去做其余事件了,那么线程 1 因为不晓得线程 2 对 stop 变量的更改,因而还会始终循环上来。

然而用 volatile 润饰之后就变得不一样了:

第一:应用 volatile 关键字会强制将批改的值立刻写入主存;

第二:应用 volatile 关键字的话,当线程 2 进行批改时,会导致线程 1 的工作内存中缓存变量 stop 的缓存行有效(反映到硬件层的话,就是 CPU 的 L1 或者 L2 缓存中对应的缓存行有效);

第三:因为线程 1 的工作内存中缓存变量 stop 的缓存行有效,所以线程 1 再次读取变量 stop 的值时会去主存读取。

那么在线程 2 批改 stop 值时(当然这里包含 2 个操作,批改线程 2 工作内存中的值,而后将批改后的值写入内存),会使得线程 1 的工作内存中缓存变量 stop 的缓存行有效,而后线程 1 读取时,发现自己的缓存行有效,它会期待缓存行对应的主存地址被更新之后,而后去对应的主存读取最新的值。

那么线程 1 读取到的就是最新的正确的值。

察看退出 volatile 关键字和没有退出 volatile 关键字时所生成的汇编代码发现,退出 volatile 关键字时,会多出一个 lock 前缀指令。lock 前缀指令其实就相当于一个内存屏障。内存屏障是一组解决指令,用来实现对内存操作的程序限度。volatile 的底层就是通过内存屏障来实现的。

2. volatile 能保障有序性吗?

在后面提到 volatile 关键字能禁止指令重排序,所以 volatile 能在肯定水平上保障有序性。

volatile 关键字禁止指令重排序有两层意思:

1)当程序执行到 volatile 变量的读操作或者写操作时,在其后面的操作的更改必定全副曾经进行,且后果曾经对前面的操作可见;在其前面的操作必定还没有进行;

2)在进行指令优化时,不能将在对 volatile 变量拜访的语句放在其前面执行,也不能把 volatile 变量前面的语句放到其后面执行。

举个简略的例子:

//x、y 为非 volatile 变量
//flag 为 volatile 变量
 
x = 2;        // 语句 1
y = 0;        // 语句 2
flag = true;  // 语句 3
x = 4;         // 语句 4
y = -1;       // 语句 5 

因为 flag 变量为 volatile 变量,那么在进行指令重排序的过程的时候,不会将语句 3 放到语句 1、语句 2 后面,也不会讲语句 3 放到语句 4、语句 5 前面。然而要留神语句 1 和语句 2 的程序、语句 4 和语句 5 的程序是不作任何保障的。

并且 volatile 关键字能保障,执行到语句 3 时,语句 1 和语句 2 必然是执行结束了的,且语句 1 和语句 2 的执行后果对语句 3、语句 4、语句 5 是可见的。

3.volatile 的原理和实现机制

后面讲述了源于 volatile 关键字的一些应用,上面咱们来探讨一下 volatile 到底如何保障可见性和禁止指令重排序的。

上面这段话摘自《深刻了解 Java 虚拟机》:

“察看退出 volatile 关键字和没有退出 volatile 关键字时所生成的汇编代码发现,退出 volatile 关键字时,会多出一个 lock 前缀指令”

lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个性能:

1)它确保指令重排序时不会把其前面的指令排到内存屏障之前的地位,也不会把后面的指令排到内存屏障的前面;即在执行到内存屏障这句指令时,在它后面的操作曾经全副实现;

2)它会强制将对缓存的批改操作立刻写入主存;

3)如果是写操作,它会导致其余 CPU 中对应的缓存行有效。

应用 volatile 关键字的场景

synchronized 关键字是避免多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile 关键字在某些状况下性能要优于 synchronized,然而要留神 volatile 关键字是无奈代替 synchronized 关键字的,因为 volatile 关键字无奈保障操作的原子性。通常来说,应用 volatile 必须具备以下 2 个条件:

1)对变量的写操作不依赖于以后值

2)该变量没有蕴含在具备其余变量的不变式中

实际上,这些条件表明,能够被写入 volatile 变量的这些有效值独立于任何程序的状态,包含变量的以后状态。

事实上,我的了解就是下面的 2 个条件须要保障操作是原子性操作,能力保障应用 volatile 关键字的程序在并发时可能正确执行。

上面列举几个 Java 中应用 volatile 的几个场景。

1. 状态标记量

volatile boolean flag = false;
 
while(!flag){doSomething();
}
 
public void setFlag() {flag = true;}

volatile boolean inited = false;
// 线程 1:
context = loadContext();  
inited = true;            
 
// 线程 2:
while(!inited){sleep()
}
doSomethingwithconfig(context);

2.double check

class Singleton{
    private volatile static Singleton instance = null;
 
    private Singleton() {}
 
    public static Singleton getInstance() {if(instance==null) {synchronized (Singleton.class) {if(instance==null)
                    instance = new Singleton();}
        }
        return instance;
    }
}

volatile 的内存语义实现

为了实现 volatile 内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优安排来最小化插入屏障的总数简直是不可能的,为此,JMM 采取了激进策略:

  1. 在每个 volatile 写操作的 后面 插入一个 StoreStore 屏障;
  2. 在每个 volatile 写操作的 前面 插入一个 StoreLoad 屏障;
  3. 在每个 volatile 读操作的 前面 插入一个 LoadLoad 屏障;
  4. 在每个 volatile 读操作的 前面 插入一个 LoadStore 屏障。

须要留神的是:volatile 写是在后面和前面 别离插入内存屏障 ,而 volatile 读操作是在 前面插入两个内存屏障

StoreStore 屏障:禁止下面的一般写和上面的 volatile 写重排序;

StoreLoad 屏障:避免下面的 volatile 写与上面可能有的 volatile 读 / 写重排序

LoadLoad 屏障:禁止上面所有的一般读操作和下面的 volatile 读重排序

LoadStore 屏障:禁止上面所有的一般写操作和下面的 volatile 读重排序

退出移动版