volatile可以保证变量的可见性。
这里的变量包括类变量、实例变量,但不包括局部变量和方法参数,因为后者是线程私有的,不存在线程竞争问题
java内存模型(JMM)规定,所有变量都存储在主内存中,同时每个线程还有自己的工作内存。
线程对变量的所有操作(读取、赋值等),都必须在工作内存中进行,而不能直接读写主存中的数据
不同线程也无法访问对方工作内存中的变量,线程间变量值的传递需要通过主存来完成(引自周志明《深入理解Java虚拟机》)
volatile可以保证
(1)任何线程更新volitale修饰的变量后都会立即同步到主存中
(2)线程每次读取volitale修饰的变量,都必须到主存中取最新值
以上亮点保证了“可见性”,即任意线程对变量的修改能立即被其他线程所知
java
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 {
//线程1
new Thread(() -> {
while (money == 1000) {
}
System.out.println("存款已经不是" + money + "了");
}).start();
Thread.sleep(2000);
//线程2
new 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虚拟机》)
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次这个问题,是概率太小还是理论有误?
java
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() {
@Override
public void run() {
LazyMan lazyMan = LazyMan.getInstance();
System.out.println(lazyMan);
}
}).start();
}
}
}