volatile作用
volatile用于保证量在并发环境中的可见性。它不需要切换线程状态所以一般情况下效率比synchronized要高很多。一般情况下在多核CPU环境中 为了提高计算效率,每个核心都会缓存变量到cpu核心本地的缓存中,cpu 计算过程会只更新本地缓存中的变量的值,待cpu比较空闲或其他恰当的实际会将本地缓存的变量值会写到系统内存。这样一来 如果一个线程修改了缓存中的值但是还没有 会写到 系统内存。那么其他线程是不会看到修改后的值的。这就是并发环境下的可见性问题。如果变量使用 volatile修饰,就可以解决可见性问题。即当cpu1修改了变量的值其他cpu会理科拿到更新后的值
除了可见性 对任意单个的volitale变量的读写具有原子性。 但是类似于 volatile++ 这种符合操作或者多个变量的操作 不具有原子性
原理:
1、对于volatile修饰的变量A,JVM会在为A赋值的代码后追加 lock指令。这个指令会告诉计算机,将更新后的值直接回写到系统主内存。
2、基于CPU硬件的缓存一致性协议,一旦变量A在主内存的值被更新那么其他缓存了变量A值的CPU会自动将缓存的值设置为无效。
如果再要读取A变量的值会直接从主内存中读取。
验证
下面代码验证了volitale对可见性的影响。
public class T {
/* volatile */ static boolean running =true;
void m1(){
System.out.println("m start");
while(running){
// System.out.println("1"); // 如果加上这行代码 即使jdk8 下即使不用 volatile 也可以停止
}
System.out.println("m end");
}
public static void main(String[] args) {
final T t=new T();
new Thread(){ // 传统线程写法 jdk8不加 valatile 只能打印 start 加了可以打印出end
@Override
public void run(){
t.m1();
}
}.start();
// new Thread(t::m1,"T1").start(); //jdk8 λ表达式写法 不加 valatile 只能打印 start 加了可以打印出end
try {
Thread.currentThread().sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t.running=false;
}
}
测试结果:
在 jdk8 环境下 如果 running变量没有使用 volatile 关键字修饰 只能看到 start输出看不到 end 输出,而如果加了 volatile 关键字 就可以同时看到 start 和end 输出。原因就是上边分析的 如果不使用volatile 线程由于缓存了变量的值,所以一直从缓存中取。加了volatile 如果主内存的值被修改线程中缓存的值失效。只能从主内存中获取最新值。
jdk8环境下如果我们不使用 volitale ,而仅仅在循环中 增加一行 打印代码的话,线程T同样会停止。这是因为虽然线程T从缓存中获取变量的值,但是一旦CPU空闲,负载不太大的 时候 ,就会尝试和主内存的值进行同步,加了打印有了ioCPU 会相对空闲,所以会同步主内存的值。
在 jdk7 环境下, 无论 running变量有没有使用 volatile 关键字修饰 都可以输出start 和 end ,即即使没有volatile main线程修改了 running 的值 T1 线程也可以看到(应该是jdk8做了优化很显然一直从缓存读对于单线程来说效率更高)。运行结果如下:
m start
m end