百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

天天在用volatile,你知道它的底层原理吗?

liebian365 2025-01-04 21:18 25 浏览 0 评论

前言

对于从事java开发工作的朋友来说,在工作中可能会经常接触volatile关键字。即使有些朋友没有直接使用volatile关键字,但是如果使用过:ConcurrentHashMap、AtomicInteger、FutureTask、ThreadPoolExecutor等功能,它们的底层都使用了volatile关键字,你就不想了解一下它们为什么要使用volatile关键字,它的底层原理是什么?

从双重检查锁开始

面试时被要求写个单例模式的代码,很多朋友可能写的是双重检查锁。代码如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {

    }

    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

有些朋友看到这里觉得有点熟悉,平时可能就是这个写的。

但是,我要告诉你的是,这个代码有问题,它在有些时候不是单例的。为什么会出现问题呢?

答案,在后面揭晓。

JMM(java内存模型)

在介绍volatile底层原理之前,让我们先看看什么是JMM(即java内存模型)。


java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝。前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

java内存模型会带来三个问题:

1.可见性问题

线程A和线程B同时操作共享数据C,线程A修改的结果,线程B是不知道的,即不可见的

2.竞争问题

刚开始数据C的值为1,线程A和线程B同时执行加1操作,正常情况下数据C应该为3,但是在并发的情况下,数据C却还是2

3.重排序问题

JVM为了优化指令的执行效率,会对一下代码指令进行重排序。

那么如何解决问题呢?


volatile的底层原理

java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类 型的处理器重排序,从而让程序按我们预想的流程去执行。

1、保证特定操作的执行顺序。

2、影响某些数据(或则是某条指令的执行结果)的内存可见性。

java的内存屏障指令如下:

对于volatile的写操作,在其前后分别加上 StoreStore 和 StoreLoad指令

对于volatile的读操作,在其后加上 LoadLoad 和 LoadStore指令

由上图可以看到,内存屏障是可以保证volatile变量前后读写顺序的。

此外,对volatile变量写操作时,使用store指令会强制线程刷新数据到主内存,读操作使用load指令会强制从主内存读取变量值。


再看看这个例子:

public class DataTest {
  
    private volatile int count = 0;

    public int getCount() {
       return count;
    }  

    public void setCount(int count) {
       this.count = count;
    }   
  
    public void incr() {
       count++;
    }  
}  

上面列子中的getCount和setCount方法这种单操作是可以保证原子性的,但是像incr方法无法保证原子性。

由此可见,volatile关键字可以解决可见性 和 重排序问题。但是不能解决竞争问题,无法保证操作的原子性,解决竞争问题需要加锁,或者使用cas等无锁技术。


再看双重检查锁问题

从上面可以看出JMM会有重排序问题,之前双重检查锁为什么有问题呢?

public static SimpleSingleton4 getInstance() {
  if (INSTANCE == null) {
     synchronized (SimpleSingleton4.class) {
        if (INSTANCE == null) {

          //1.分配内存空间
          //2.初始化引用
          //3.将实际的内存地址赋值给当前引用
          INSTANCE = new SimpleSingleton4();
        }
    }
  }
  return INSTANCE;
}

从代码中的注释可以看出,INSTANCE = new SimpleSingleton4();这一行代码其实经历了三个过程:

1.分配内存空间

2.初始化引用

3.将实际的内存地址赋值给当前引用

正常情况下是按照1、2、3的顺序执行的,但是指令重排之后也不排除按照1、3、2的顺序执行的可能性,如果按照1、3、2的顺序。

上面错误双重检查锁定的示例代码中,如果线程 1 获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程1 执行到 t3 时刻,线程 2 刚好进入,由于此时对象已经不为 Null,所以线程 2 可以自由访问该对象。然后该对象还未初始化,所以线程 2 访问时将会发生异常。

解决这个问题,可以把INSTANCE定义成volatile的。

private volatile static SimpleSingleton4 INSTANCE;

其实,创建单例的方法有很多,最好的还是静态内部类。

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}


总结

volatile的底层是通过:store,load等内存屏障命令,解决JMM的可见性和重排序问题的。但是它无法解决竞争问题,要解决竞争问题需要加锁,或使用cas等无锁技术。单例模式不建议使用双重检查锁,推荐使用静态内部类的方式创建。


彩蛋

使用volatile保证线程间的可见性和重排序问题,相对于synchronized等加锁机制更轻量级,但是对性能还是有一定的消耗,如何优化性能呢?

可以参考spring中DefaultNamespaceHandlerResolver类的getHandlerMappings方法

@Nullable
private volatile Map<String, Object> handlerMappings;

.......

private Map<String, Object> getHandlerMappings() {
		Map<String, Object> handlerMappings = this.handlerMappings;
		if (handlerMappings == null) {
			synchronized (this) {
				handlerMappings = this.handlerMappings;
				if (handlerMappings == null) {
					if (logger.isDebugEnabled()) {
						logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
					}
					try {
						Properties mappings =
								PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
						if (logger.isDebugEnabled()) {
							logger.debug("Loaded NamespaceHandler mappings: " + mappings);
						}
						handlerMappings = new ConcurrentHashMap<>(mappings.size());
						CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
						this.handlerMappings = handlerMappings;
					}
					catch (IOException ex) {
						throw new IllegalStateException(
								"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
					}
				}
			}
		}
		return handlerMappings;
}

该方法就使用了双重检查锁,可以看到方法内部使用局部变量,首先将实例变量值赋值给该局部变量,然后再进行判断。最后内容先写入局部变量,然后再将局部变量赋值给实例变量。使用局部变量相对于不使用局部变量,可以提高性能。主要是由于 volatile 变量创建对象时需要禁止指令重排序,这就需要一些额外的操作。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下:苏三说技术,或者点赞,转发一下,坚持原创不易,您的支持是我前进最大的动力,谢谢。

相关推荐

4万多吨豪华游轮遇险 竟是因为这个原因……

(观察者网讯)4.7万吨豪华游轮搁浅,竟是因为油量太低?据观察者网此前报道,挪威游轮“维京天空”号上周六(23日)在挪威近海发生引擎故障搁浅。船上载有1300多人,其中28人受伤住院。经过数天的调...

“菜鸟黑客”必用兵器之“渗透测试篇二”

"菜鸟黑客"必用兵器之"渗透测试篇二"上篇文章主要针对伙伴们对"渗透测试"应该如何学习?"渗透测试"的基本流程?本篇文章继续上次的分享,接着介绍一下黑客们常用的渗透测试工具有哪些?以及用实验环境让大家...

科幻春晚丨《震动羽翼说“Hello”》两万年星间飞行,探测器对地球的最终告白

作者|藤井太洋译者|祝力新【编者按】2021年科幻春晚的最后一篇小说,来自大家喜爱的日本科幻作家藤井太洋。小说将视角放在一颗太空探测器上,延续了他一贯的浪漫风格。...

麦子陪你做作业(二):KEGG通路数据库的正确打开姿势

作者:麦子KEGG是通路数据库中最庞大的,涵盖基因组网络信息,主要注释基因的功能和调控关系。当我们选到了合适的候选分子,单变量研究也已做完,接着研究机制的时便可使用到它。你需要了解你的分子目前已有哪些...

知存科技王绍迪:突破存储墙瓶颈,详解存算一体架构优势

智东西(公众号:zhidxcom)编辑|韦世玮智东西6月5日消息,近日,在落幕不久的GTIC2021嵌入式AI创新峰会上,知存科技CEO王绍迪博士以《存算一体AI芯片:AIoT设备的算力新选择》...

每日新闻播报(September 14)_每日新闻播报英文

AnOscarstatuestandscoveredwithplasticduringpreparationsleadinguptothe87thAcademyAward...

香港新巴城巴开放实时到站数据 供科技界研发使用

中新网3月22日电据香港《明报》报道,香港特区政府致力推动智慧城市,鼓励公私营机构开放数据,以便科技界研发使用。香港运输署21日与新巴及城巴(两巴)公司签署谅解备忘录,两巴将于2019年第3季度,开...

5款不容错过的APP: Red Bull Alert,Flipagram,WifiMapper

本周有不少非常出色的app推出,鸵鸟电台做了一个小合集。亮相本周榜单的有WifiMapper's安卓版的app,其中包含了RedBull的一款新型闹钟,还有一款可爱的怪物主题益智游戏。一起来看看我...

Qt动画效果展示_qt显示图片

今天在这篇博文中,主要实践Qt动画,做一个实例来讲解Qt动画使用,其界面如下图所示(由于没有录制为gif动画图片,所以请各位下载查看效果):该程序使用应用程序单窗口,主窗口继承于QMainWindow...

如何从0到1设计实现一门自己的脚本语言

作者:dong...

三年级语文上册 仿写句子 需要的直接下载打印吧

描写秋天的好句好段1.秋天来了,山野变成了美丽的图画。苹果露出红红的脸庞,梨树挂起金黄的灯笼,高粱举起了燃烧的火把。大雁在天空一会儿写“人”字,一会儿写“一”字。2.花园里,菊花争奇斗艳,红的似火,粉...

C++|那些一看就很简洁、优雅、经典的小代码段

目录0等概率随机洗牌:1大小写转换2字符串复制...

二年级上册语文必考句子仿写,家长打印,孩子照着练

二年级上册语文必考句子仿写,家长打印,孩子照着练。具体如下:...

一年级语文上 句子专项练习(可打印)

...

亲自上阵!C++ 大佬深度“剧透”:C++26 将如何在代码生成上对抗 Rust?

...

取消回复欢迎 发表评论: