这里没有人在 Java 中提供此算法的正确/安全实现。我不确定 John W 的解决方案应该如何工作,因为它缺少部分(即 ThreadLocals 的声明和对他的数组中应该包含的内容的解释 - 原始 booleans 没有 get() 和set())。
Chapter 17 of the Java Language Specification 解释了 Java 内存模型。特别感兴趣的是Section 17.4.5,它描述了 happens-before 顺序。在单个线程中很容易考虑。考虑 sn-p:
int x, y, z, w;
x = 0;
y = 5;
z = x;
w = y;
每个人都会同意,在这个 sn-p 结束时,x 和 z 都等于 0,y 和 w 都等于 5。忽略声明,我们在这里有六个操作:
- 写信给
x
- 写信给
y
- 来自
x的读取
- 写信给
z
- 来自
y的读取
- 写信给
w
因为它们都出现在同一个线程中,JLS 说这些读取和写入保证表现出这种顺序:上面的每个动作 n (因为动作在单个线程中)都有一个与所有操作 m、m > n 的发生前关系。
但是不同的线程呢? 对于正常的字段访问,线程之间没有建立之前发生的关系。这意味着线程 A 可以增加共享变量,线程 B 可能会读取该变量,但 看不到新值。在 JVM 中程序的执行过程中,线程 A 的写入传播可能已被重新排序,以发生在线程 B 的读取之后。
事实上,线程 A 可以写入变量 x,然后写入变量 y,从而在线程 A 中的这两个操作之间建立起先发生关系。但线程 B 可能会读取 x 和 @ 987654346@,B 在x 的新值出现之前获取y 的新值是合法的。规范说:
更具体地说,如果两个动作
分享发生前的关系,
他们不一定要出现
以这种顺序发生在任何
他们不共享的代码
发生之前的关系。
我们如何解决这个问题?对于普通的字段访问,volatile 关键字就足够了:
对 volatile 变量的写入
(§8.3.1.4) v 与所有同步
任何线程对 v 的后续读取
(其中后续根据定义
同步顺序)。
synchronizes-with 是比happens-before 更强的条件,并且由于happens-before 是可传递的,如果线程A 希望线程B 看到它对x 和y 的写入,它只需在写入x 和y 后写入一个易失性变量z。线程 B 需要在读取 x 和 y 之前从 z 读取,并且可以保证看到 x 和 y 的新值。
在 Gabriel 的解决方案中,我们看到了这种模式:in 发生写入,其他线程不可见,但随后turn 发生写入,因此其他线程可以保证看到这两个写入只要他们首先阅读turn。
不幸的是,while 循环的条件是向后的:为了保证线程不会看到in 的陈旧数据,while 循环应该首先从turn 读取:
// ...
while (turn == other() && in[other()]) {
// ...
考虑到这个修复,解决方案的其余大部分都可以:在临界区,我们不关心数据的陈旧性,因为,好吧,我们处于临界区!唯一的其他缺陷出现在最后:Runnable 将 in[id] 设置为新值并退出。是否保证其他线程看到in[id] 的新值?规范说不:
线程 T1 中的最后一个动作
与任何动作同步
另一个检测到 T1 的线程 T2
已终止。 T2可以完成
通过调用 T1.isAlive() 或
T1.join().
那么我们该如何解决呢?只需在方法末尾添加另一个写入turn:
// ...
in[id] = false;
turn = other();
}
// ...
由于我们对 while 循环进行了重新排序,因此其他线程将保证看到 in[id] 的新 false 值,因为对 in[id] 的写入发生在对 turn 的写入之前发生在对 @987654373 的读取之前@happens-before 从in[id] 读取。
不用说,如果没有 ton 的 cmets 度量,这种方法很脆弱,有人可能会出现并改变某些东西并巧妙地破坏正确性。仅仅声明数组volatile 是不够的:正如Bill Pugh 解释的in this thread(Java 内存模型的lead researchers 之一),声明数组volatile 会更新数组reference 对其他线程可见。数组元素的更新不一定是可见的(因此我们只需要通过使用另一个 volatile 变量来保护对数组元素的访问来跳过所有循环)。
如果您希望代码清晰简洁,请保持原样并将in 更改为AtomicIntegerArray(使用 0 表示 false,1 表示 true;没有 AtomicBooleanArray)。这个类就像一个数组,其元素都是volatile,因此可以很好地解决我们所有的问题。或者,您可以只声明两个 volatile 变量 boolean in0 和 boolean in1,并更新它们而不是使用布尔数组。