在多线程并发编程中synchronized和volatile都扮演者重要角色。volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
内存可见性:线程A对一个volatile变量的修改,对于其他线程是可见的,即线程获取volatile变量的值都是最新的。
内存屏障(memory barriers)是一组处理器指令,用于实现对内存操作的顺序限制。
缓冲行(cache line)CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时,会加载整个缓存行。
原子操作(atomic operations)不可中断的一个或一系列操作。
在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile写操作时的情况。
Java代码
instance = new Singleton();//instance是volatile变量
转变为汇编代码
0x01a3deld:movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
有volatile变量修饰的共享变量进行写操作时会多一行汇编代码,其中有lock关键词,Lock前缀指令在多核处理器下会引发两件事情:
为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但是操作完不知道何时写到内存中。
如果对声明了volatile变量进行写操作,JVM就会向处理器发送一个Lock前缀指令,将这个变量所在的缓存行的数据写回到系统内存。但其他处理器缓存的值还是旧的,所有在多处理器环境下,为保证各个处理器的缓存是一致的,就回实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将该缓存行设置为无效状态,当处理器对这个数据进行操作时就会重新从内存中读取数据到缓存中。
1.Lock前缀指令会引起处理器缓存写回到内存。
2.一个处理器的缓存写回到内存会导致其他处理器的缓存无效。
通过关键字sychronize可以防止多个线程进入同一段代码,在某些特定场景中,volatile相当于一个轻量级的sychronize,因为不会引起线程的上下文切换,但是使用volatile必须满足两个条件:
在项目中经常会用到volatile关键字的两个场景:
1、状态标记量
在高并发环境中,通过一个boolean类型的变量 flag,控制代码是否走其他逻辑。
public class Handler {
private volatile flag;
public void setFlag(boolean flag) {
this.flag = falag;
}
public void run() {
if (flag) {
//其他逻辑
} else {
//正常逻辑
}
}
}
用户的请求线程执行run方法,如果需要开启其他活动,可以通过后台设置,具体实现可以发送一个请求,调用其他方法并设置flag为true,由于flag是volatile修饰的,所以一经修改,其他线程都可以拿到flag的最新值,用户请求就可以执行其他逻辑了。
2.双检锁/双重校验锁(DCL,即 double-checked locking)
单例模式的一种实现方式,如果没有volatile关键字,会出现bug。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
参考资料:
-《Java并发编程的艺术》 方腾飞 魏鹏 程晓明 著
- https://www.jianshu.com/p/195ae7c77afe 占小狼