【Java并发】- 3.对Volatile关键字的深入解析

1.Volatile简介

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度

2.Volatile关键字的作用

  1. 实现long/double类型变量的原子操作
  2. 防止指令重排序
  3. 实现变量的可见性
volatile double a = 1.0

当一个被Volatile修饰的变量,如果程序要获取该变量的值,不会从寄存器获取,而是从内存(高速缓存)中获取。

3.Volatile与锁(synchronized和Lock两种锁)的比较

1.相同点

  • 确保了变量的内存可见性
  • 防止指令重排序

指令重排序指的是:当Java虚拟机再把Java文件编译成class文件时,在cpu执行相关机器代码时为了提高程序的执行效率,会对程序进行重排序,重排序的代码遵循as-if-seria原则。

as-if-seria:即不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变

2.不同点

锁会保证程序的原子性,且锁具备排他性。即如果一个线程对锁内代码进行操作时,其他线程不能对改代码操作。

volatile会保证对变量写操作的原子性但是不保证排他性。即如果一个线程对volatile修饰的变量进行写操作,同时其他线程也可以对该变量操作。导致最后修改的结果不符合预期,所以一般也称volatile关键字不具备原子性。

另一个点在于:与synchronized锁相比,synchronized关键字会导致线程的上下文切换(用户态与内核态之间的切换)。而volatile关键字不会导致这种情况出现。这也是为什么说volatile是轻量级锁的原因。

如何实现volatile关键字写操作的原子性

    volatile int a = b + 2;

    volatile int a = a++;

代码的volatile写操作的原子性不具备原子性。

如果要实现volatile写操作的原子性,那么在等号右侧的赋值变量中就不能出现被多线程所共享的变量,哪怕这个变量也是个volatile也不可以。 即右边的值应该是一个常量。

    volatile int count = 1;
    volatile boolean flag = false;

而且,volatile只能保证原始类型的写操作原子性,不能保证引用类型变量的原子性。如

volatile Date date = new Date();

因为new Date(); 不是一个原子操作。它分为申请变量空间,和获取空间的引用两个操作。

4.volatile的内存屏障

volatile保证可见性和防止指令重排序都是由内存屏障(Memory barrier)实现的。

volatile有两种内存屏障

写入屏障

这种屏障是发生在对volatile修饰的变量进行写入时。

    int a = 1;
    String s = "hello";
    volatile boolean v = false;  // 写入操作

那么系统就会在变量v前后加入写入屏障

    内存屏障 (Release Barrier,释放屏障)

    volatile boolean v = false;  // 写入操作

    内存屏障 (Store Barrier,存储屏障)
  • Release Barrier:释放屏障,防止cpu对vloatile变量与该变量之前的代码进行重排序
  • Store Barrier:存储屏障,刷新处理器的缓存,保证该屏障之前的所以操作所生成的结果对其他处理器来说是可见的

读取屏障(volatile变量获取其他变量的值)

    volatile boolean v1 = v;
    int a = 1;
    String s = "hello";

对读取来说也会加入内存屏障

    内存屏障(Load Barrier,加载屏障)

    volatile boolean v1 = v;

    内存屏障(Acquire Barrier,获取屏障)
  • Load Barrier:加载屏障,刷新处理器,获取其他处理器对volatile变量的修改结果
  • Acquire Barrier:获取屏障,保证volatile变量不会与其之后的代码进行指令重排序。

总结

对于volatile关键字变量的读写操作,本质上都是通过内存屏障来执行的。

  1. 对于读取操作来说,volatile可以确保该操作与其后续的所有读写操作都不会进行指令重排序。
  2. 对于修改操作来说,volatile可以确保该操作与其上面的所有读写操作都不会进行指令重排序。

内存屏障兼具了两方面能力:1. 防止指令重排序, 2. 实现变量内存的可见性。

有一点,synchronized关键字锁的实现可见性和防止指令重排序也是通过内存屏障来实现的,内存屏障会加在monitorenter和monitorexit指令之间

    monitorenter
    内存屏障(Acquire Barrier,获取屏障)
    ......

    内存屏障 (Release Barrier,释放屏障)
    monitorexit

5.补充知识happen-before原则

  1. 顺序执行规则(限定在单个线程上的):该线程的每个动作都happen-before它的后面的动作。
  2. 隐式锁(monitor)规则:unlock happen-before lock,之前的线程对于同步代码块的所有执行结果对于后续获取锁的线程来说都是可见的。
  3. volatile读写规则:对于一个volatile变量的写操作一定会happen-before后续对该变量的读操作。
  4. 多线程的启动规则:Thread对象的start方法happen-before该线程run方法中的任何一个动作,包括在其中启动的任何子线程。
  5. 多线程的终止规则:一个线程启动了一个子线程,并且调用了子线程的join方法等待其结束,那么当子线程结束后,父线程的接下来的所有操作 都可以看到子线程run方法中的执行结果。
  6. 线程的中断规则:可以调用interrupt方法来中断线程,这个调用happen-before对该线程中断的检查(isInterrupted)。

6、Volatile的使用场景

(1)状态标志(开关模式)

public class ShutDowsnDemmo extends Thread{
	private volatile boolean started=false;
	
	@Override
	public void run() {
		while(started){
			dowork();
		}
	} 
	
	public void shutdown(){
		started=false;
	}
}

(2)双重检查锁定(double-checked-locking)DCL

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

(3)需要利用有序性

posted @ 2020-06-18 23:57  紫月冰凌  阅读(173)  评论(0编辑  收藏  举报