volatile的线程可见性和禁止指令重排序-多线程与高并发

目录

1、保证线程可见性

(1)cpu高速缓存

(2)验证程序

2、禁止指令重排序

(1)指令重排序


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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值