1.volatile 的特性
- 在不同线程的工作时保证了变量的可见性,即主内存和线程的工作内存同步。(可见性)
- 禁止指令重排序。(有序性)
- volatile只针对单次I/O的原子性,例如:a++,这种属于多步操作运算。
2.volatile 的实现原理
2.1可见性原理
- volatile保证变量的可见性是基于内存屏障(Memory Barrier)来实现的。
- 内存屏障,在cpu的场景下又称内存栅栏(sfence,loadfence,mfence),前面两个分别是读写屏障,mfence则是用于同步指令执行的。
- 在多核cpu中,cpu比内存快,一般都有自己的高速缓存(工作内存);在一段代码的执行过程中,如果其中一个cpu修改了变量值,这个值首先会刷新高速缓存中,再刷新到主内存中,此时另外一个cpu内核获取的依然是旧值;而加上volatile之后,线程修修改变量时,其它线程访问时必定是新值。
- volatile做了什么?
- 当在共享变量前面加上volatile在转换为汇编语言时,会多出一条lock(cmpxchg)为前缀的指令,当cpu收到指令时,会立即将工作内存的值刷新到共享该变量主内存中,然后通知其它cpu共享变量的地址无效,其它cpu这时会再向主内存拉取新的值到各自的工作内存。
- MESI协议(缓存一致性协议):在早期的CPU中,是通过在总线加LOCK#锁的方式实现的,但是这种方式开销太大,所以Intel开发了缓存一致性协议,也就是MESI协议。该缓存一致性思路:当CPU写数据时,如果发现操作的变量时共享变量,即其他线程的工作内存也存在该变量,于是会发信号通知其他CPU该变量的内存地址无效。当其他线程需要使用这个变量时,如内存地址失效,那么它们会在主存中重新读取该值。
public class Test {
volatile boolean running = true;
void k(){
System.out.println("k start");
while (running){
/*System.out.println("System.out会进行线程同步,但是其他方法就未必同步");*/
}
System.out.println("k");
}
public static void main(String[] args) throws InterruptedException{
Test test = new Test();
new Thread(test::k,"t1").start();
try {
TimeUnit.SECONDS.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
test.running = false;
}
}
(可见性测试代码)
2.2有序性
- 因为cpu在保证最终一致性的标准下,会对指令操作进行优化,即会发生重排序,加上volatile后会禁止指令重排序。
- 在volatile关键字修饰的内部,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 内存屏障说明:
- StoreStore 屏障:禁止上面的普通写和下面的 volatile 写重排序。
- StoreLoad 屏障:防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
- LoadLoad 屏障: 禁止下面所有的普通读操作和上面的 volatile 读重排序。
- LoadStore 屏障:禁止下面所有的普通写操作和上面的 volatile 读重排序。
- 内存屏障说明:
3.volatile 的应用场景
- DCL模型(double check block)
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
这里第一判空没有进入到锁的竞争,所以第一只是增强并发效率,syschronized锁的第一次,再次判空锁的第二次,这里锁的概念留到下一篇幅讲,暂时略过,这里先讲一下对象实例化过程。
- 对象实例化过程:
- 在堆内存申请空间,给与变量初始化值。
- 修改初始化值。
- 在栈内与堆建立引用指向。
这里如果没加上volatile,当两个线程A,B同时进入syschronized方法进行等待,有可能A在指令重排的过程进行上述1,3的操作,即对象已经不为null了,这时B线程发现构造的对象已经不是null了,则直接返回对象,所以得到的只是堆空间的初始值,加上volatile则禁止了cpu进行指令重排序。
后记
敬请期待后续~~