Java Volatile Keyword

Java Volatile Keyword

Java volatile关键字用于将Java变量标记为“存储在主存储器中”。 更确切地说,这意味着,每次读取一个volatile变量都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且每次写入volatile变量都将写入主内存,而不仅仅是CPU缓存。

实际上,从Java 5开始,volatile关键字不仅仅保证将volatile变量写入主内存并从主内存中读取。 我将在以下部分解释。

Variable visibility Problems

Java volatile关键字可确保跨线程变量的可见性。 这可能听起来有点抽象,所以让我详细说明。

在线程操作非volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主存储器复制到CPU高速缓存中。 如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。 这意味着,每个线程可以将变量复制到不同CPU的CPU缓存中。 这在这里说明:

CPU Working Model

对于非volatile变量,无法保证Java虚拟机(JVM)何时将数据从主内存读取到CPU缓存中,或将数据从CPU缓存写入主内存。 这可能会导致一些问题,我将在以下部分中解释。

想象一下两个或多个线程可以访问共享对象的情况,该共享对象包含一个声明如下的计数器变量:

public class SharedObject {
	
	public int counter = 0;

}

假设如果只有线程1递增计数器变量,但线程1和线程2都可能不时读取计数器变量.

如果计数器变量未声明为volatile,则无法保证何时将计数器变量的值从CPU高速缓存写回主存储器。 这意味着CPU缓存中的计数器变量值可能与主存储器中的计数器变量值不同。 这种情况如下所示:

CPU Working Model

线程没有看到变量的最新值的原因,是因为它还没有被另一个线程写回主内存,这就被被称为“可见性”问题。 其他线程看不到一个线程对非volatile共享变量的更新。

The Java volatile Visibility Guarantee

Java volatile关键字旨在解决可变可见性问题。 通过声明计数器变量volatile,对计数器变量的所有写操作都将立即写回主存储器。 此外,计数器变量的所有读取都将直接从主存储器中读取。

以下是计数器变量的volatile声明:

public class SharedObject {
	
	public volatile int couter = 0;

}

因此,声明变量volatile可以保证对该变量的其他写入线程的可见性。

在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明计数器变量volatile足以保证对计数器变量的写入T2的可见性。

但是,如果T1和T2都在递增计数器变量,那么声明计数器变量volatile是不够的。 稍后会详细介绍。

Full volatile Visibility Guarantee

实际上,Java volatile的可见性保证超出了volatile变量本身。 能见度保证如下:

  1. 如果线程A写入一个volatile变量而线程B随后读取相同的volatile变量,那么在写入volatile变量之前,线程A可见的所有变量在读取volatile变量后也将对线程B可见。
  2. 如果线程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。

The full volatile visibility guarantee意味着,当一个值写入天数时,线程可见的所有变量也会写入主存储器。 这意味着,当一个值写入天数时,年和月的值也会写入主存。

在读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 years, int months, int days) {
		this.years = years;
		this.months = months;
		this.days = days;
	}
}

请注意,totalDays()方法首先将天数值读入总变量。 在读取天数值时,月份和年份的值也会被读入主存储器中。 因此,您可以使用上述读取顺序查看days,months和years的最新值。

Instruction Reordering Challenges

为了提高性能,一般允许JVM和CPU在保证程序语义不变的情况下对程序中指令进行重排序,比如:

int a = 1;
int b = 2;

a++;
b++;

这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:

int a = 1;
a++

int b = 1;
b++;

然而,当其中一个变量是volatile修饰时,对指令重新排序提出了挑战。 让我们看看这个Java 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,新写入的years和months也会写入主内存。 但是,如果Java VM重新排序指令,如下所示:

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

修改days变量时,仍会将months和years的值写入主内存,但这次是在将新值写入months和years之前。 因此,新值不能正确地对其他线程可见。 重新排序的指令的语义含义已经改变。

Java有一个解决这个问题的方法,我们将在下一节中看到。

The Java volatile Happens-Before Guarantee

为了解决指令重排序的挑战,除了可见性保证之外,Java volatile关键字还提供“happens-before”保证。 "happens-before"保证:

  1. 对其他变量的读写指令不能重排序到对一个volatile变量的写指令之后;
  2. 对其他变量的读写指令不能重排序到对一个volatile变量的读指令之前;

上述"happens-before"确保正在强制执行volatile关键字的可见性保证。

volatile is Not Always Enough

即使volatile关键字保证直接从主存储器读取volatile变量,并且所有对volatile变量的写入都直接写入主存储器,仍然存在声明变量volatile不足保证线程安全的情况。

在前面解释的情况中,只有线程1写入共享计数器变量,声明计数器变量volatile足以确保线程2始终看到最新的写入值。

一旦线程需要首先读取volatile变量的值,并且基于该值为共享volatile变量生成新值,volatile变量就不再足以保证正确的可见性。 读取volatile变量和写入新值之间的短时间间隔会产生竞态,其中多个线程可能读取volatile变量的相同值,为变量生成新值,并在写入值时 回到主内存 - 覆盖彼此的值。

多个线程递增相同计数器的情况正是这样一种情况,即volatile变量不够保证线程安全性。 以下部分更详细地解释了这种情况:

想象一下,如果线程1将值为0的共享计数器变量读入其CPU高速缓存,则将其递增为1并且还未将更改的值写回主存储器。 然后,线程2可以从主存储器读取相同的计数器变量,其中变量的值仍为0,进入其自己的CPU高速缓存。 然后,线程2也可以将计数器递增到1,也还未将其写回主存储器。 这种情况如下图所示:

CPU Working Model

线程1和线程2现在几乎不同步。 共享计数器变量的实际值应为2,但每个线程的CPU缓存中的变量值为1,而主存中的值仍为0.这是一个混乱! 即使线程最终将共享计数器变量的值写回主存储器,该值也将是错误的。

When is volatile Enough

正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用volatile关键字是不够的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子的。 读取或写入volatile变量不会阻塞其他线程读取或写入。 为此,您必须在关键部分周围使用synchronized关键字。

作为synchronized块的替代方法,您还可以使用java.util.concurrent包中的众多原子数据类型之一。 例如,AtomicLong或AtomicReference或其他之一。

如果只有一个线程读取和写入volatile变量的值,而其他线程只读取变量,那么读取线程将保证看到写入volatile变量的最新值。 如果不使变量变为volatile,则无法保证。

volatile关键字保证可以在32位和64位变量上运行。

Performance Considerations of volatile

从主存储器直接读入或写入比访问CPU低效的多, 而且volatile变量一定程度上阻止了指令重排序这一正常的性能增强技术. 因此,除非你确实需要强制实施变量可见性时,不然应尽量少用volatile修饰变量

翻译自: http://tutorials.jenkov.com/java-concurrency/volatile.html
原作者: Jakob Jenkov
Orz Orz Orz

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值