Java 中的Volatile关键字

Volatile关键字:

Java的volatile关键字用于去标记一个java变量被存储在主内存的意思,更精确的说,对于volatile变量的每一次读,都是从计算机的主内存读取,不是从缓存读取,并且对volatile变量的每一次写会直接写到主内存,不仅仅是到cpu缓存;

事实上,自Java5以来,volatile关键字不仅仅保证volatile变量写到主内存/从主内存读取,原因会在以下环节解释:

变量可见性问题:

Java volatile关键字保证线程之间变量改变的可见性,这个可能听起来有点抽象,所以让我细化它。

在一个多线程的应用里,线程操作非volatile变量,因为性能的原因,每一个线程都会从主内存里复制变量进入它的CPU缓存,并且用那份复制变量进行工作,如果你的电脑不止一个CPU,每一个线程可能在不同的CPU上运行,那就意味着,每一个线程会复制变量进入不同的CPU缓存;

没有volatile变量的话,对于java虚拟机(JVM)什么时候会从主内存读取数据到CPU缓存,或者从CPU缓存写数据到主内存,没有任何保证,这可能导致几个问题,接下来会一一解释:

假设情形一:两个或者更多的线程都能访问一个共享对象,这个对象里面包含一个计数器(counter),如下:

public class SharedObject{
	public int counter = 0;
}

再假设,只有线程1增加计数器的值,线程1和线程2都会反复的读取计数器的值

如果计数器变量没有声明为vliatile,对于计数器的值什么时候会从CPU缓存写回主内存没有任何保证,这就是说,在CPU缓存里的计数器的值可能跟主内存里计数器变量的值不一样,这个情形演示如下:
在这里插入图片描述
线程没有看到变量最新的值的问题,是因为变量的值一直没有被另一个线程写回主内存,就叫可见性问题,一个线程的更新对另一个线程不可见。

Java volatile关键字的可见性保证

Java volatile关键字就是用来解释变量可见性问题的,通过标记计数器变量为volatile的,所有对volatile变量的写,会立即写到主内存,同样,对于计数器变量所有的读,都会从主内存里直接读取;

这是volatile变量修饰的计数器变量的样子:

public class ShareObject{
	public volatile int counter = 0;
}

声明一个变量的volatile的,保证了对那个变量的写的操作对其它变量的可见性。

在上边给定的情形中,线程1(T1)修改变量的值,线程2(T2)读取变量的值(永远不会修改变量),把计数器(counter)声明为volatile的,足够保证线程1(T1)写给计数器的值对于线程2(T2)的可见性了。

如果T1和T2都增加计数器(counter)的值,那么声明counter变量为volatile就不够。

Volatile的完全可见性保证:

事实上,Java volatile关键字的可见性保证超出了volatile的变量本身,可见性保证描述如下:

  • 如果线程A对volatile变量进行写操作,线程B随后读取相同的volatile变量,那么对volatile变量进行写操作之前,对线程A可见的所有变量,在线程B读取了volatile变量之后,也会对变量B可见。
  • 如果线程A读取一个volatile变量,在从主内存读取volatile变量时,然后对线程A可见的所有变量也会从主内存中读取,用代码例子展示如下:
public class MyClass{
	private int years;
	private int months;
	private volatile int days;
	
	public void update(int years,int months,int days){
		this.years = years;
		this.months = months;
		this.days = days;
	}
}

update()方法写三个变量,其中只有days是volatile修饰的,完全的volatile可见性保证是指,当一个值被写到days的时候,对当前线程可见的所有变量(years,months,days)都会被写进主内存,即使years跟months没有被volatile变量修饰,也会被写进主内存,当读取years,months跟days的值的时候,你可以这么做:

public class MyClass{
	private int years;
	private int months;
	private volatile int days;
	
	public int totalDays(){
		int total = this.days;
		total += months*30;
		total += years*365;
		return total;
	}
	
	public void update(int days,int months,int years){
		this.years = years;
		this.months = months;
		this.days = days;
	}
}

注意totalDays()方法读取days的值开始,当读取days的值的时候,months跟years的值也从主内存中读取过来了,因此用上面的顺序,你肯定能看到最新的days,months,years的值;

指令重拍的挑战

因为性能的原因,JVM跟CPU被允许去重拍程序里的指令,只要指令的语义保持一致即可,例如,下述指令:

int a = 1;
int b = 2;
a++;
b++;

这些指令可以被重排到如下序列,并且不会丢失程序的语义:

int a = 1;
a++;
int b = 2;
b++;

然而,当有一个变量是volatile变量时,指令重拍就遇到了挑战,我们继续看前边出现过的MyClass类

public class MyClass{
	private int years;
	private int months;
	private volatile int days;
	public void update(int years,int months,int days){
		this.years = years;
		this.months = months;
		this.days = days;
	}
}

一旦update()方法写一个值到days变量,新写到months跟years的值也会被写到主内存,但是,如果JVM重排序这些指令,像这样:

public void update(int years,int months,int days){
	this.days = days;
	this.months = months;
	this.years = years;
}

当days变量被修改的时候,months跟years的值仍然被写到内存里去了,但是这一次把days变量的值写到主内存这件事发生在给months跟years赋予新的值之前,months跟years的新值没有恰当的对其它线程可见,指令重排序的语义发生了变化。

对于这个问题,Java有一个解决方案。

Java volatile的Happens-before保证

为了解决指令重拍的挑战,除了可见性保证,java volatile关键字还给予了一个"Happens-Before"保证,Happens-Before保证确保:

1、对volatile变量进行写操作:

  • 如果对其他变量的读/写发生在对volatile变量的写(write)操作之,对其他变量的读/写操作不能被重排到对volatile变量的写(write) 操作之后
  • 如果对其他变量的读/写发生在对volatile变量的写(write)操作之,对其他变量的读/写操作可以被重排到对volatile变量的写(write) 操作之前

总结:对volatile变量进行写操作时,对后面变量的读/写操作可以被重排到前面来,对于前面变量的读写操作重排到后边去不行。

2、对volatile变量进行读操作:

  • 如果对其它变量的读/写最开始是发生在volatile变量读(read)操作之后,对其他变量的读/写操作不能被重排序到volatile变量读(read) 操作之前
  • 如果对其它变量的读/写最开始是发生在volatile变量读(read)操作之前,对其他变量的读/写操作可以被重排序到volatile变量读(read) 操作之后

总结:对volatile变量进行读操作时,对后面变量的读/写操作不能被重排到前面来,对于前面变量的读写操作可以重排到后边去。

上述的Happens-before保证确保volatile关键字的可见性保证被强制执行。

volatile不总是够用的,就算volatile变量保证对于一个volatile的读操作是直接从主内存中读取的,所有的写操作是直接写到主内存的,仍然有一些情形,在这些情况下,声明变量是volatile是不够的:

在前面解释过的情况下:只有线程1(T1)对共享计数器(counter)变量进行写操作 ,把计数器变量(counter)声明为volatile足够确保线程2(T2)看到counter的最新值了。

事实上,多个线程都可以对volatile变量进行写操作,而且仍然后正确的值存储在主内存里,如果变量的新值不依赖变量的旧值的话,换句话说,如果一个线程给volatile变量写入新值的时候,并不需要先去读取volatile变量旧值来计算新值。

只要线程需要先读取volatile变量的值,并且基于读取的值来计算新值的话,一个volatile变量不在能够确保正确的可见性了。

在从主内存读取volatile变量跟volatile变量的新值写入主内存之间的时间间隔里,有一个竞态条件(race condition):多个线程可能读取相同的volatile变量值,给变量产生一个新的值,并且在把新值写回主内存的时候覆盖其它线程写的值。

多个线程修改同一个counter计数器的情形就是volatile变量不够用的情况,以下部分会解释的更加详细。

假设线程1读取共享计数器counter进入它的CPU缓存,counter的值是0,把counter增加到1,并且没有把更改的值写回主内存,线程2然后从主内存读取相同的counter,counter仍然是0,进入它的CPU缓存,线程2也把counter的值增加到1,也不把值也回主内存,这个情况演示在下面这个图表里:
在这里插入图片描述
线程1和线程2现在显然失去同步了,共享变量counter的值应该是2才对,但是每一个线程对于他们CPU缓存里的值都是1,并且主内存中变量的值还是0,这就很糟糕了,就算线程最终把counter的值写回主内存,counter的值仍然是错的。

什么时候volatile够用了?

正如我前面提到的,如果两个线程同时对一个共享变量进行读写,使用volatile关键字还不够,在那种情况下,你需要使用synchronized关键字确保读写操作都是原子的,volatile变量的读写不阻塞其他线程,为了确保读写的原子性,你必须在重要区域使用synchronized关键字;

作为synchronized代码块的可选项,你可以使用java.util.current包里众多原子类中的任何一个,例如:LongAdder跟AtomicReference或者其他的。

在只有一个线程读和写volatile变量并且其他线程都只读volatile变量的情况下,读线程确保会看到写给volatile变量的最新的值,如果不标记变量为volatile就不会发生。

volatile关键字保证在32字节跟64字节变量上起作用。

volatile的性能考虑:参考文章

对于volatile变量的读写导致变量直接被写进主内存或者从主内存中读取出来,相比访问CPU缓存,对主内存的访问时更消耗时间的,访问volatile变量也阻止了指令重排这项性能增强技术,因此,你只应该在的确需要确保可见性的情况下才使用volatile关键字。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

你好像很好吃a

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

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

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

打赏作者

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

抵扣说明:

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

余额充值