//最近无意在短视频中刷到关于volatile关键字的讲解,突然发现从前这个陌生的词汇也渐渐步入大家的视野,结合自己之前所掌握的知识,做一下总结。也是方便自己以后翻看查找,我会尽量用自己理解的语言来描述它。
首先,关键字volatile可以说是JVM最轻量级的同步机制,但它并不容易被正确的、完整的理解使用。所以每当我们需要处理多线程数据竞争问题的时候,一概使用了synchronized进行同步加锁。
实际可以将volatile分为三大特性来讲解,非原子性与可见性、防止指令重排序
可见性与非原子性
public class VolatileExample {
private static volatile int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++){
counter++;
}
}
});
thread.start();
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
当一个变量被volatile定义之后,在多个线程的情况下,可以保证在变量发生改变的时候,其他线程便可立即可得知,而普通变量在多线程间的传递均需要通过访问主内存才能知晓。这就造成一种情况,当A线程中的变量发生修改的时候,还未传回主内存,B线程就使用原来缓存的变量值进行运算了。
而使用volatile关键字修饰的变量,当A线程变量发生修改的时候,B线程会在第一时间收到通知,至于是否能第一时间获取到修改后的值,这里就要说volatile的非原子性,也就是我上面代码所处场景。当变量进行++操作的时候,实际上是counter = counter+1 这一过程并非是原子操作,实际上时进行了多步操作,从变量counter 中读取读取counter 的值->值+1 ->将+1后的值写回counter 中。所有有人会说通过上面这段代码觉的volatile的可见性是没有用的,其实不然只是要分在什么场景下使用。只需满足两个条件1、运算结果并不依赖变量的当前值,或者能够确保只有单一线程改变变量的值。2、变量不需要与其他状态变量共同参与不变约束。
volatile boolean shutdownRuerested;
public void shutdown(){
shutdownRuerested = true;
}
public void doWork(){
while (!shutdownRuerested){
//do stuff
}
}
实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
原子性,指的是一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程打断。
java中变量的8种原子操作分为:lock(锁定)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)、unlock(解锁)
防止指令重排序
JAVA中对象的创建并不是原子性的,分为三部分,创建内存,初始化对象,将对象指针指向内存。
编译器为了提高效率,很有可能会发生指令重排,颠倒二三顺序,在多线程情况下,其他线程很有可能会读取到没有初始化的对象的对象引用而返回空对象。为了解决这一问题,JVM提供了内存屏障机制,主要分为storestore、storeload、loadstore、loadload,四种方式来防止指令重排。其底层主要是使用了总线锁定的方式。
而通过volatile修饰的对象就可以有效防止指令重排。
其著名的应用场景就是解决单例模式中懒汉模式在多线程下不安全的问题。
关于具体安全的懒汉单例模式写法可以查看我之前写过的懒汉单例模式线程安全问题解决方法