可以保证
为什么能实现这些功能,其底层原理就是内存屏障
volatile关键字可以保证共享变量可见性,相较于普通的共享变量,使用volatile关键字可以保证共享变量的可见性
一句话,volatile修饰的变量在某个工作内存修改后立刻会刷新会主内存,并把其他工作内存的该变量设置为无效。
回忆volatile的作用
可见性
有序性
内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令 ,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性 。
内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
一句话:对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。
写屏障(Store Memory Barrier) :告诉处理器在写屏障之前将所有存储在缓存(store bufferes) 中的数据同步到主内存。也就是说当看到Store屏障指令, 就必须把该指令之前所有写入指令执行完毕才能继续往下执行。
读屏障(Load Memory Barrier) :处理器在读屏障之后的读操作, 都在读屏障之后执行。也就是说在Load屏障指令之后就能够保证后面的读取数据指令一定能够读取到最新的数据。
重排序有可能影响程序的执行和实现, 因此, 我们有时候希望告诉JVM你别“自作聪明”给我重排序, 我这里不需要排序, 听主人的。
对于编译器的重排序, JMM会根据重排序的规则, 禁止特定类型的编译器重排序。
对于处理器的重排序, Java编译器在生成指令序列的适当位置, 插入内存屏障指令, 来禁止特定类型的处理器排序。
对一个 volatile 域的写, happens-before 于任意后续对这个 volatile 域的读,也叫写后读。
这里暂时先有个印象着就行
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。
读屏障
volatile读
操作的后面插入一个LoadLoad
屏障volatile读
操作的后面插入一个LoadStore
屏障写屏障
在每个volatile写
操作的前面插入一个StoreStore
屏障
在每个volatile写
操作的后面插入一个StoreLoad
屏障
public class VolatileTest1 {
// static boolean flag = true;//不加volatile,没有可见性
static volatile boolean flag = true;//加volatile,有可见性
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
while (flag){//默认flag是true,如果未被修改就一直循环,下面那句话也打不出来
}
System.out.println(Thread.currentThread().getName()+"\t flag被修改为false,退出.....");
},"t1").start();
//暂停几秒
TimeUnit.SECONDS.sleep(2);
flag=false;
System.out.println("main线程修改完成");
}
}
//没有volatile时
//t1 come in
//main线程修改完成
//--------程序一直在跑(在循环里)
//有volatile时
//t1 come in
//main线程修改完成
//t1 flag被修改为false,退出.....
上述代码原理解释
问题可能:
我们的诉求:
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:
Java内存模型定义了8种每个线程工作内存与物理主内存之间的原子操作
由于上述6条只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
其中最核心的操作是在于我们的write操作,当我们从工作内存写到主内存的时候,会进行lock加锁操作,加锁后会清空其他线程工作内存变量的值,如果其他线程要使用该变量前必须重写从主内存加载值,当write完毕后,进行unlock进行解锁 ,这样就保证了可见性
synchronized
和volatile
代码演示class MyNumber{
//volatile int num=0;
int num=0;
public synchronized void add(){
num++;
}
}
public class VolatileNoAtomicDemo {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
for (int i = 0; i <10; i++) {
new Thread(()->{
for (int j = 0; j < 100; j++) {
myNumber.add();
}
}).start();
}
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "\t" + myNumber.num);
}
}
//-------------volatile情况下
//main 941
//-----------synchronized请款下
//main 1000
当线程1对主内存对象发起read操作到write操作第一套流程的时间里,线程2随时都有可能对这个主内存对象发起第二套操作
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序
不存在数据依赖关系,可以重排序;
存在数据依赖关系 ,禁止重排序
但重排后的指令绝对不能改变原有的串行语义!这点在并发设计中必须要重点考虑!
下面这两个单一赋值可以的
volatile int a = 10;
volatile boolean flag = false
//这个前面讲过
public class UseVolatileDemo{
private volatile static boolean flag = true;
public static void main(String[] args){
new Thread(() -> {
while(flag) {
//do something......循环
}
},"t1").start();
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(() -> {
flag = false;
},"t2").start();
}
}
当读远多于写
public class UseVolatileDemo{
//
// 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
// 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
public class Counter {
private volatile int value;
public int getValue(){
return value; //利用volatile保证读取操作的可见性
}
public synchronized int increment(){
return value++; //利用synchronized保证复合操作的原子性
}
}
}
public class SafeDoubleCheckSingleton
{
private static SafeDoubleCheckSingleton singleton; //-----这里没加volatile
//私有化构造方法
private SafeDoubleCheckSingleton(){
}
//双重锁设计
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
//1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
singleton = new SafeDoubleCheckSingleton();
//实例化分为三步
//1.分配对象的内存空间
//2.初始化对象
//3.设置对象指向分配的内存地址
}
}
}
//2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
return singleton;
}
}
单线程情况下
//三步
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置对象指向分配的内存地址
多线程情况下(由于指令重排序)
隐患:多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象 。(没初始化完的就是null)
正常情况
//三步
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置对象指向分配的内存地址
非正常情况
//三步
memory = allocate(); //1.分配对象的内存空间
instance = memory; //3.设置对象指向分配的内存地址---这里指令重排了,但是对象还没有初始化
ctorInstance(memory); //2.初始化对象
解决
public class SafeDoubleCheckSingleton
{
//通过volatile声明,实现线程安全的延迟初始化。
private volatile static SafeDoubleCheckSingleton singleton;
//私有化构造方法
private SafeDoubleCheckSingleton(){
}
//双重锁设计
public static SafeDoubleCheckSingleton getInstance(){
if (singleton == null){
//1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
synchronized (SafeDoubleCheckSingleton.class){
if (singleton == null){
//隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
//原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
singleton = new SafeDoubleCheckSingleton();
}
}
}
//2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
return singleton;
}
}
实例化singleton分多步执行(分配内存空间、初始化对象、将对象指向分配的内存空间),某些编译器为了性能原因,会将第二步和第三步进行重排序(java分配内存空间、将对象指向分配的内存空间、初始化对象)。这样,某个线程可能会获得一个未完全初始化的实例。
内存屏障是一种屏障指令,它使得CPU或编译器对屏障指令的前和后所发出的内存操作执行一个排序的约束。也叫内存栅栏或栅栏指令
3句话总结