以volatile int sharedVar为例。我们知道,JLS向我们提供了以下保证:
w在其将值i按程序顺序写入sharedVar之前的每个动作,happens-before写入操作;i的写入w happens-before,i通过读取线程r成功地从sharedVar读取;i的读取线程r happens-before按程序顺序从sharedVar成功读取r的所有后续操作。但是,当读取线程观察值i时,仍然没有给出对的时钟时间保证。一个简单的从不让读取线程看到这个值的实现仍然符合这个契约。
我想这件事已经有一段时间了,我看不出有什么漏洞,但我想一定有。请指出我推理中的漏洞。
发布于 2012-08-01 18:52:09
事实证明,答案和随后的讨论只巩固了我最初的推理。我现在有证据了:
由于Java内存模型不引用挂钟时间,因此不会有任何障碍。现在有两个线程与读取线程并行执行,观察到写入线程没有执行任何操作。QED。
例1:一次写作,一次阅读
要使这一发现具有极大的痛苦和真实性,请考虑以下程序:
static volatile int sharedVar;
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
final long[] aTimes = new long[5], bTimes = new long[5];
final Thread
a = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
sharedVar = 1;
aTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}},
b = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
bTimes[i] = sharedVar == 0?
System.currentTimeMillis()-startTime : -1;
briefPause();
}
}};
a.start(); b.start();
a.join(); b.join();
System.out.println("Thread A wrote 1 at: " + Arrays.toString(aTimes));
System.out.println("Thread B read 0 at: " + Arrays.toString(bTimes));
}
static void briefPause() {
try { Thread.sleep(3); }
catch (InterruptedException e) {throw new RuntimeException(e);}
}就JLS而言,这是一项法律产出:
Thread A wrote 1 at: [0, 2, 5, 7, 9]
Thread B read 0 at: [0, 2, 5, 7, 9]请注意,我不依赖于currentTimeMillis的任何故障报告。“时代”的报道是真实的。但是,实现确实选择只在读取线程的所有操作之后才使写入线程的所有操作可见。
示例2:读取和写入两个线程
现在@StephenC认为,许多人都会同意他的观点,尽管在此之前,尽管没有明确提及,但仍然暗示着是一个时间顺序。因此,我提出了我的第二个程序,它说明了这可能达到的确切程度。
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
final long[] aTimes = new long[5], bTimes = new long[5];
final int[] aVals = new int[5], bVals = new int[5];
final Thread
a = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
aVals[i] = sharedVar++;
aTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}},
b = new Thread() { public void run() {
for (int i = 0; i < 5; i++) {
bVals[i] = sharedVar++;
bTimes[i] = System.currentTimeMillis()-startTime;
briefPause();
}
}};
a.start(); b.start();
a.join(); b.join();
System.out.format("Thread A read %s at %s\n",
Arrays.toString(aVals), Arrays.toString(aTimes));
System.out.format("Thread B read %s at %s\n",
Arrays.toString(bVals), Arrays.toString(bTimes));
}为了帮助理解代码,这将是一个典型的、真实的结果:
Thread A read [0, 2, 3, 6, 8] at [1, 4, 8, 11, 14]
Thread B read [1, 2, 4, 5, 7] at [1, 4, 8, 11, 14]另一方面,您从来没有想过会看到这样的情况,但是按照JMM的标准,它仍然是合法的。
Thread A read [0, 1, 2, 3, 4] at [1, 4, 8, 11, 14]
Thread B read [5, 6, 7, 8, 9] at [1, 4, 8, 11, 14]JVM实际上需要预测线程A在第14时将写什么,以便知道如何让线程B在第1时间读取。这是否可信,甚至可行性是相当可疑的。
在此基础上,我们可以定义实现可以采取的以下实际的自由:
线程不间断发布操作的可见性可以安全地推迟到中断线程的获取操作之前。
术语发布和获取是在JLS§17.4.4中定义的。
这条规则的一个推论是,只写入和从不读取任何内容的线程的操作可以被无限期地延迟,而不会违反发生之前的关系。
清理易失性概念
volatile修饰符实际上是关于两个不同的概念:
注意,JLS没有以任何方式指定第2点,它是由一般期望产生的。显然,一个违背承诺的实现仍然是符合的。随着时间的推移,当我们转向大规模并行架构时,这一承诺可能确实是相当灵活的。因此,我预计,在未来,担保与承诺的结合将证明是不够的:取决于需求,我们将需要一个没有另一个,一个具有另一个不同的味道,或任何数量的其他组合。
发布于 2012-08-01 14:50:51
你说的部分是对的。我的理解是,如果并且只有当线程r没有进行任何其他发生过的操作时--在相对于线程w的关系之前--这才是合法的。
因此,在时间方面没有保证,但在程序中有其他同步点的保证。
(如果这让您感到困扰,请考虑一下,从更根本的意义上说,JVM并不能保证JVM能够及时执行任何字节码。一个只会永远陷入停滞的JVM几乎肯定是合法的,因为在执行过程中提供硬时间保证基本上是不可能的。)
发布于 2012-08-01 14:44:58
请看本节(17.4.4)。您稍微扭曲了规范,这就是让您感到困惑的地方。易失性变量的读/写规范没有提到特定的值,特别是:
更新:
正如@AndrzejDoyle所提到的,您可以想象,只要线程在该点之后不做任何其他操作,就可以让线程r读取一个陈旧的值,然后在执行的以后的某个点与线程w建立一个同步点(就像这样,您将违反规范)。因此,确实存在一些回旋余地,但是线程r在其所能做的事情上将受到很大的限制(例如,写入System.out将建立一个稍后的同步点,因为大多数流内嵌都是同步的)。
https://stackoverflow.com/questions/11761552
复制相似问题