###作用
- 保证不同线程对
volatile
修饰的变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 - 禁止进行指令重排序。
##volatile 的可见性
public class Test_09 {
/*volatile*/ boolean b = true;
void m(){
System.out.println("start");
while(b){}
System.out.println("end");
}
public static void main(String[] args) {
final Test_09 t = new Test_09();
new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t.b = false;
}
}
运行结果:
start
然后一直死循环阻塞。
我们不是修改了 b 吗?为什么还在执行循环?
这就是线程的可见性问题了。
###原因
在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile
修饰的变量是线程可见的,当 JVM 解释 volatile
修饰的变量时,会通知 CPU,在计算过程中,每次使用变量参与计算时,都会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。
当把上面代码的 boolean 前加上 volatile
时,
运行结果:
start
end
volatile
只是通知 OS 底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同步。
volatile
通知 OS 操作系统底层,在 CPU 计算过程中,都要检查内存中数据的有效性。保证最新的内存数据被使用。
##volatile 的非原子性问题
代码解读:
我在 main 方法中启动了 10 个线程,然后通过调用 join 方法使得一个线程执行完以后再执行其他线程,通过 volatile
关键字可以让每个线程都取到最新的数据 i。
i 初始化为 0 。run 方法就是把 i 加 10000。预想结果 1 个线程加 10000,那么 10 个线程加 10 0000。
public class Test_10 {
volatile int count = 0;
/*synchronized*/ void m(){
for(int i = 0; i < 10000; i++){
count++;
}
}
public static void main(String[] args) {
final Test_10 t = new Test_10();
List<Thread> threads = new ArrayList<>();
for(int i = 0; i < 10; i++){
threads.add(new Thread(new Runnable() {
@Override
public void run() {
t.m();
}
}));
}
for(Thread thread : threads){
thread.start();
}
for(Thread thread : threads){
try {
thread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
System.out.println(t.count);
}
}
运行结果:
35694
为什么会是 35694 而不是 10 0000 呢?
这就是线程的原子性问题了。
在多线程执行的过程中,当线程 A 取到 i=2000 时,i 这个值会被缓存到高速缓存中,此时 CPU 中断,线程 B 获得 CPU 资源并开始计算,此时内存中 i 的值还是 2000。因为线程 A 还没有来得及进行加的操作就让出了 CPU。当线程 B 执行 +1 的操作后 i 变为 2001。此时线程 A 重新获得 CPU ,执行 +1 操作。缓存中的 i 变为 2001 写入到内存中,把刚才线程B 运算的结果覆盖。这样的话就相当于俩次循环 i 加了 1。发生多次这样的情况下,得到的结果就会比预想的少了许多。
volatile
只能保证可见性,不能保证原子性。
DCL 的问题
在并发的情况下可能会存在读写同步的情况。 volatile 保证了对于它修饰的属性的写先于读发生(happens-before 规则),这样就可以避免写的时候如果一个对象还没有完成初始化就被其他线程读到的情况。