volatile 是什么?
java虚拟体提供的轻量级同步机制,可以保证内存可见性,不能保证原子性,禁止指令重排序
java内存模型
java memory model。java内存模型一种抽象概念或规范,通过这组规范定义了程序访问变量(实力字段,静态字段,数组元素等)的访问方式。
JVM运行程序的实体是线程,每个线程在创建是JVM都会为其创建一个工作内存或称栈空间,工作内存是每个线程私有的数据区域,而java内存模型中规定变量都是存储在主内存中,主内存是线程共享的内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,首先要将变量从主内存中拷贝到自己的工作内存中,对变量进行操作,操作完成后写会主存中,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中变量的副本,线程之间无法访问对方的工作内存,线程间的通信(值的传递)必须通过主内存来完成。
内存可见性测试
class MyData{
int number = 0;
public void addNumber(){
this.number = 10;
System.out.println("number值改为10");
}
}
public class TestVolatile {
public static void main(String[] args) {
MyData myData = new MyData();
// 开启一个新的线程延时3毫秒后改变number的值
new Thread(()->{
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addNumber();
System.out.println("线程"+Thread.currentThread().getName()+"修改了myData的number值");
},"线程A").start();
// 如果myData的number一直为0则程序死循环不结束
while(myData.number == 0){
}
System.out.println("main线程结束,myData.number != 0 ");
}
}
程序运行结果出乎意料,在线程A修改number的值后并没有正常的结束,由于while条件判断执行的很快,没有时间去主存中读取已经被线程A修改后的number值,一直在判断的是main线程工作内存中的number的副本。(可以在while中添加延时操作,这样main线程有机会重新加载主存中的值也可以结束),正常情况下JMM不能保证内存可见性,线程A修改变量number,对于main线程不是实时可见的。在number成员变量前添加 volatile 修饰 保证内存可见,程序即可正常退出。
不保证原子性测试
volatile并不能保证变量操作的原子执行,只能保证变量的修改时其他线程实时可见。(++操作不是原子的),如果想要保证原子性,可以使用synchronized、ReentrantLock或AtomicInteger
class MyData2 {
volatile int number = 0;
public void addNumber() {
this.number++;
}
}
public class TestVolatileAtomic {
public static void main(String[] args) {
MyData2 myData = new MyData2();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.addNumber(); }
}, String.valueOf(i)).start();
}
// 如果有多余的线程,main线程与gc共2个
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(myData.number);
}
}
为什么volatile可以保证内存可见?
Java内存模型也规定了工作内存与主内存之间交互的协议,定义了8种原子操作:
- lock:将主内存的变量锁定,为一个线程所独占。
- unlock:将lock加的锁定解除,此时其他线程可以有机会访问此变量。
- read:将主内存中的变量值读到工作线程中。
- load:将read读取到的值保存到工作内存中的变量副本中。
- use:将值传递给线程的代码执行引擎。
- assign:将执行引擎处理返回的值重新赋值给变量副本。
- store:将变量副本的值存储到主内存中。
- write:将store存储的值写入到主内存的共享变量中。
lock、unlock是同步锁所产生的(如:synchronized、com.util.concurrent中的原子类)。
volatile 的作用就是保证【read、load、use】与【assign、store、write】这每组里面的操作都是有序的,强制,三条指令一起执行,实际意义就是,每次使用变量都从主内存中读取,每次修改完变量都立即刷新回主存中。
volatile禁止指令重排
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。
int a = 10; // 1
int b = 20; // 2
int c = a*b; // 3
1,2的执行顺序,并不影响步骤3的执行结果,程序在运行期间可能根据情况对指令进行重排序提升运行效率。
重排序带来的问题
一个经典的案例就是双重检测锁实现单例模式。
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton(); //非原子操作
}
}
}
return instance;
}
}
由于jvm可能会对非原子操作进行重排序,这就会带来一些问题。
instance = new Singleton();
对象的new操作并不是原子性的。实际上new操作可抽象为三个操作指令
memory =allocate();
instance =memory;
ctorInstance(memory);
- 分配内存空间
- instance引用指向分配的内存空间
- 使用分配的内存初始化对象
如果jvm对new操作进行了指令重排可能会是这样
memory =allocate();
instance =memory;
ctorInstance(memory)
- 分配内存空间
- instance引用指向分配的内存空间
- 初始化对象
这就会带了一个问题,在多线程中,线程A第一次获取单实例执行new Singleton操作,instace为null,在执行memory =allocate();instance =memory;操作后发生线程的切换,线程B获取单实例,但这时instance引用指向了分配的内存已经不为空,但内存的初始化工作还没有执行,这就让线程B拿到了一个未初始化完成的对象,给instance添加volatile修饰禁止指令重排问题解决。
总结
volatile的关键作用
- 保证多线程环境中,变量在线程之间的实时可见(在AQS,原子变量中都有使用)。
- 防止指令重排序。
切记 volatile无法保证原子性
交流Q群 892480622