当前位置: 首页 > 后端技术 > Java

翻译《Java并发编程之volatile》

时间:2023-04-01 15:25:43 Java

作者:JacobJenkov原文:http://tutorials.jenkov.com/j...翻译:潘神莲个人网站如果大家有更好的翻译版本,欢迎??提issue或投稿~更新:2022-02-24Java的volatile关键字用于将Java变量标记为“存储在主内存中”。更准确地说,每次读取volatile变量都会从计算机的主内存中读取,而不是从CPU缓存中读取,而每次对volatile变量的写入都会写入主内存,而不仅仅是写入CPU缓存。其实从Java5开始,volatile关键字就不仅仅用来保证volatile变量读写主存了。我将在下面解释这一点。Javavolatile教程视频如果您喜欢视频,我这里有这个Javavolatile教程的视频版本:Javavolatile教程视频变量可见性问题Java的volatile关键字保证共享变量在多线程中的“可见性”。这听起来可能有点抽象,所以让我详细说明一下。在多线程应用程序中,如果多个线程对同一个未声明为volatile关键字的变量进行操作,出于性能考虑,每个线程都可以在处理变量的同时将变量从主存复制到CPU缓存中。如果您的计算机有多个CPU,每个线程可能运行在不同的CPU上。这意味着每个线程都可以复制不同CPU的CPU缓存中的变量。这里解释一下:对于没有使用volatile关键字声明的变量,无法保证Java虚拟机(JVM)何时会将数据从主内存读取到CPU缓存,或将数据从CPU缓存写入主内存。这可能会导致几个问题,我将在以下部分中对此进行解释。想象这样一个场景,多个线程访问一个包含计数器变量的共享对象,声明如下:publicclassSharedObject{publicintcounter=0;}假设只有线程1会增加计数器变量的值,但是线程1和线程2会不时读取这个计数器变量。如果计数器变量没有声明volatile关键字,则无法保证计数器变量的值何时从CPU缓存写回主存。这意味着每个CPU缓存上的计数器变量的值可能与主存中变量的值不一致。这种情况是这样的:一个线程的写操作还没有写回主存(每个线程都有一个本地缓存,也就是CPU缓存,一般写成功会从cpu缓存中flush到主存中)主存),其他线程看不到它。变量的最新值,这就是“可见性”问题,即一个线程的更新对其他线程是不可见的。Java的volatile可见性保证Java的volatile关键字就是为了解决变量可见性的问题。通过在计数器变量上声明volatile关键字,所有写入该变量的线程将立即同步到主存,所有读取该变量的线程将直接从主存读取。下面是counter变量声明的关键字volatile的使用:publicclassSharedObject{publicvolatileintcounter=0;}所以用volatile关键字声明的变量保证了其他线程写入该变量的可见性。在上面给出的场景中,一个线程(T1)修改计数器变量,另一个线程(T2)读取计数器变量(但不修改它)。在这种场景下,如果为计数器(counter)变量word声明了volatilekey,则可以保证对计数器(counter)变量的写入对线程(T2)可见。但是如果thread(T1)和thread(T2)都修改了counter变量,那么为counter变量声明volatile关键字并不能保证可见性,后面会讲到。volatile全局可见性保证事实上,Java的volatile关键字可见性保证超过了volatile变量本身的可见性。可见性保证如下:如果线程A写入一个volatile变量,而线程B随后读取同一个volatile变量,那么在线程A写入该volatile变量之前,所有变量的可见性对线程A可见,并且对线程B读取volatile变量后线程B。如果线程A读取一个volatile变量,则在读取该volatile变量时,线程A可见的所有变量也会从主内存中重新读取。让我用一个代码示例来说明:publicclassMyClass{privateintyears;privateintmonthsprivatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years=years;this.months=月数;这天=天;}}udpate()方法写入三个变量,其中只有变量days被声明为volatile。用volatile关键字声明的变量在写入时直接从本地线程缓存刷新到主存。volatile的全局可见性保证意味着当一个值被写入days时,所有对当前写入线程可见的变量也会被写入主存。这意味着当一个值被写入days变量时,year和months变量也被写入主内存。在读取年、月、日的值时,可以这样做:publicclassMyClass{privateintyears;privateintmonthsprivatevolatileintdays;publicinttotalDays(){inttotal=this.days;总计+=月*30;总计+=年*365;返回总计;}publicvoidupdate(intyears,intmonths,intdays){this.years=years;this.months=月数;这天=天;}}注意,totalDays()方法会先将days变量的值读入total变量。程序在读取days变量时,还会从主存中读取month变量和years变量的值。因此,可以保证通过上面的读取顺序读取到days、months、years这三个变量的最新值。指令重排序的挑战为了提高性能,一般允许JVM和CPU在保持程序语义不变的情况下,对程序中的指令进行重排序。例如:inta=1;整数b=2;一个++;b++;这些指令可以重新排序为以下顺序而不会丢失程序的语义:inta=1;一个++;整数b=2;b++;但是,当其中一个变量是使用volatile关键字声明的变量时,指令重排会带来一些挑战。让我们看看上一教程中的MyClass类示例:publicclassMyClass{privateintyears;privateintmonthsprivatevolatileintdays;publicvoidupdate(intyears,intmonths,intdays){this.years=years;这。月=月;这天=天;}}一旦update()方法向days变量写入值,写入years和months变量的最新值也被写入主存。但是,如果Java虚拟机重新排列指令,例如:publicvoidupdate(intyears,intmonths,intdays){this.days=days;this.months=月数;this.years=years;}当修改days变量时,months和years变量的值仍然会写入主存,但是这个节点出现在新值写入months之前和年份变量。所以months变量和years变量的最新值不可能对其他线程正确可见。这种指令的重新排列导致语义的改变。Java针对这个问题提供了解决方案,我们往下看。JavavolatileHappens-Before规则为了应对指令重排序的挑战,除了可见性保证之外,Java的volatile关键字还提供了Happens-Before规则。Happens-Before规则保证如果其他变量的读写操作原本发生在volatile变量的写操作之前,那么其他变量的读写指令不能在volatile变量的写指令之后重新排序;在volatile变量的写操作之前,发生其他变量的读写,Happens-Beforeofwriteofvolatilevariables。注意:比如在volatile变量写入之后的其他变量读写,在volatile变量写入之前,可能仍然会重新排序。只是不能反过来,允许后面的读写重排到前面,但不允许前面的读写重排到后面。如果其他变量的读写操作本来是发生在volatile变量的读操作之后,那么其他变量的读写指令不能重新排序在volatile变量的读指令之前;注意:比如其他变量在volatile变量读取之前读取,可能在读取一个volatile变量之后重新排列。只是不能倒过来,前面的reads允许重排到后面,后面的reads不允许重排到前面。上面的Happens-Before规则确保强制执行volatile关键字的可见性保证。仅声明volatile不足以保证线程安全即使volatile关键字保证volatile变量直接从主内存读取,并且对volatile变量的所有写入都直接写入主内存,但在某些情况下仅声明变量volatile是不够的保证线程安全。在前面解释的情况下,只有线程1写入共享计数器变量,将计数器变量声明为volatile足以确保线程2始终看到最新写入的值。事实上,如果写入变量的新值不依赖于先前的值,多个线程可以同时写入一个易失性共享变量并且仍然将正确的值存储在主内存中。也就是说,如果一个线程只写一个volatile共享变量,它不需要先读取这个变量的值,再计算下一个值。一旦线程需要先读取volatile变量的值,然后根据volatile共享变量的值生成新的值,volatile变量就不再足以保证正确的可见性。读取volatile变量和写入新值之间的短时间会造成资源竞争。有多个线程同时读取volatile变量,得到相同的值,都把新值赋给变量,然后把值写回去。在主内存中,从而覆盖彼此的值。当多个线程自增同一个计数器(counter)变量时,volatile变量不足以保证线程安全。下一节将更详细地解释这种情况:想象一下,如果线程1将一个值为0的共享计数器变量读入其CPU缓存,将其递增为1,并且尚未将更改后的值写回主内存。同时,线程2也可以从主存中读取同一个计数器变量,这里变量的值还是0,存入自己的CPU缓存中。然后,线程2也可以将计数器递增到1,而无需将其写回主内存。下图说明了这种情况:线程1和线程2现在几乎不同步。共享计数器(counter)变量的实际值应该是2,但每个线程在其CPU缓存中的值为1,而在主存中该值仍为0。真是一团糟!即使线程最终将其共享计数器变量的值写回主内存,该值也将是错误的。volatile线程何时安全?正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用volatile关键字不足以确保线程安全。一般这种情况下,需要使用synchronized来保证变量的读写是原子的。读取或写入volatile变量不会阻止其他线程读取或写入。为此,您必须在关键部分周围使用synchronized关键字。作为同步块的替代方案,您可以选择使用java.util.concurrent包中的原子数据类型。例如,AtomicLong或AtomicReference或其中之一。如果只有一个线程读取和写入一个volatile变量的值,而其他线程只读取该变量,那么读取线程可以保证看到写入该volatile变量的最新值。如果不使变量变易失,则无法保证。volatile关键字保证适用于32位和64位。volatile的性能考虑读写volatile变量会直接从主存中读写,比从CPUcache读写需要更多的开销,但是访问volatile变量可以防止指令重排序,这是一种正常的性能提升技术。因此,除非你真的需要强制变量的可见性,否则减少使用volatile变量。(本文完)原文:http://tutorials.jenkov.com/j...翻译:潘神莲个人网站如果大家有更好的翻译版本,欢迎??提issue或投稿~

猜你喜欢