一、作用
1、保障线程可见性
main线程和T线程共享堆内存的数据;main和T也有本人的工作空间,当要拜访共享内存的数据flag,会把共享内存的flag复制一份到本人的工作空间中。
例如main线程对flag进行了扭转,首先是在本人的空间进行扭转,批改后的值会马上批改到共享内存;然而T线程何时查看共享内存的值有没有被扭转不好管制。
即 main线程的批改并没有及时的反馈到T线程,也就是线程之间不可见。对这个变量加了volatile后,可能保障一个线程对这个变量批改后,另一个线程可能马上晓得。
底层是通过CPU的缓存一致性协定(MESI)保障的。
2、禁止指令重排序
指令重排序:CPU原来执行一条指令时,是一步一步的程序执行;当初的CPU为了提高效率,会并发的执行指令,就是第一个指令执行到一半时,第二个指令可能曾经开始执行了,也就是流水线似的执行。这时就要求编译器,要可能对指令重排序产生影响的状况进行相应的解决,此时volatile就派上用场了。
拿DCL单例(Double Check Lock)来解释
/** * @author Java和算法学习:周一 */public class Singleton { private static volatile Singleton INSTANCE; private Singleton() { } public static Singleton getInstance() { if (INSTANCE == null) { //双重查看 synchronized (Singleton.class) { if (INSTANCE == null) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } INSTANCE = new Singleton(); } } } return INSTANCE; } public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { System.out.println(Singleton.getInstance().hashCode()); }).start(); } } }
INSTANCE = new Singleton();(如果外面有个变量int a = 100)new对象的过程分为3步
1)申请内存(赋默认值);a=0
2)初始化;a=100
3)赋值,把值给变量;把a的值赋值给INSTANCE,即让INSTANCE指向变量a的地址
如果外面存在指令重排序的话,就存在还未初始化的变量就进行了赋值操作,即2、3两步地位替换了;也就是对象处于半初始化状态就进行了赋值操作。当第一个线程(只管加了锁),执行到new Singleton()时,new了一半;此时第二个线程来了,首先判断INSTANCE 是否为空,因为INSTANCE曾经是半初始化状态,外面曾经有值了,不再是空值了,也就是第二个线程曾经拿到了这个对象了,这个线程就能够间接应用该对象了,很可能就会应用外面的这个值。原本冀望这个值是100,然而这个值却是0,如果这个值是订单数的值这时就存在问题。加了volatile后,对这个对象的指令重排序就不容许存在,即必须是在初始化实现之后才会进行赋值操作。
3、volatile不能保障原子性
/** * @author Java和算法学习:周一 */public class T { public volatile int count = 0; public synchronized void m() { for (int i = 0; i < 1000; i++) { count++; } } public static void main(String[] args) { T t = new T(); List<Thread> threadList = new ArrayList<>(); for (int i = 0; i < 10; i++) { threadList.add(new Thread(t::m)); } threadList.forEach((o)->{ o.start(); }); threadList.forEach((o)->{ try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); }}
m办法,如果不加synchronized,count始终不会到10000。起因是,当一个线程把count值批改为1后,此时进来了第二、第三个线程,都读到count为1,批改后把count加1(即2)写回去,这时两个线程对count批改后,count的值只从1变到了2,所以最初的后果总是小于10000。归根结底就是count的值是保障了可见性,然而count++自身不是原子性的操作(底层分为几个步骤)。
volatile能保障线程的可见性,然而并不能代替synchronized保障原子性。