Java 多线程 - 7 - Java volatile 使用

1. volatile 概述

volatile 相当于 synchronized 的弱实现,也就是说 volatile 实现了类似 synchronized 的语义,却又没有锁机制.它确保对 volatile 字段的更新以可预见的方式告知其他的线程.

2. volatile 语义

(1)Java 存储模型不会对volatile指令的操作进行重排序:这个保证对 volatile 变量的操作时按照指令的出现顺序执行的. volatile 可以禁止进行指令重排.

指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的.指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性.

volatile 关键字禁止指令重排序有两层意思

  • 程序执行到 volatile 修饰变量的读操作或者写操作时,在其前面的操作肯定已经完成,且结果已经对后面的操作可见,在其后面的操作肯定还没有进行.

  • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行.

//线程1:  
context = loadContext();   //语句1  context初始化操作  
inited = true;             //语句2  
   
//线程2:  
while(!inited ){  
  sleep()  
}  
doSomethingwithconfig(context);

因为指令重排序,有可能语句2会在语句1之前执行,可能导致 context 还没被初始化,而线程 2 中就使用未初始化的 context 去进行操作,导致程序出错.

这里如果用 volatile 关键字对 inited 变量进行修饰,就不会出现这种问题了.

(2)volatile变 量不会被缓存在寄存器中(只有拥有线程可见)或者其他对 CPU 不可见的地方,每次总是从主存中读取 volatile 变量的结果.也就是说对于 volatile 变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量.也就是在 happens-before 法则中,对一个 valatile 变量的写操作后,其后的任何读操作理解可见此写操作的结果.

尽管 volatile 变量的特性不错,但是 volatile 并不能保证线程安全的,也就是说 volatile 字段的操作不是原子性的,volatile 变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

使用 volatile 关键字去修饰变量的时候,线程每次都会直接读取该变量并且不缓存它.这就确保了线程读取到的变量是同内存中是一致的

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
 
//线程2
stop = true;

这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法.但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了).

下面解释一下这段代码为何有可能导致无法中断线程.在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将 stop 变量的值拷贝一份放在自己的工作内存当中.

那么当线程2更改了 stop 变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对 stop 变量的更改,因此还会一直循环下去.

但是用 volatile 修饰之后就变得不一样了:

第一:使用 volatile 关键字会强制将修改的值立即写入主存.

第二:使用 volatile 关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量 stop 的缓存行无效(反映到硬件层的话,就是 CPU 的 L1 或者 L2 缓存中对应的缓存行无效).

第三:由于线程 1 的工作内存中缓存变量 stop 的缓存行无效,所以线程 1 再次读取变量 stop 的值时会去主存读取.

那么在线程2修改 stop 值时(当然这里包括 2 个操作,修改线程 2 工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量 stop 的缓存行无效,然后线程 1 读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值.

那么线程1读取到的就是最新的正确的值.

3. 应用 volatile 变量原则

(1)写入变量不依赖此变量的值,或者只有一个线程修改此变量

(2)变量的状态不需要与其它变量共同参与不变约束

(3)访问变量不需要加锁

4. volatile 原理和实现机制

下面这段话摘自《深入理解 Java 虚拟机》:

“观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,加入 volatile 关键字时,会多出一个 lock 前缀指令”

lock 前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3 个功能:

1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成.

2)它会强制将对缓存的修改操作立即写入主存.

3)如果是写操作,它会导致其他 CPU 中对应的缓存行无效.

5. synchronized 和 volatile 对比

  • volatile 变量是一种稍弱的同步机制在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此 volatile 变量是一种比 synchronized 关键字更轻量级的同步机制.
  • 从内存可见性的角度看,写入 volatile 变量相当于退出同步代码块,而读取 volatile 变量相当于进入同步代码块.
  • 在代码中如果过度依赖 volatile 变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解.仅当 volatile 变量能简化代码的实现以及对同步策略的验证时,才应该使用它.一般来说,用同步机制会更安全些.
  • 加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而 volatile 变量只能确保可见性,原因是声明为 volatile 的简单变量如果当前值与该变量以前的值相关,那么 volatile 关键字不起作用,也就是说如下的表达式都不是原子操作:count++、count = count+1.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老高的IT职业路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值