volatile
Java并发编程一:并发基础必知
Java并发编程二:Java中线程
什么是volatile
在Java的concurrent包里面用了大量的volatile,相比较于锁,volatile是轻量级的,它不会阻塞线程,保证了变量可见性。下面一段代码可以看出volatile的使用,可以想象一下如果不使用volatile可以停止线程嘛?
// private static volatile boolean flag=false;
private static boolean flag=false;
private static int i;
public static void main(String[] args) throws InterruptedException {
// 启动一个线程不停的i++
new Thread(()->{
while (!flag){
i++;
}
}).start();
// main线程睡眠 保证上面的运行
Thread.sleep(1000);
// 修改变量值,使得线程停止退出
flag=true;
System.out.println(i);
}
答案是线程不会停止。这是因为JMM定义了变量保存在主内存,线程拷贝主内存变量到自己工作内存。 由于是普通变量,main线程修改完后不会立即写入到主内存,线程之间通信必须通过主内存来进行通信。
如果使用volatile修饰变量,它会把修改后的值立即写入到主内存,其他线程对应的值置为无效,会从主内存重新读取变量。
定义volatile变量时就具备了两个特性:保证了变量在内存的可见性,禁止重排序优化。volatile是使用内存屏障来禁止重排序。
内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制。在前两章我们知道了最后程序运行是通过编译器和处理器优化完后的指令,在使用volatile修饰变量时,在每个volatile写操作前插入一个StoreStore屏障,写操作后插入一个StoreLoad屏障,在每个volatile读操作插入LoadLoad屏障和LoadStore屏障。
volatile写操作:
volatile读操作:
下面一个列子说明:
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
在进行重排序的时候。flag是volatile修饰的,那么语句1和语句2不能放到语句3后执行,语句4语句5也不能放到语句3之前,并且语句1语句2必定是执行完毕的,其运行结果对语句345可见,但是语句12顺序可以调整,语句45顺序也可以调整。
单例中的volatile
大家肯定写过双重检查式单例,至于为什么这么写、单例的好处是什么可以自行百度。
public class Singleton {
// 语句1
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { //线程B
synchronized (Singleton.class) {
if (singleton == null) {
// 语句2
singleton = new Singleton(); 线程A} }
}
return singleton;
}
}
这里语句1修饰singleton使用了volatile,至于为什么使用它来修饰,是因为语句2 singleton = new Singleton(),可以看成是三条指令:1.分配对象内存空间,2.初始化对象,3.设置singleton 指向刚才的内存空间。在这三条指令可能会被重排序变成132,导致线程获取一个未初始化的对象。比如线程A运行到new Singleton(),此时线程B 运行到第一次判断是否为null,如果不用volatile修饰变量,那么就会出现以下情况:
线程A | 线程B |
---|---|
分配内存空间 | |
设置singleton 指向内存空间 | |
判断 singleton 是否为null | |
由于singleton不为null,访问引用对象 | |
初始化对象 | |
访问引用对象 |
何时使用volatile
使用volatile不会造成线程阻塞,不会造成线程上下文的切换,虽然可以保证线程的可见性,但是不能保证原子性。那么何时使用volatile:
- 写入的变量不依赖当前变量值,保证了可见性,不保证原子性。如果依赖当前值,比如i++,获取-计算-写入三步。
- 读写变量时没有加锁。因为加锁已经可以保证可见性。