返回信息流上回歪楼了。
原帖贴了个题目,类似下面的程序。问题是:“程序有没有可能输出y=2而x!=1的情况?如果可能,x可能会是什么值?如果不可能,为什么?”
public class HelloWorld {
public static volatile int x = 0;
public static volatile int y = 0;
static class Job1 implements Runnable {
public void run() {
x = 1;
y = 2;
}
}
static class Job2 implements Runnable {
public void run() {
int b = y;
int a = x;
if (b == 2 && a != 1) {
System.out.println("Surprise! y==2, but x!=1. x=" + a);
}
}
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(new Job1());
Thread t2 = new Thread(new Job2());
t1.start();
t2.start();
t1.join();
t2.join();
}
}
给不耐烦的人公布一下答案:“不可能。如果y==2,那么x一定是1。”如果不是,你可以向Java虚拟机的开发人员报告bug了。
为什么?
因为x和y用volatile修饰符修饰了。(实际上,只要y是volatile就够了)根据Java的内存模型(memory model),可以保证:“只要线程t2看到了t1给y写的新值2,那么同样也能看到t1之前写的x的值。”至于为什么,听我细细说。
====我是朴素的分割线====
volatile做了什么保证?
1. 对volatile成员变量的读和写都是原子的。确切地说,Java规定对volatile成员变量的读写都会读出/写入“一致”(consistent)的值。意思是不会读到没有写入过的值。比如,把一个64位整数分成高低两个32位数分别写,然后读到了写了一半的值,是不允许的。注意,只有读和写两个操作是原子的,像x++、x+=2这种表达式还是相当于先读,然后再写,两次进行,可以读到中间的值。
2. 对所有的volatile的变量的所有次读写操作,组成一个全局的全序关系。全序关系的意思是:任何两个操作之间都可以比较先后关系。这个全序关系叫“同步顺序”(synchronization order)。这个同步顺序和“程序顺序”(program order,也就是单个线程里各个操作的顺序)是一致的。根据这个顺序,每次读操作,看到的一定是它之前最后一次对同一个变量写的值,如果它之前没有对这个变量的写操作,就读到初始值(0、null、false)。
3. volatile变量可以用来建立synchronize-with关系,这有点复杂,但这一题可以不考虑这个。
详细介绍见这一页: http://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html
所以,按照这个顺序,原例子里,线程t1里有两个写:x=1、y=2。由于同步顺序必须和程序顺序一致,在同步顺序里,x=1也必须在y=2的前面。同理,在同步顺序里,线程2里的b=y也必须在a=x的前面。如果线程2里,读操作b=y真的读到了线程1里的写操作y=2写入的值2,那么这个y=2也必须在b=y的前面,毕竟y=2之前再也没有别的写操作可以把y的值写成2。所以,如果线程2看到了y==2,整个程序里的4个volatile读写操作只能构成下面这个“同步顺序”:
t1: x=1
t1: y=2
t2: b=y
t2: a=x
根据这个顺序,a=x读到的值一定是它前面的最后一个对x的写操作的值,也就是那个x=1。
所以,如果线程2读到y==2,必定读到x==1。
====下面是额外的内容====
30楼 @mysjmr2012
“还有在我看来,如果先跑2进程,那Y的值一定是0啊x也是0,不会进if,如果Y=2,那X一定=1啊,那为什么会争论X的值?”
我觉得这句话可以直接点中“内存模型”(memory model)的必要性。多线程的程序难道不是各个中各个操作的互相交叠吗?也许以前是这样。现在绝不能这样想。
1、cache的存在,会让一个CPU写的数据以不同的速度让别的CPU观察到。
2、CPU执行指令的顺序可以和程序顺序不一样,这样可以充分利用里面的计算元件。相关的概念有“乱序执行”、“超标量流水线”等。
3、编译器会进行优化,调换内存操作的顺序。
举个例子,假设初始x=y=0。比如线程1执行"x=1;y=2",而线程2执行"r1=y;r2=x"。如果认为“程序执行的结果是原程序各个线程的操作按原顺序交叠”,这种情况好像是绝不会出现的,要么y==0,要么y==2而且x==1。但是,如果执行线程1的CPU“不小心”调换了两个写的顺序,y比x更早写到内存里,那么线程2就会读到y==2而x==0。这个结果和任何一种交叠都不一样。
但是,这个问题引出了一个概念:
如果一个多线程的程序的执行的结果,等价于某种不改变每个线程的顺序,将各个线程中的操作混合成一个串行程序,而执行出来的结果,那么,我们称这次执行是顺序一致(sequentially consistent)的。
保证顺序一致性的代价是很高的,所以CPU和编程语言一般都不保证顺序一致性。Java并不保证任何程序的任何执行都是顺序一致的。Java提供一个内存模型(memory model),这是一套规则,根据这套规则可以写出行为可以预料的多线程程序。
====我是朴素的分割线====
17楼 @bixiaopeng
“一定打印1吧,用那个什么vxxxx标注的变量的读写,应该不会被乱序吧,,,所以既然读到2了,那肯定已经x=1了,,,读vxxx的变量应该会直接读到现在的值就是1,,,
是么? ”
坚定一点多好,答案就是一定。
====我是朴素的分割线====
24楼 @zwan0518
“ volatile应该是保证每次读都是从内存,而不是缓存。 ”
不是这样的。volatile只要保证原子性和顺序就行了。
====我是朴素的分割线====
27楼 @S530723542
“ 如果打印了println,x不一定等于1,java里面在单线程内是有序的,而别的线程看这个线程的时候是无序的。volatile关键字属于干扰条件,他的作用是,当a被修改以后,别的线程能马上看到。但对Job2来说,Job1是乱序的。 ”
volatile才是真正决定这个程序的结果的因素呢。对volatile变量的操作都是“同步操作”(synchronization action),不仅保证顺序,还有别的性质。
volatile的意思并不是“马上”看到。信号从一个cpu传到另一个cpu总是要花时间的。“光速是有限的”在处理器的设计里已经是个不小的问题了。
====我是朴素的分割线====
28楼 @FatGhosta
“ 我觉得x可以打出0啊。总觉得这道题不应该扯到happen before的问题上。具体怎么解释我再想想。。。”
这道题的结果可以用happen before来解释。
{t1: x=1} comes before {t1: y=2} in program order
{t1: y=2} synchronizes with {t2: b=y}
{t2: b=y} comes before {t2: a=x} in program order
所以{t1: x=1} happens before {t2: a=x},也是唯一一个happen before这个读操作的对x的写。所以看到的就是x==1
但是用synchronization order更容易解释。
====我是朴素的分割线====
29楼 @nancheng2008
“ 谈谈我的理解,volatile只保证变量的可见性,不保证原子性。使用这个关键词会导致“缓存锁定”。
缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
就是说,只要能打印,y就等于2了,同时x肯定被改变了,此时读取出来的x肯定是1了。不知道这么理解对不对。。 ”
如果缓存一致性使用“所有制”的话,一个处理器是可以“拥有”多个区域的,只是如果别的处理器要写同一个区域,会把这个区域的所有权“夺走”。关键是它们按什么顺序写回。这就看CPU怎么处理这个了,Java的volatile的顺序要求太强了,有时候真的要在两个操作之间插入fence的。
====我是朴素的分割线====
33楼 @Eclipse
“也可能是0,Job1的x,y没有依赖关系,所以可能先执行y=2,然后执行x=1 ”
还是再说一遍吧,Java的内存模型不涉及数据依赖。
====我是朴素的分割线====
35楼 @glazard
“记得面阿里系统工程师的时候,当时提到CPU的乱序执行,被问过volatile能否保证顺序一致性,算是这个问题的C/C++版吧… ”
C/C++的volatile的定义很模糊:“volatile的读写要严格按C/C++规范来实现”。但什么叫“严格按照”,就不知道了,一般人用volatile来做IO。
====我是朴素的分割线====
36楼 @zlwmosquito
“ 我觉得是如果打印的话,一定会打印x=1
用JMM的happens before规则的传递性可以推出来 ”
干得漂亮
====我是朴素的分割线====
39楼 @xydaxia
“ 肯定只能打印1啊,volatile关键字能保证可见和有序,但是不能保证原子性。x.y都是volatile的,所以肯定是有序的。 ”
干得漂亮。
====我是朴素的分割线====
41楼 @dream
“在java里面volatile不仅能保证线程不对其本身做优化,更重要的是防止乱序的发生,……
所以只能打印出1来。。。 ”
总之,干的漂亮。不过,确切地说volatile不能阻止优化,而是给出一个规则。这个规则可以限制什么样的优化可以做,什么样的优化不能做。如果某种神奇的实现真的能调换指令的顺序,却还能骗过所有的程序的所有Java线程(真可能吗?不知道,也许做了“全程序分析”),也算是合法的Java实现。“你可以作弊,只要你不被抓住”
这是一条镜像帖。来源:北邮人论坛 / java / #39220同步于 2015/3/13
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Java机器人发帖
歪楼的volatile。答案其实很简单。
nuanyangyang
2015/3/13镜像同步68 回复
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
其实说实话,个人认为像volatile的出现是没有办法的办法。单从使用角度来说,一个好的语言应该使使用者尽量少的关心底层细节,比如这个问题,就像写Java不必要关心其在运行过程中是怎样的机器码,理论上也没有理由关心自己的代码在底层的优化细节(尤其是这种优化细节还会造成正确性的问题)。但是没办法啊,估计语言的设计者也没有找到效率和易用性两全其美的办法,所以只能这么干了。另外一个反例可以看下python的GIL,为了保证线程安全牺牲了性能,你会发现多线程的python程序有时候反倒没有一个线程运行的快,语言的开发真是一件任重而道远的活。。。
【 在 dream 的大作中提到: 】
: 其实说实话,个人认为像volatile的出现是没有办法的办法。单从使用角度来说,一个好的语言应该使使用者尽量少的关心底层细节,比如这个问题,就像写Java不必要关心其在运行过程中是怎样的机器码,理论上也没有理由关心自己的代码在底层的优化细节(尤其是这种优化细节还会造成正确性的问题)。但是没办法啊,估计语言的设计者也没有找到效率和易用性两全其美的办法,所以只能这么干了。另外一个反例可以看下python的GIL,为了保证线程安全牺牲了性能,你会发现多线程的python程序有时候反倒没有一个线程运行的快,语言的开发真是一件任重而道远的活。。。
volatile并不需要java程序员知道底层细节。从java层次上看,volatile只有3个意思:
1. 对volatile变量的读写是原子的
2. 所有对volatile变量的所有读写组成一个全序关系
3. 如果一个线程读到了另一个线程对同一个volatile变量写的值,那么这一对读写建立“synchronizes with”关系,和“happens before”关系一致。
这三个意思没有一个涉及底层实现。Java的实现只要能骗过所有的Java程序,在底层怎么实现都可以。比如:
1. 底层使用green thread,或者GIL,任何时间只有一个线程在跑。(这种实现很烂,但却是合法的Java实现)
2. 每个读写前后都插入fence。这有点过分,但还是正确的。
3. 细细地推断什么地方应该插入fence,或者用特殊的指令(ARMv8的lda和stl已经可以保证sequential consistency)。
正确使用memory model的话,Java程序员不用知道实现细节,就能写出正确的多线程程序。
“所有对volatile变量的所有读写组成一个全序关系”,这只是JAVA的特性吗?C++应该是没有这限制,所以,上述若是C++代码的话,是有可能输出 Surprise! y==2, but x!=1. x=.. 的?