volatile可以保证变量的可见性。 这里的变量包括类变量、实例变量,但不包括局部变量和方法参数,因为后者是线程私有的,不存在线程竞争问题java内存模型(JMM)规定,所有变量都存储在主内存中,同时每个线程还有自己的工作内存。 线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主存中的数据 不同线程也无法访问对方工作内存中的变量,线程间变量值的传递需要通过主存来完成(引自周志明《深入理解Java虚拟机》)volatile可以保证 (1)任何线程更新volitale修饰的变量后都会立即同步到主存中 (2)线程每次读取volitale修饰的变量,都必须到主存中取最新值 以上亮点保证了“可见性”,即任意线程对变量的修改能立即被其他线程所知
package com.concurrent;public class VolatileDemo {// 如果不加volatile,你会发现线程2已经结束很久了,线程1还在死循环。// 但是一旦加上volatile,线程2一执行,线程1会立刻跳出循环 // 这是因为volatile可以保证变量的可见性。// 这里的变量包括类变量、实例变量,但不包括局部变量和方法参数,因为后者是线程私有的,不存在线程竞争问题// java内存模型(JMM)规定,所有变量都存储在主内存中,同时每个线程还有自己的工作内存。// 线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主存中的数据,// 不同线程也无法访问对方工作内存中的变量,线程间变量值的传递需要通过主存来完成。// volatile可以保证// (1)任何线程更新volitale修饰的变量后都会立即同步到主存中// (2)线程每次读取volitale修饰的变量,都必须到主存中取最新值// 以上亮点保证了“可见性”,即任意线程对变量的修改能立即被其他线程所知////public static Integer money = 1000;public volatile static Integer money = 1000;public static void main(String[] args) throws InterruptedException {//线程1new Thread(() -> {while (money == 1000) {}System.out.println("存款已经不是" + money + "了");}).start();Thread.sleep(2000);//线程2new Thread(() -> {money = 900;System.out.println("存款现在是" + money);}).start();}
}
但是,volatile并不能保证原子性。下述代码如果能保证原子性的情况下应该返回200000,但实际执行后打印的结果远小于这个值。这是因为race++这个看似简单的操作实际上包含3个步骤:
(1)从主存获取值
(2)执行加1(iconst_1,iadd指令)
(3)写回主存
在线程执行iconst_1,i_add这些指令的时候,其他线程可能已经把race的值改变了,因此当前内存写回主存时就可能覆盖掉最新的值而把老值加1的结果写回去。(此处代码和解释均引自周志明《深入理解Java虚拟机》)
package com.concurrent;public class VolatileDemo2 {public static volatile int race = 0;public static void increase(){race++;}private static final int THREADS_COUNT = 20;public static void main(String[] args) {Thread[] threads = new Thread[THREADS_COUNT];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(()->{for (int j = 0; j < 10000; j++) {increase();}});threads[i].start();}while(Thread.activeCount() > 2){Thread.yield();}System.out.println(race);}
}
同时,volatile能防止指令重排。所谓指令重排,就是虚拟机在执行执行时,不考虑并发的影响,再保证结果不变的情况下,将部分指令重新排序的现象,也就是所谓的“线程内表现为串行”(With-Thread as-if-serial semantics).
一个典型的例子是懒汉是单例的双重检测锁模式。如果不加volatile修饰,由于new对象并非原子操作,就有可能出现指令重排的现象。new对象分为三个步骤(1)分配内存空间 (2)执行构造方法,初始化对象 (3)把对象指针执行这片空间。指令重排后原本(1)(2)(3)的顺序可能变成(1)(3)(2),这样有可能线程A执行完(1)(3),正要执行(2)的时候,线程B抢到执行权,判断lazyMan == null,为false,于是返回对象,但是此时的对象还并没有初始化,这时候去使用此对象显然会有问题。比如这里的使用就是打印,那就有可能出现先打印对象,然后再执行构造方法中的打印。但是,经过多次尝试,并没有出现1次这个问题,是概率太小还是理论有误?
package com.concurrent;public class LazyMan {private LazyMan(){System.out.println(Thread.currentThread().getName()+"ok");}//private static LazyMan lazyMan = null;private static volatile LazyMan lazyMan = null;//双重检测锁模式的懒汉式单例 DCL懒汉式public static LazyMan getInstance(){if(lazyMan == null){synchronized (LazyMan.class){if(lazyMan == null){lazyMan = new LazyMan();}}}return lazyMan;}public static void main(String[] args) {for (int i = 0; i < 100; i++) {new Thread(new Runnable() {@Overridepublic void run() {LazyMan lazyMan = LazyMan.getInstance();System.out.println(lazyMan);}}).start();}}}