java关键字volatile用法详解

本文详细介绍了Java关键字volatile的用法,包括其在多线程环境下保证变量可见性和禁止指令重排序的作用。通过对CPU缓存和内存模型的分析,解释了volatile如何解决多核CPU中的缓存不一致问题。此外,文章还讨论了volatile的可见性与有序性,并通过实例说明了volatile无法保证原子性,指出在需要原子性操作的场景中,应结合synchronized或Lock使用。
摘要由CSDN通过智能技术生成

volatile关键字想必大家都不陌生,在java 5之前有着挺大的争议,在java 5之后才逐渐被大家接受,同时作为java的关键字之一,其作用自然是不可小觑的,要知道它是java.util.concurrent包的核心,没有volatile就没有这么多的并发类给我们使用。

一、定义:表明两个或者多个变量必须同步地发生变化。(源自百度百科)

二、作用:volatile关键字的作用是保证变量在多线程之间的可见性,同时禁止进行指令重排序,要想真正了解volatile关键字的实现原理,有必要对java的内存模型和cpu的缓存进行一定的了解,所以在文章的一开始,首先介绍一些关于这两方面的知识。

1.java的内存模型和cpu的缓存

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,所以导致CPU可能会花费很长时间等待数据到来或把数据写入内存,因此cpu通常会将所需的数据存到缓存中,(缓存分为一级缓存,二级缓存,三级缓存,每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。)读取数据时就可以直接从缓存中读取,不必要每次都要去内存中读取,而缓存中读取数据的速度要远远高于内存,在单核中,这些不存在太大的问题,但是如果到了多核中,就会产生缓存不一致的问题,什么叫缓存不一致呢?

i = i + 1;

当我们执行上面代码时,会从内存中读取i的值,将其存到缓存中,最后执行完毕后再将缓存中的值刷新到内存中,整个流程如果放在单线程中执行,那不会有什么问题,但是若是放在多线程中来执行,那就不一样呢,假设有两个线程来执行上面代码,i的初始值都是0,线程1和线程2分别从内存中读取i的值0,将其存进各自的缓存中,等到线程1执行代码后,缓存中i的值变成1,然后再将缓存中i=1刷新到内存中,此时轮到线程2执行上述代码,和线程1一样,先从缓存中读取i的值,然后进行操作,最后更新到内存中去,可是此时i的值已经是1了,经过线程2后,i的值却根本没有什么变化,依旧是1,这就是一个Bug,那么,又该如何解决这个Bug呢?通常来说有两种解决方法:

1.1 通过在总线加LOCK#锁的方式

1.2 通过缓存一致性协议

在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。比如上面例子中 如果一个线程在执行 i = i +1,如果在执行这段代码的过程中,在总线上发出了LCOK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能从变量i所在的内存读取变量,然后进行相应的操作。这样就解决了缓存不一致的问题。

但是上面的方式会有一个问题,由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。

所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

2.在谈及volatile关键字的作用时,提到了两个概念,一个是可见性,一个是指令重排序,有必要对这两个概念进行单独的说明。

2.1 可见性:是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

复制代码

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

复制代码

假若执行线程1的是CPU1,执行线程2的是CPU2。当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10。这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。

对于可见性,Java提供了volatile关键字来保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

2.2 指令重排序:是指处理器为了提高程序运行效率,可能会对代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致ÿ

  • 21
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值