概述

volatile是Java的一个修饰符,它在多线程编程开发中保证了共享变量的可见性有序性。相对于各种排他锁,volatile在使用和执行成本上占用资源较少。

实现原理

那么volatile如何保证可见性和有序性呢?我们写一段单例模式的java代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.test;

public class SingletonObject {

// 单例对象
private static volatile SingletonObject instance;

// 获取单例对象方法
public static SingletonObject get(){

if(instance == null){
instance = new SingletonObject();
}
return instance;
}

public static void main(String[] args) {
SingletonObject.get();
}
}

然后用idea运行main方法并打印汇编代码,jvm参数:
-server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly
-XX:CompileCommand=compileonly,*SingletonObject.get (只打印SingletonObject的get方法)

运行打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# {method} {0x0000000128e022b0} 'get' '()Lcom/test/SingletonObject;' in 'com/test/SingletonObject'
# [sp+0x40] (sp of caller)

省略代码.....

0x000000010af1fb54: movb $0x0,(%rax,%rsi,1)
0x000000011b6f4e58: lock addl $0x0,(%rsp) ;*putstatic instance
; - com.test.SingletonObject::get@13 (line 12)
省略代码.....

0x000000010af1f701: mov %r12b,(%r11,%r10,1)
0x000000011b6f4a05: lock addl $0x0,(%rsp) ;*putstatic instance
; - com.test.SingletonObject::get@13 (line 12)
省略代码.....

我们可以看到被volatile修饰的共享变量进行写操作的时候,会比普通公共变量的读写操作多一行lock addl $0x0,(%rsp)前缀的代码,lock前缀指令有俩个作用:

  • 使用总线锁或缓存一致性协议来保证数据的可见性。
  • 不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序保证有序性。

总结

volatile的使用场景不是很多,常用在多线程下的状态标记量和双重检查等,也有很多地方配合CAS来实现无锁编程。因为volatile只能保证线程每次拿到的数据是最新的,对于数据的单纯查询没有任何问题(jvm自动保证基本数据类型和引用的取值赋值为原子操作,lock指令保证有序性和可见性),但是对于i++、懒汉式单例模式等对变量操作依赖当前值的情况,就显得无能为力。

评论