| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 深入理解并发编程中volatile关键字 -> 正文阅读 |
|
[Java知识库]深入理解并发编程中volatile关键字 |
volatile是java并发编程中的一种轻量级的同步机制,是java虚拟机提供的一种轻量级的同步机制,其主要作用有3个,即:保证可见性, 不保证原子性,禁止指令重排。
其主要访问过程如下图: 在了解了JMM模型后? 我们继续volatile 可见性的理解: 先看下面这段代码: 运行结果为: ?显然我们发现了此时程序一直是在运行的状态,并没有停止,显然此时程序一直停留在了上那段代码的while循环中一直没有出来。由此可知上面的那段代码中的Main主线程一直认为num的值是0,所以一直在哪里死循环着。但是有小伙伴就有疑问了,不是在上面的代码中我们开启了一个线程A并且将num的值修改为60了吗?按道理来说此时num的值应该就是60,程序不应该一直在那里死循环呀。那是因为当你开启了一个线程A修改了num的值得时候,这个时候线程A会首先修改自己工作内存中的num值,然后会将修改好的值同步回主内存,但是由于是并发执行的,你的main线程在程序启动的时候也将主内存中的值读取到了自己的工作内存,而此时主内存中的值还没有被修改成60,所以main线程最开始读取到的num值是0,所以即使是你A线程后面讲num的值修改成了60,main线程依然不知道,它知道的num值永远都是0,所以程序一直在那里死循环。
而此时我们只需要在num前面加上一volatile关键字,请看下面代码: 与上面那段代码有一点不同的是我们在num前面加了一个volatile关键字,我们看看程序运行的结果: 显然这个时候发现程序是运行结束了的,并且最后也输出了main线程获得的值为60。那这又是为什么呢?为什么你加了一个volatile关键字过后与上面的代码的运行结果就完全不一样了? 那是因为volatile一个很关键的特性即:保证可见性,也就是说,当你上面的A线程修改了num的值过后由于num前面使用了volatile关键字那么在底层会打开MESI也就是缓存一致性协议,那么线程A就会在修改num值过后会先将num的值写会主内存,然后再写会主内存的时候会通过总线也就是MESI缓存一致性协议,在通过总线的时候main线程就会通过嗅探机制感知到有线程会操作主内存中的数据,那么main线程就会先让自己工作内存中的数据失效,然后从新读取主内存中的数据,这样main线程就读取到了num的最新值,那么上面的程序也就不会处于死循环中了,这就是volatile的一个重要特性---->保证可见性。 2.聊完了volatile的保证可见性,下面我们来聊聊volatile 的另外一个特性--->不保证原子性: 首先看看下面代码: 上面代码我们做了一个很简单的操作那就是开启了20个线程,循环1000次对number进行++操作,按正常的逻辑来讲我们得到的number结果应该是10x2000=20000,但是结果真的是20000吗?我们不妨运行程序看看结果: ? ?这里我只调试了3次,3次的结果都是小于20000,但是不排除也会出现20000的情况,小伙伴没们可以下来自己多调试几次,这里我就不多调试了。 那么我们此时就会想为什么会出现小于20000的情况呢?而不是等于20000,这就是因为在多线程的环境下volatile也不会保证数据的原子性。下面我们换个图来理解一下为什么结果会小于20000: ? 线程1将工作内存中的number??? 值修改为1的时候,并且将修改好的值写回了主内存,此时住内存中的number值已经改为了1,但是当线程2在线程1正在写会主内存的同时,线程2也也将工作内存中的number值改为1,并且也立即写回主内存,此时就是线程1将修改好的值写回了主内存,而线程2 还没来得急感知到主内存中的值已经修改了,也同时将线程2修改好的值写回主内存那么这个时候就出问题了,明明是两个线程都修改了值此时的结果应该是2了,结果主内存中的值还是1那么这样下去最终的结果肯定是会小于20000的。
3.聊完了volatile的可见性,不保证原子性,下面我们来聊聊volatile的最后一个特性:禁止指令重排。 什么是指令重排?简单说就是代码在底层运行的时候有可能不会按照开发人员写的代码顺序运行,底层编译为了优化提高效率会对没有相互数据依赖的程序进行重排运行,不会按照开发人员写代码的顺序运行。 看 下面代码: 可以发现开发人员从上往下编写的这段代码,那么在底层编译执行 的时候,完全可以不按照这个顺序执行, 它可以先执行int x=5;int z=x+4;然后再执行其他的,但是它不能将int u=y+z;执行到int z=x+4;前面,因为他们之间存在数据依赖。 有时候指令重排也会出现问题的,为了防止指令重排我们可以加上volatile关键字,volatile关键字可以禁止指令重排,在底层volatile是通过内存屏障来防止指令重排的。 4.为什么在多线程的环境下单利模式需要加上volatile关键字?先看看下面代码: ? ? 这个是单线程下的单利模式,构造方法只调用了一起,并且输出结果都是true,可以发现没有任何问题,下面我们 看看多线程的情况下会出现什么结果? 显然根据输出结果可知,当我们在多线程的环境下上面的那种单利模式的写法已经不能够保证单利了,那么我们怎么保证在多线程的环境下依然还是单利的呢?两种方法1.加上synchronized关键字,确保在多线程的条件下是单利的。2.使用双重检查机制(DCL)来保证单利。具体看下面代码理解。 ?(1) ? 从输出的结果可以看出使用synchronized关键字确实是保证了单利,但是想一想这种方法有什么缺陷呢?其实大家都知道synchronized是一把重锁,它锁的是一整块代码块,一旦加上这把重所,那么所有的线程进入都要排队了,一整块代码块都被锁住了,这样是不是效率就变得很低了呀,于是一般情况下我们不使用这个方法,我们一般都是使用第二种也就是双重检测(DCL). (2) ? 采用双重检查从输出结果可以发现依然可以保证在多线程的环境下能够保证单利。 不知有没小伙伴发现在以上的单利模式中我一直没有使用volatile关键字,即使没有使用volatile关键字发现在多线程的环境下程序依然没有出错,依然能够正常的运行。但是需要注意的是volatile还有一个很重要的特性那就是禁止指令重牌,当上面程序在底层编译的时候会出现指令重排的现象,即使出错的概率非常非常低但是一旦出错就会很严重,所以我们应该避免出现指令重排这种情况发生。那么在单利模式中哪里出现指令重排会出现问题呀?------》就是在getInstance()方法中创建对象的时候可能会出现指令重排。不知小伙伴们是否了解创建对象的具体过程,在这里我就叙述这里需要使用的步骤,如果还需了解跟多,可以翻阅我其他文章有详细讲解对象的创建。在创建对象的时候底层会进行下面几个步骤:(1).为对象分配空间,(2)初始化对象,(2)引用指向对象。那么在底层编译的时候可能出现指令重排的现象即:由于(2)和(3)没有数据依赖,那么(2)和(3)完全有可能被重排,导致出现的问题就有点严重了即:instance这个对象它的地址不为空,但是他的内容为空了,这是一个非常严中的问题,为了避免这个问题我们必须加上volatile关键字避免指令重排现象发生。
所以完整的多线程环境下单利模式应该如下: ?
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 11:35:00- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |