16. JUC-volatile

*简单谈谈volatile是什么意思

volatile是Java虚拟机提供的一种轻量级的同步机制

它能够保证可见性、有序性、但是不保证原子性。

*JMM

Java内存模型,本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.

*可见性

*详细解释

假设有多个线程从主内存中拷贝值到各自线程的工作内存,若有一个线程改了当前拷贝的值,并将修改好的值返回给了主内存,那就会导致其它线程操作的数据与主内存的数据产生偏差。

这个时候就必须要有一种机制,就是一旦有一个线程修改完工作内存的值,并返回给主内存之后,要及时通知其它线程,这样及时通知的这种情况,就是JMM内存模型里面的一个重要特性,可见性。

官方解释

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方被称为栈空间),

工作内存是每个线程的私有数据区域

而Java内存模型中规定所有变量都存储在主内存,主内存处于一片共享的内存区域,所有线程都可以访问,但线程不能直接操作主内存中的变量,对变量的操作(读取赋值等)必须在工作内存中进行,所以首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回主内存

各个线程中的工作内存存储着主内存的变量拷贝副本,因此不同的线程无法访问对方的工作内存,线程间的通信(传值)必须要通过主内存来完成

*写一个可见性demo

public class VolatileDemo{
	public static void main(String[] args){
		MyData m = new MyData();
		new Thread(() -> {
			System.out.println(Thread.currentThread().getName());
			try{
				TimeUnit.SECONDS.sleep(3);
			}catch(InterruptedException e){
				e.printStackTrace();
			}
			m.addNum();
		},"AAA").start();
		
		while(m.num == 0){
		
		}
		
		System.out.println("Mission Finish num " + m.num);
	}
}

 class MyData{
 	// 加不加volatile是两个结果
	volatile int num = 0;
	
	void addNum(){
		num = 60;
	}
}

原子性

*写一个不保证原子性的demo

public class VolatileDemo {
	public static void main(String[] args){
		MyData m = new MyData();
		for(int i = 1; i <= 20; i++){
			new Thread(() -> {
				for(int j = 1; j <= 1000; j++){
					m.addPlusPlus();
				}
			}, String.valueOf(i)).start();
		}
		
		while(Thread.activeCount() > 2){
			Thread.yield();
		}
		
		System.out.println(Thread.currentThread().getName() + "\t" + m.num);
	}
}

class MyData{
	int num = 0;
	
	void addPlusPlus(){
		num++;
	}
}

*为什么会出现数值少于20000?volatile为什么不能保证原子性

假设主物理内存中有一个num的变量,初始值为0,现在有3个线程要来读取并操作这个主物理内存的num,

A、B、C三个线程通过副本拷贝将num分别拷贝到了自己所在线程的工作空间,这个时候A、B线程同时调用了一个使num++的方法,

A线程的执行完之后,要将结果1返回到主物理内存进行更新时,因为多线程之间的调度关系,A线程突然被挂起了,

此时B线程也进行了同样的操作,然后B线程成功将1写入到主内存了,这时会通知其它线程,

此时挂起的A线程被唤醒,它继续将之前工作空间中计算出的1写入到主内存,结果就是A线程的1将B线程的1覆盖掉,

导致最终出现了数据丢失。

*结论:

在底层的源码编译中,putfield这步写回去的时候,有很多值因为线程的调度可能被挂起了,刚好也没有收到最新值的通知,

有这么一个纳秒级别的时间差,一写就出现了写覆盖,所以最后就把人家的值覆盖掉了

*如何解决不保证原子性的问题

使用AtomicInteger

public class VolatileDemo {
	public static void main(String[] args){
		MyData m = new MyData();
		for(int i = 1; i <= 20; i++){
			new Thread(() -> {
				for(int j = 1; j <= 1000; j++){
					m.addPlusPlus();
                    m.addMyAtomic();	
				}
			}, String.valueOf(i)).start();
		}
		
		while(Thread.activeCount() > 2){
			Thread.yield();
		}
		
		System.out.println(Thread.currentThread().getName() + "\tint\t\t" + m.num);
		System.out.println(Thread.currentThread().getName() + "\tAtomicInteger\t" + m.ai);
	}
}

class MyData{
	int num = 0;
	
	void addPlusPlus(){
		num++;
	}
    
    AtomicInteger ai = new AtomicInteger();
    void addMyAtomic(){
        ai.getAndIncrement();
    }
}

*为什么加了AtomicInteger就能解决不能保证原子性问题?

因为它里面有一个方法,getAndIncrement,它的意思就是带原子性的使一个值加1

这样其它线程就必须等待操作它的线程执行完之后,才可以对它进行操作,这样就能解决不保证原子性的问题

*AtomicInteger底层是什么

CAS

*有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一般分为以下三种

image-20210106102643195

处理器在进行重新排序时必须要考虑指令之间的数据依赖性

*写一个有序性demo

public class ResortSeqDemo{
    int a = 0;
    boolean flag = false;
    
    void method1() {
        a = 1;	//语句1
        flag = true;	//语句2
    }

    void method2() {
        if (flag) {
            a = a + 5;	//语句3
            System.out.println(a);
        }
    }	
}

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用过的变量能否保持一致性是无法确定的,结果无法预测

以上图的代码为例,如果开启多个线程操作这个资源类,语句1和语句2的执行顺序就无法得到保障,从而导致的结果就会完全不同。

总结

*你在哪些地方用过volatile

单例模式DCL+volatile

public class SingletonDemo{
	private static volatile SingletonDemo instance = null;//关键
	
	private SingletonDemo(){
		System.out.println(Thread.currentThread().getName() + "\tSingletonDemo()");
	}
	
	public static SingletonDemo getInstance(){
		if(instance == null){
			//Double Check Lock,双端检锁:在加锁前后都进行判断
			synchronized(SingletonDemo.class){
				if(instance == null){
					instance = new SingletonDemo();	
				}
			}
		}
		return instance;
	}
	
	public static void main(String[] args){
		for(int i = 0; i < 10; i++){
			new Thread(() -> {
				SingletonDemo.getInstance();
			},String.valueOf(i)).start();
		}
	}
}

*分析

DCL(双端检锁)不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排

原因在于某一个线程在执行第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化

instance = new SingletonDemo();可以分为以下步骤

memory = allocate();//1.分配对象内存空间
instance(memory);//2.初始化对象
instance = memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序执行的结果在单线程中并没有改变,因此这种重排优化是允许的

memory = allocate();//1.分配对象内存空间
instance = memory;//3.设置instance的指向刚分配的内存地址,此时instance!=null 但对象还没有初始化完.
instance(memory);//2.初始化对象

所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也会造成线程安全问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值