目录
volatile在多线程程序中的作用,一是保证线程可见性,二是禁止指令重排序。
1、保证线程可见性
线程安全本质是数据安全,线程可见性本质也是数据可见性。即一个变量或对象被多个线程访问的问题。一个线程对一个变量做了操作,如何使得其他线程能知道该变量发生变化,并在变化的基础上继续运算操作。
(1)cpu高速缓存
CPU的运算速度远快与内存的存取速度,“水桶效应”使得计算机不能完全发挥cpu的高性能。为解决该问题,在cpu内集成了高速缓存,用于缓存常用的数据,但是多核cpu相当于每个核都有自己的高速缓存,如何保证多份数据拷贝一致,于是就有了缓存一致性原理的解决方案。MESI(Modified Exclusive Shared Or Invalid)(也称为伊利诺斯协议,是因为该协议由伊利诺斯州立大学提出)是一种广泛使用的支持写回策略的缓存一致性协议。
在JVM的内存模型中,每个线程有自己的工作内存,线程的工作内存其实是cpu寄存器和高速缓存的抽象。
(2)验证程序
/**
* volatile 关键字,使一个变量在多个线程间可见
* A B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道
* 使用volatile关键字,会让所有线程都会读到变量的修改值
*
* 在下面的代码中,running是存在于堆内存的t对象中
* 当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去
* 读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行
*
* 使用volatile,将会强制所有线程都去堆内存中读取running的值
*/
package basic._volatile;
import java.util.concurrent.TimeUnit;
public class HelloVolatile {
/*volatile*/ boolean running = true; //对比一下有无volatile的情况下,整个程序运行结果的区别
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}
public static void main(String[] args) {
HelloVolatile t = new HelloVolatile();
new Thread(t::m, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.running = false;
}
}
2、禁止指令重排序
(1)指令重排序
为了充分的利用CPU,在编译器和CPU执行期,都可能对指令重排。
Object o=new Object();
new一个对象的三个步骤:
a.开辟内存空间
b.初始化、赋值
c.将堆内存空间的地址赋给栈里的对象引用o。我们看一下单例模式中的双重检查的实现方式。
public class Singleton{
private static Singleton instance =null;
private Singleton(){}
public static Singleton getInstance(){
if(instance ==null){
synchronzied(Singleton.class){
if(instance ==null){
instance =new Singleton();//
}
}
}
return instance;
}
}
上面这段代码,初看没问题,但是在并发模型下,可能会出错,那是因为instance= new Singleton()并非一个原子操作,它实际上下面这三个操作:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象
如此,两个同时进入 if(instance ==null)判断的线程,一个线程拿到锁但是并没有完全初始化完成一个实例对象时,另一个对象拿到锁,直接将未初始化完成的对象引用返回。
为防止类似情况发生,我们要做private static Singleton instance =null;中加volatile关键字。
https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html