• Java——volatile关键字详解


    volatile介绍

    volatile概述

    1. volatile是比synchronized关键字更轻量级同步机制,访问volatile变量时会执行加锁操作,因此会使执行线程阻塞
    2. volatile保证可见性禁止指令重排序,底层是通过“内存屏障”来实现,但不保证原子性
    3. 写入volatile变量相当于退出同步代码块读取volatile变量相当于进入同步代码块

    volatile的使用场景

    1. 对变量的入操作不依赖变量的当前值,或能确保只有单个线程更新变量的值;
    2. 该变量不会与其他状态变量一起纳入不变性条件中;(该变量没有包含在其他变量的不变式中)
    3. 在访问变量时不需要加锁

    使用案例

    1. 状态标记量:根据状态标记,终止线程。即volatile可以在检查某个状态标记以判断是否退出循环的场景中使用。
    2. 单例模式中的double check:volatile修饰instance是因为instance = new Singleton()这行代码不是原子性操作,给instance分配内存,调用Singleton的构造函数初始化成员变量,将instance对象指向分配的内存空间。

    JMM对volatile变量定义的特殊规则:

    1. 当前线程每次使用变量前都必须先从主内存刷新最新的值,用于保证能看到其他线程对变量的修改。(read、load、use连续执行,从内存到工作内存。)
    2. 当前每次修改变量后都必须立刻同步到主内存中,用于保证其他线程可以看到自己对线程的修改。(assign、store、write连续执行,从工作内存到内存。)
    3. volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。

    可见性 && 禁止指令重排序优化原理

    (通过底层的lock指令来实现)
      volatile可以保证线程可见性并且提供一定的有序性,但是无法保证原子性,在JVM底层volatile是采用“内存屏障”来实现。两层语义:保证可见性,不保证原子性禁止指令重排序

    可见性

      保证volatile变量对所有线程的可见性。可见性指当一条线程修改该变量值,新值对于其他线程来说是立即得知的。普通变量不可以,普通变量的值在线程间传递均需要通过主内存来完成,如线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完之后从主内存进行读取操作,新变量值才对线程B可见。
      volatile底层通过lock前缀,作用使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于对Cache中的变量进行了JMM的“store和write”操作,本线程使其他线程的该变量的缓存行无效,当其他线程需要读该变量时发现该变量缓存行无效,就从主存中重新加载数据,所以保证了数据的可见性
      写入volatile变量相当于退出同步代码块读取volatile变量相当于进入同步代码块。加锁机制既可以确保可见性又可以确保原子性,但是volatile变量只保证可见性,不保证原子性

    禁止指令重排序优化

      当变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因为不会将该变量上的操作与其他内存操作一起重排序,volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方。
      volatile修饰的变量会在汇编代码中多执行一个lock操作,相当于一个内存屏障(Memory Barrier,指重排序时不能把后面的指令重排序到内存屏障之前的位置)。
      只有一个CPU访问内存时并不需要内存屏障;但若有多个CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性。lock指令把修改同步到内存时,意味着所有之前的操作都已经执行完毕,这样就可以形成一种给人以指令重排序无法越过内存屏障的感觉。
    volatile禁止指令重排序的原则:
    在这里插入图片描述

    1. 第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
    2. 第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
    3. 第一个操作是volatile写第二个操作是volatile读时,不能重排序

    Q&A

    as-if-serial语义允许对存在控制依赖的操作做重排序的原因是什么?

      在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

    happen-before先行发生原则是哪些?

    1. 顺序执行代码规则:同一个线程中的,前面的操作 happen-before 后续的操作;
    2. 加解锁规则:监视器上的解锁操作 happen-before 其后续的加锁操作;
    3. volatile规则:对volatile变量的操作 happen-before 后续的操作;
    4. 线程启动规则:线程的start()方法 happen-before 该线程所有的后续操作;
    5. 线程终止规则:线程所有的操作 happen-before 其他线程在该线程上调用 join()返回成功后的操作;
    6. 线程中断规则:对线程interrupt()方法的调用 happen-before 被中断的代码检测到中断事件的发生,可通过Thread.interrupted()方法检测到是否有中断发生。
    7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen-before finalize()方法的开始。
    8. 传递性:如果a happen-before b,b happen-before c,则a happen-before c。

    加锁和volatile的区别是什么?

    相同点:
      写入volatile变量相当于退出同步代码块读取volatile变量相当于进入同步代码块
    不同点:
      加锁机制既可以确保可见性又可以确保原子性;但是volatile变量只保证可见性不保证原子性

    普通变量和volatile变量的区别是什么?

      volatile的特殊规则保证新值能立即同步主内存中,每次使用前立即能够从主内存刷新。所以volatile可以保证多线程操作时变量的可见性,而普通变量不保证

    1. volatile修饰变量能保证“可见性”:当一条线程修改了这个变量的值,新值对于其他线程是立即得知的。
    2. 普通变量不能够保证“可见性”:普通变量的值在线程间的传递均需要主内存来完成,线程A修改一个普通变量的值,先加锁从主内存读取,然后向主内存回写(lock、read、load、use、assign、store、write、unlock)。另外一条线程B在线程A回写完成后(解锁),再从主内存进行读取最新值(lock、read、load、use、unlock)操作,新变量值才会对线程B可见。

    原子性的语义是什么?

    volatile不保证原子性,但是讲一下原子性的语义。
    1)原子性定义:
    即一个操作或者多个操作要么全部执行且执行的过程不会被任何因素打断要么都不执行
    2)示例:

    i = 2;          
    i = j ;           
    i++;           
    i = j + 2;
    

    在Java中,只保证基本数据类型的变量和赋值操作是原子性操作(如果是在32位的JDK环境下,对于long和double64位数据读取不是原子操作,会分为高低两次32位操作。)单线程中可以将整个步骤视为原子性的,但是多线程需要通过synchronized和锁来保证原子性。
    i = 2操作是原子的,只涉及到将基本数据类型2赋值给i;
    i = j,是两个操作,先取j值,然后将j的值赋给i,非原子操作
    i++,是三个操作,先取i值,然后将i值自增,最后将自增的结果赋给i,是非原子操作
    i = j + 2,是三个操作,先取j值,然后j+2,最后将j+2的结果赋给i,是非原子操作

    JDK1.5之前Java无法安全使用DCL(双锁检测)来实现单例模式的原因?

      volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复,之前volatile变量前后的代码仍然存在重排序问题。
    1)DCL单例模式

    public class Singleton {
    
    	// volatile修饰变量
    	private volatile static Singleton instance;
    	
    	private Singleton(){}
    	
    	// 单例方法
    	public static Singleton getInstance(){
    		if(instance == null){
    			// 加锁synchronized代码块
    			synchronized (Singleton.class){
    				if(instance == null){
    					instance = new Singleton();
    				}
    			}
    		}
    		return instance;
    	}
    	public static void main(String[] args) {
    		Singleton.getInstance();
    	}    
    }
    

    2)使用静态内部类来实现更安全的机制

    /* 静态内部类 */
    public class SingletonInner { 
    
        // 静态内部类
    	private static class Holder { 
    		private static SingletonInner singleton = new SingletonInner();
    	 } 
    	 
    	private SingletonInner(){} 
    	
    	public static SingletonInner getSingleton(){ 
    		return Holder.singleton; 
    	}
     }
    
  • 相关阅读:
    撤回本地的提交
    antd Table每列样式修改
    大数组拼树
    滑动加载
    数组合并去除重复内容
    获取前一周期日期
    js 对象根据value获取对应的key
    less git上传问题处理
    5G
    Linux怎么安装node.js
  • 原文地址:https://www.cnblogs.com/Andya/p/12716090.html
Copyright © 2020-2023  润新知