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关系,这有点复杂,但这一题可以不考虑这个。详细介绍见这一页: 所以,按照这个顺序,原例子里,线程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=1t1: y=2t2: b=yt2: 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实现。“你可以作弊,只要你不被抓住”