文章目录
- volatile的作用
- 一、volatile实现内存可见性
- 二、volatile禁止代码重排序
- 三、volatile不支持原子性
提示:以下是本篇文章正文内容,Java系列学习将会持续更新 volatile的作用
1. volatile能保证变量在多线程之间的可见性。
2. volatile禁止CPU执行时进行指令重排操作(内存屏障)从而能保证有序执性。
3. volatile不能保证原子性。
内存可见性问题
内存可见性问题:在多线程共享一个数据块下,一个线程对数据进行修改操作时,其它线程是无法感知的。甚至会被编译器优化到完全不可见的程度。
例如:当两个线程同时操作一个内存,例如一个读一个写,但是当“写线程”进行的修改的时候,“读线程”可能读到修改前的数据,也可能读到修改后的数据,这是不确定的。
不可见的原因:
CPU为了提高数据获取速率,会设置缓存。
在多核CPU下,每个核都有自己的独占缓存进行数据存取,只有在所有处理结束后,才会将数据同步到主存中。
所以会导致有些核读取到的是过期的数据。
volatile的原理
简单来说:当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。
当一个线程向被volatile关键字修饰的变量写入数据的时候,虚拟机会强制它被值刷新到主内存中。
当一个线程用到被volatile关键字修饰的值的时候,虚拟机会强制要求它从主内存中读取。
内部如何做到缓存主存同步的?
缓存一致性协议(MESI):当CPU写数据时,如果发现操作的变量时共享变量,即其他线程的工作内存也存在该变量,于是会发信号通知其他CPU该变量的内存地址无效。当其他线程需要使用这个变量时,如内存地址失效,那么它们会在主存中重新读取该值。
现象:
import java.util.concurrent.TimeUnit;
public class Demo {
// 保证内存可见性
public static volatile boolean flag = true;
static class Thread1 extends Thread {
@Override
public void run() {
while (flag) {
// 一直循环,等待flag被改变
}
System.out.println("线程1发现flag被改变了。。。");
}
}
static class Thread2 extends Thread {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("线程2修改了flag的值。。。");
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 t1 = new Thread1();
Thread2 t2 = new Thread2();
t2.start();
t1.start();
}
}
回到目录…
二、volatile禁止代码重排序指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。
这在单线程中不会构成问题,但是在多线程中就会出现问题。
非常经典的例子是在单例方法中同时对字段加入voliate,就是为了防止指令重排序。
private volatile static LazyModeV3 instance;
对象实例化的步骤:
memory = allocate(); // 1.分配对象内存空间 instance(memory); // 2.初始化对象 instance = memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null
代码重排序后的步骤:
memory=allocate(); // 1.分配对象内存空间 instance=memory; // 3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成! instance(memory); // 2.初始化对象
volatile的原理
volatile禁止重排序是利用内存屏障,保证有序性。
内存屏障是一组CPU指令,用于实现对内存操作的顺序限制。
Java编译器在生成指令系列时,在适当的位置会插入内存屏障来禁止处理器对指令的重新排序。
(1)volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的。
(2)volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。
回到目录…
三、volatile不支持原子性直接上代码,看现象:
public class Test {
public static volatile int i = 0;
static class Thread1 extends Thread {
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
i ++;
}
}
}
static class Thread2 extends Thread {
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
i --;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread1 t1 = new Thread1();
t1.start();
Thread2 t2 = new Thread2();
t2.start();
t1.join();
t2.join();
System.out.println(i);
// 结果是随机数,说明volatile不支持原子性
}
}
总结:volatile不能保证原子性,要想保证原子性我们要使用锁机制。
回到目录…
总结:
提示:这里对文章进行总结:
以上就是今天的学习内容,本文是Java多线程的学习,深入学习了关键字volatile的作用以及原理。之后的学习内容将持续更新!!!



