volatile关键字主要有两大特性:
1、内存可见性
2、防止指令重排(指令重排是JVM底层优化)
内存可见性解析
多个线程对同一个变量进行操作时,其他线程对变量的修改对于当前线程不可见。线程对变量进行操作时,会去主存中读取该变量的值,然后将该值作为副本保存在当前线程中,当有其他线程对该值进行修改时,当前线程并不会去主存中读取最新值,而是继续使用副本值。
代码示例
/**
* 启动两个线程任务,分别对Apple实例中的num变量进行读写
*/
public class VolatileTest {
public static void main(String[] args) {
Apple apple = new Apple();
new Thread(new Runnable() {
@Override
public void run() {
/** 线程不睡,后面线程读取的就是刷新后的值
* 线程睡了之后,后面线程从主存中读取的就是修改前的值,该值存在后面线程中作为副本,
* 其他线程修改的值刷新到主存中,但是后面线程不会去主存中读取,所以后续while循环的值是线程副本中的值,
* 所以其他线程修改的值对后面线程不可见,因此while循环不会读取最新值一直死循环,
* 加volatile关键字修饰变量即可保证内存可见性,让线程每次读取该变量的时候都直接到主存中去读取
*/
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
apple.setNum(10);
System.out.println("已修改num值为10");
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
if (apple.getNum() == 10) {
// 不加volatile无法打印
System.out.println("--------------");
break;
}
}
}
}).start();
}
}
class Apple {
// 加volatile修饰保证内存可见性,可以去掉看下有什么异同
private volatile int num = 1;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
指令重排解析
当前创建对象的过程是
1)栈内存开启空间给对象引用
2)堆内存分配对象地址,并准备初始化对象
3)初始化对象
4)栈内存引用指向堆内存中对象地址
优化之后可能会变成 1,2,4,3。此时栈空间已经引用了堆空间的地址,但是此时对象还没初始化,那么对象的使用可能就会抛出
nullPointException.
最后
使用volatile关键字会防止JVM底层优化指令重排,指令重排提升程序执行效率,但是也会给我们程序带来不确定性(空指针异常)。
volatile确保其他线程对变量的更新操作会通知到其他线程,可以看作一个轻量级的锁,但是它与锁又有所不同:
1)没有锁的互斥性
2)不能保证变量的原子性操作