Volatile关键字解决了什么问题,它的实现原理是什么?

2021.01.08 06:01 33
阅读约 6 分钟

① 🧵volatile的作用

在并发编程中Synchronizedvolatile都扮演着重要的角色,volatile是轻量级的Synchronized它在并发编程中保证了共享变量的可见性

可见性的意思是:当一个线程修改了一个共享变量,另外一个线程可以立即读到这个修改的值。

线程对变量的所有操作都必须在线程自己的工作内存中完成,而不能直接读取主存中的变量,这是JMM的规定。所以每个线程都会有自己的工作内存,工作内存中存放了共享变量的副本。而正是因为这样,才造成了可见性的问题。

如果volatile使用恰当的话,它比Synchronized的使用成本和执行成本更低,因为它不会引入线程上下文切换的额外调度。

1.1 volatile的特性

特性简介
可见性对一个volatile变量的读,总是能看到对这个volatile变量最后的写入
原子性对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

理解volatile特性的一个办法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面是具体的示例:

class VolatileFeaturesExamples{
	volatile long vl = 0L; //使用volatile声明64位的long型变量
	
	public void set(long l){
	   vl = l;				// 单个volatile 变量的写
	}
	public void getAndIncrement(){
		vl++;				// 复合volatile变量的读/写
	}
	public long get(){
		return vl;			// 单个volatile变量的读
	}
}

假设有多个线程分别调用上面的方法,这个程序在语义上和下面的程序相似

class VolatileFeaturesExamples{
	 long vl = 0L;  
	
	public Synchronized void set(long l){
	   vl = l;				// 对单个变量的写用同一个锁同步
	}
	public void getAndIncrement(){  // 普通方法调用
		long temp =get();				// 调用已同步的读方法
		temp += 1L;					// 普通写操作
		set(temp);						// 调用已同步的写方法
	}
	public Synchronized  long get(){
		return vl;			//对单个变量的读用同一个锁同步
	}
}

因此:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

1.3 volatile写读的内存语义

写的内存语义:

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

读的内存语义:

  1. 当读一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量置为无效。线程接下来会从竹内中读取共享变量。

② 🔐volatile内存语义的实现原理

JMM(JAVA MEMORY MODEL)针对volatile分别对编译器重排序与处理器重排序进行了限制。下表是JMM针对编译器指定的重排序规则。

是否能重排序第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写  NO
volatile读NONONO
 volatile写 NONO

举个例子:第三行最后一格的意思是:在程序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

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

 

如何保证可见性

写屏障保证在该屏障之前的,对共享变量的改动,都同步到主内存中。

public void actor2(I_Result r) {
 num = 2;
 ready = true; //   volatile 赋值带写屏,保证前面的读写操作不会被
 // 写屏障
}

读屏障保证在该屏障之后的,对共享变量的读取,加载的都是主内存中的最新值。

public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

如何保证有序性

写屏障会确保指令重排序时,volatile写之前的读写操作不会跑到后面。

读屏障会确保指令重排序时,volatile读之后的任何操作不会跑到前面。

👨‍🚀总结

由于volatile仅仅保证单个变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁更强大;在可伸缩执行上,volatile更有优势。

字数:1648 发布于 2 个月前
Copyright 2018-2021 Siques