13、Java并发性和多线程-Java Volatile关键字
以下内容转自http://tutorials.jenkov.com/java-concurrency/volatile.html(使用谷歌翻译):
Java volatile
关键字用于将Java变量标记为“存储在主存储器”中。更准确地说,这意味着,每个读取volatile变量将从计算机的主存储器中读取,而不是从CPU缓存中读取,并且每个写入volatile变量的写入将被写入主存储器,而不仅仅是写入CPU缓存。
实际上,由于Java 5的volatile
关键字保证不仅仅是volatile变量被写入和从主内存读取。我将在以下各节中解释一下。
Java volatile可见性保证
Java volatile
关键字可确保跨线程对变量的更改的可见性。这可能听起来有点抽象,所以让我详细说明一下。
出于性能原因,线程在非volatile变量上运行的多线程应用程序中,每个线程可能会将变量从主存储器复制到CPU高速缓存中。如果你的计算机包含多个CPU,则每个线程可能在不同的CPU上运行。这意味着每个线程都可以将变量复制到不同CPU的CPU缓存中。这在这里说明了:
对于非volatile变量,不能保证Java虚拟机(JVM)将数据从主存储器读取到CPU高速缓存中,或者将数据从CPU缓存写入主存储器。这可能会导致几个问题,我将在以下部分中解释。
想象一下,两个或多个线程可以访问共享对象的情况,该对象包含一个如下所示的计数器变量:
public class SharedObject { public int counter = 0; }
想象一下,只有线程1增加counter
变量,但线程1和线程2都可能对counter
不时读取变量。
如果counter
未声明变量,volatile则不能保证将counter
变量的值从CPU缓存写回主存储器。这意味着counter在
CPU缓存中的变量值可能与主内存不一样。这种情况在这里说明:
没有看到变量的最新值,因为还没有被另一个线程写回到主内存的线程的问题被称为“可见性”问题。一个线程的更新对其他线程是不可见的。
通过声明counter
变量,对变量的volatile
所有写入counter
将立即写回主内存。此外,counter
变量的所有读取将直接从主存储器读取。下面是如何volatile
在声明counter
变量的样子:
public class SharedObject { public volatile int counter = 0; }
因此, 声明一个volatile变量可以保证该变量的其他写入线程的可见性。
Java volatile事件保证
由于Java 5的volatile
关键字不仅仅保证了对变量的主内存的读取和写入。实际上,volatile
关键字保证:
如果线程A写入volatile变量和线程B随后读取相同的volatile变量,然后看到线程A的所有变量之前写volatile变量,也将是可见的线程B后,它已经读volatile变量。
volatile变量的读写指令不能被JVM重新排序(只要JVM从重新排序中没有检测到程序行为的变化,JVM可能会因为性能原因重新排序指令)。之前和之后的指令可以重新排序,但是这些指令不能混合写入或写入。无论读取还是写入volatile变量,任何指令都将保证在读取或写入后发生。
这些陈述需要更深入的解释。
当一个线程写入一个volatile变量时,不仅将volatile变量本身写入主存储器。在写入volatile变量之前,线程更改的所有其他变量也被刷新到主存储器。当一个线程读取一个volatile变量时,它也将从主存储器中读取与volatile变量一起刷新到主存储器的所有其他变量。
看这个例子:
Thread A:
sharedObject.nonVolatile = 123;
sharedObject.counter = sharedObject.counter + 1; Thread B:
int counter = sharedObject.counter;
int nonVolatile = sharedObject.nonVolatile;
由于线程A在写入volatile变量sharedObject.counter之前写入非volatile变量sharedObject.nonVolatile,所以当线程A写入sharedObject.counter(volatile变量)时,sharedObject.nonVolatile和sharedObject.counter都将写入主内存。
由于线程B从读取volatile的sharedObject.counter开始,所以sharedObject.counter和sharedObject.nonVolatile都从主内存读取到线程B使用的CPU高速缓存。当线程B读取sharedObject.nonVolatile时,它会看到值由线程A写。
开发人员可以使用这种扩展的可见性保证来优化线程之间变量的可见性。而不是声明每个变量volatile
,只需要声明一个或几个变量volatile
。这是一个简单的Exchanger
类的例子:
public class Exchanger { private Object object = null;
private volatile hasNewObject = false; public void put(Object newObject) {
while(hasNewObject) {
//wait - do not overwrite existing new object
}
object = newObject;
hasNewObject = true; //volatile write
} public Object take(){
while(!hasNewObject){ //volatile read
//wait - don't take old object (or null)
}
Object obj = object;
hasNewObject = false; //volatile write
return obj;
}
}
线程A可能会通过调用put()来不时地设置对象。线程B可能会通过调用take()来不时地获取对象。只要线程A调用put()并且只有线程B调用take(),这个Exchanger可以使用volatile变量(不使用同步块)来正常工作。
但是,如果JVM可以在不改变重新排序的指令的语义的情况下,JVM可以重新排序Java指令来优化性能。如果JVM切换的读取和顺序写入里面会发生什么,put()
和take()
?如果put()
真的执行如下:
while(hasNewObject) {
//wait - do not overwrite existing new object
}
hasNewObject = true; //volatile write
object = newObject;
注意,在实际设置新对象之前,对volatile变量hasNewObject的写入将被执行。对于JVM,这可能看起来完全有效。两个写入指令的值不依赖于彼此。
但是,重新排序指令执行会损害对象变量的可见性。首先,线程B可能会在线程A实际上为对象变量写入一个新值之前看到hasNewObject设置为true。第二,现在甚至不能保证写入对象的新值将被刷新回主内存(以下是线程A在某处写入volatile变量的情况)。
为了防止上述情况发生,volatile关键词带有“在保证之前发生”。在保证之前发生的事件保证了易失性变量的读写指令无法重新排序。之前和之后的指令可以重新排序,但是无法通过在其之前或之后发生的任何指令来重新排序易失性读/写指令。
看这个例子:
sharedObject.nonVolatile1 = 123;
sharedObject.nonVolatile2 = 456;
sharedObject.nonVolatile3 = 789; sharedObject.volatile = true; //a volatile variable int someValue1 = sharedObject.nonVolatile4;
int someValue2 = sharedObject.nonVolatile5;
int someValue3 = sharedObject.nonVolatile6;
只要所有这些指令都发生在易失性写入指令之前(它们必须在易失性写入指令之前都必须执行),JVM可能会重新排序前3个指令。
类似地,只要在所有这些指令之前发生易失性写入指令,JVM可以重新排序最后3条指令。在易失性写入指令之前,最后3条指令都不能重新排序。
这基本上是Java保护之前发生的volatile的意思。
volatile并不总是足够
即使volatile
关键字保证volatile
变量的所有读取都直接从主存储器读取,并且对volatile
变量的所有写入都直接写入主存储器,仍然存在声明volatile变量还不够的情况。
在前面所述的情况下,只有线程1写入共享counter
变量,声明counter
变量volatile
就足以确保线程2总是看到最新的写入值。
事实上,volatile
如果写入变量的新值不依赖于其先前的值,多线程甚至可能写入一个共享变量,并且仍然具有存储在主存储器中的正确值。换句话说,如果一个向共享volatile
变量写值的线程首先不需要读取它的值来找出它的下一个值。
一旦线程需要首先读取volatile
变量的值,并且基于该值为共享volatile
变量生成新值,则变量volatile
不再足以保证正确的可见性。在读取volatile
变量和写入新值之间的短时间间隙创建了一个竞争条件 ,其中多个线程可能读取相同的volatile
变量值,为变量生成一个新值,并将该值写回到主内存-覆盖彼此的值。
多线程增加相同计数器的情况正是这种情况,其中volatile
变量不够。以下部分将更详细地解释这一情况。
想象一下,如果线程1将counter值为0的共享变量读入其CPU缓存,将其递增到1,而不是将更改的值写入主存储器。线程2然后可以从counter
变量的值仍然为0的主存储器读取相同的变量到自己的CPU缓存中。线程2也可以将计数器递增到1,也不会将其写回主存储器。这种情况如下图所示:
线程1和线程2现在实际上不同步。共享counter
变量的实际值应为2,但每个线程的CPU缓存中的变量的值为1,主内存中的值仍为0。这是一个混乱!即使线程最终将共享counter
变量的值写回到主内存中,该值也将是错误的。
什么时候使用呢?
如前所述,如果两个线程都是共享变量的读取和写入,则使用volatile
关键字是不够的。 在这种情况下,你需要使用synchronized来保证变量的读写是原子的。读取或写入volatile变量不阻止线程读取或写入。为了实现这一点,你必须在关键部分周围使用synchronized关键字。
作为synchronized
块的替代,你还可以使用java.util.concurrent
包中发现的许多原子数据类型之一。例如,AtomicLong
或 AtomicReference
其他人之一。
如果只有一个线程读写volatile变量的值,并且其他线程只读取变量,则读取线程将被保证看到写入volatile变量的最新值。在不变量变动的情况下,这不能保证。
该volatile
关键字保证在32位和64变量上工作。
性能考虑波动
读写volatile变量会导致变量被读取或写入主存储器。读取和写入主内存比访问CPU缓存更昂贵。访问volatile变量还可以防止指令重新排序,这是正常的性能增强技术。因此,当你真正需要强制实现变量的可见性时,你应该只使用volatile变量。
13、Java并发性和多线程-Java Volatile关键字的更多相关文章
- 11、Java并发性和多线程-Java内存模型
以下内容转自http://ifeve.com/java-memory-model-6/: Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的.Java虚拟机是一个完整的计算机的一个模型, ...
- 21、Java并发性和多线程-Java中的锁
以下内容转自http://ifeve.com/locks/: 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的 ...
- 14、Java并发性和多线程-Java ThreadLocal
以下内容转自http://ifeve.com/java-theadlocal/: Java中的ThreadLocal类可以让你创建的变量只被同一个线程进行读和写操作.因此,尽管有两个线程同时执行一段相 ...
- 12、Java并发性和多线程-Java同步块
以下内容转自http://ifeve.com/synchronized-blocks/: Java 同步块(synchronized block)用来标记方法或者代码块是同步的.Java同步块用来避免 ...
- 22、Java并发性和多线程-Java中的读/写锁
以下内容转自http://ifeve.com/read-write-locks/: 相比Java中的锁(Locks in Java)里Lock实现,读写锁更复杂一些.假设你的程序中涉及到对一些共享资源 ...
- java 并发性和多线程 -- 读感 (一 线程的基本概念部分)
1.目录略览 线程的基本概念:介绍线程的优点,代价,并发编程的模型.如何创建运行java 线程. 线程间通讯的机制:竞态条件与临界区,线程安全和共享资源与不可变性.java内存模型 ...
- Java 并发和多线程(一) Java并发性和多线程介绍[转]
作者:Jakob Jenkov 译者:Simon-SZ 校对:方腾飞 http://tutorials.jenkov.com/java-concurrency/index.html 在过去单CPU时 ...
- Java并发性和多线程
Java并发性和多线程介绍 java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题, ...
- Java并发性和多线程介绍
java并发性和多线程介绍: 单个程序内运行多个线程,多任务并发运行 多线程优点: 高效运行,多组件并行.读->操作->写: 程序设计的简单性,遇到多问题,多开线程就好: 快速响应,异步式 ...
随机推荐
- LeetCode 要记得一些小trick
最近搞了几场编程比赛,面试题或者是LeetCode周赛.每次都不能做完,发现时间不够用. 看了别人的代码才知道,同样实现相同的功能,可能别人只需要用一个恰当的函数,就会比自己少些不少代码,争得了时间. ...
- Java 8 (11) 新的日期和时间API
在Java 1.0中,对日期和时间的支持只能依赖java.util.Date类.这个类只能以毫秒的精度表示时间.这个类还有很多糟糕的问题,比如年份的起始选择是1900年,月份的起始从0开始.这意味着你 ...
- Java 8 (2) 使用Lambda表达式
什么是Lambda? 可以把Lambda表达式理解为 简洁的表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表.函数主体.返回类型,可能还有一个可以抛出的异常列表. 使用Lambda可以让你更 ...
- dubbo与zookeeper学习中的问题
环境: spring5.1.5 dubbo 2.6.2 异常信息: java.lang.NoClassDefFoundError: org/apache/curator/RetryPolicy at ...
- poj1778 All Discs Considered
思路: 拓扑排序.贪心. 实现: #include <bits/stdc++.h> using namespace std; vector<]; int n1, n2; inline ...
- Appium Python API 汇总(中文版)
网络搜集而来,留着备用,方便自己也方便他人.感谢总结的人! 1.contexts contexts(self): Returns the contexts within the current ses ...
- 浏览器的两种模式quirks mode 和strict mode
关键字: javascript.quirks mode.strict mode 在看js代码时,有时会看到关于quirks mode(怪异模式)和strict mode(严格格式)的东西,一直也没深究 ...
- 使用vs2010打开vs2015的项目
本来在单位项目一直使用vs2010写,五一放假拿回家 ,用vs2015捣鼓了一下 当然向下兼容打开毫无问题,结果回来悲催了,用vs2010打不开了 ,打不开. 记得以前有个转换向导,可是这次没看见,一 ...
- 18SVN进行版本控制
SVN进行版本控制 SVN是Subversion的简称,是一个开放源代码的版本控制系统,相较于RCS.CVS,它采用了分支管理系统,它的设计目标就是取代CVS. SVN Website.
- SSH命令行传输文件到远程服务器
Ubuntu操作系统 SCP命令 使用方式如下: 1.上传本地文件到远程服务器 scp /var/www/test.php root@192.168.0.101:/var/www/ 把本机/var/w ...