谈谈对volatile的理解
volatile是java虚拟机提供的轻量级同步机制,它保证了可见性,不保证原子性,并且禁止指令重排
--JMM内存模型之可见性
JMM本身是一种抽象的概念并不真实存在,它描述的是一组规则或者规范,通过这一组规范定义了程序中的各个变量的访问方式。JMM要求保证可见性,原子性和有序性
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(栈空间),工作内存时每个线程的私有数据区域,而java内存模型中规定所有变量都储存在主内存,主内存是共享内存区域,所有的线程都可以访问,但是线程对变量的操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后再对变量进行操作,操作完成后再将比变量写回主内存,不能直接操作主内存里的变量,各个线程中的工作内存中储存着主内存中的变量拷贝副本,因此不同的线程无法访问对方的工作内存,线程间的通信必须通过主内存来完成。
所谓可见性就是:当一个线程在本地内存更改了数据并且把变量写回主内存的适合要通知其他线程,现在这个变量已经更改了。
验证可见性的demo
/**
* @ Author wuyimin
* @ Date 2021/9/6-20:47
* @ Description 验证volatile的可见性
*/
public class JMMTest {
public static void main(String[] args) {
//MyData myData = new MyData();
MyDataWithVolatile myData=new MyDataWithVolatile();//添加了可见性任务之后才会打印任务完成
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"进来了");
try{
TimeUnit.SECONDS.sleep(3);
myData.change();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"更新了值");
},"A").start();
//第二个线程直接使用我们的主线程
while (myData.num==0){}//如果我们不设置可见性那么就会一直在这里自旋导致下面那句话不会打印
System.out.println(Thread.currentThread().getName()+"任务完成");
}
}
//不添加volatile关键字
class MyData{
int num=0;
public void change(){
this.num=60;
}
}
//不添加volatile关键字
class MyDataWithVolatile{
volatile int num=0;
public void change(){
this.num=60;
}
}
两种运行结果
volatile不保证原子性
/*
不保证原子性
*/
private static void notProvideAtomicTest() {
MyDataWithVolatile myData = new MyDataWithVolatile();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.add();//理论上来说保证原子性20个线程加完以后应该就是20000
}
}, "" + i).start();
}
//等待上面的线程计算完成,再使用main线程查看最后结果(这一步不影响定论,但是少了这一步最终结果会大量减少 13452
while (Thread.activeCount() > 2) {
Thread.yield();//线程让步,可以让低于当前优先级的线程优先,
}
System.out.println(myData.num);//19107..每次的值都不一样
}
//添加volatile关键字
class MyDataWithVolatile{
volatile int num=0;
public void change(){
this.num=60;
}
public void add(){
num++;//num++其实有三个操作getfield拿到原始值,iadd执行加一操作,执行putfield把累加后的值写回
}
}
如何不加synchronized来解决?(synchronized过于重量级)
使用包装类AtomicInteger
//不添加volatile关键字
class MyDataWithVolatile {
volatile AtomicInteger atomicNum=new AtomicInteger(0);
public void atomicAdd() {
atomicNum.getAndIncrement();
}
}
指令重排:
计算机再执行程序的适合,为了提高性能,编译器和处理器常常会对指令做重排,对于如下的语句
int x=1; //1
int y=2; //2
x=x+1; //3
y=x*x; //4
正常执行时1234,底层重写会考虑数据依赖性可能是2134,1324(但是怎么也不会把语句4放到前面),这两种排序在单线程下都是一致的。下面给出一个指令重排导致的结果不一致的例子:
a,b,x,y都初始为0,假设两个线程同时执行x=a.y=b
线程1执行:x=a;b=1;
线程2执行:y=b;a=2;
结果是x=0,y=0;
现在加入指令重排
线程1执行:b=1 x=a;
线程2执行:a=2 y=b;
结果是x=2,y=1;
单例模式在多线程情况下产生的不安全问题--懒汉式问题改成双重检查锁模式
public class Singleton3 {
private volatile static Singleton3 instance;//防止指令重排,保证可见性,不保证原子性
private Singleton3(){}
public static Singleton3 getInstance(){
if(instance==null){
synchronized (Singleton3.class){
if(instance==null){
instance=new Singleton3();
}
}
}
return instance;
}
}
双重检查锁模式需要加volatile关键字的原因:
instance=new SingletonDemo();并非原子操作,它的正常操作流程如下:
1.分配对象内存空间
2.初始化对象
3.设置instance指向刚刚分配的内存地址,这一步决定了instance不为null
因为23步不存在数据依赖的关系,,并且在单线程情况下,结果并不会改变.因此23步是可以发生指令重排的,那么它会引发什么问题呢
如果顺序变成了1->3->2,执行到3的时候,我们已经为instance分配了内存地址,它已经不为null了,但是初始化还没有完成,这就会导致别的线程默认它已经初始化完成了,会直接返回一个还没有进行初始化的对象