这里没有人提供Java中这种算法的正确/安全实现。我不确定John
W的解决方案应该如何工作,因为它缺少一些内容(即ThreadLocals的声明以及他的数组中应该包含的内容的解释—基本
booleans没有
get()和
set())。
Java语言规范的第17章介绍了Java内存模型。特别受关注的是第17.4.5节,该节描述了
事前发生的 顺序。在单个线程中考虑是很容易的。考虑一下代码片段:
int x, y, z, w;x = 0;y = 5;z = x;w = y;
每个人都会同意,在此代码段的末尾,
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,
y并且B可以获取
y
before 的新值是合法的。的新值
x出现。规格说明:
更具体地说,如果两个动作共享事前发生关系,则它们不一定必须按照顺序与没有事前发生关系的任何代码发生过关系。
我们该如何解决?对于普通的字段访问,
volatile关键字就足够了:
对volatile变量(第8.3.1.4节)的写操作将与任何线程对v的所有后续读取进行同步(其中,后续操作是根据同步顺序定义的)。
与-同步 是比发生-之前更强的条件,并且由于发生-在-
之前是可传递的,因此如果线程A希望线程B看到其对
x和的写入
y,则它只需
z在写入
x和之后写入一个易失变量
y。线程B需要从读
z读书之前
x和
y,这将保证看到的新的价值观
x和
y。
在Gabriel的解决方案中,我们看到了这种模式:对发生
in写操作
turn,其他线程看不到,但随后对进行写操作,因此保证其他线程只要
turn先读就可以看到这两个写操作。
不幸的是,while循环的条件是向后的:为了确保线程不会看到的过时数据
in,while循环应该从
turn第一个读取:
// ... while (turn == other() && in[other()]) { // ...考虑到此修复程序,解决方案的其余大部分都是可以的:在关键部分,我们不在乎数据的陈旧性,因为好在关键部分!唯一的其他缺陷出现在最后:Runnable设置
in[id]为新值并退出。是否可以保证另一个线程看到的新值
in[id]?规范说不:
线程T1中的最终操作与另一个线程T2中检测到T1已终止的任何操作同步。T2可以通过调用T1.isAlive()或T1.join()来实现。
那么我们如何解决呢?只需
turn在方法末尾添加另一个写入:
// ... in[id] = false; turn = other();}// ...
由于我们对while循环进行了重新排序,因此可以保证另一个线程看到新的false值,
in[id]因为写入操作
in[id]发生在写入
turn发生之前,读取
turn发生发生之前,读取发生之前
in[id]。
不用说,如果没有 大量 评论,此方法将很脆弱,有人可能会出现并更改某些内容并巧妙地破坏了正确性。仅声明数组
volatile还不够好:正如Bill
Pugh(Java内存模型的主要研究人员之一)在该线程中所解释的那样,声明数组会使对数组
引用的
更新对其他线程可见。对数组元素的更新不一定是可见的(因此,我们只需使用另一个变量来保护对数组元素的访问就可以跳过所有循环)。
volatile
__
volatile
如果您希望代码简洁明了,请保持原样并更改
in为AtomicIntegerArray(将0表示为false,将1表示为true;不使用AtomicBooleanArray)。此类的行为就像一个数组,其元素均为all
volatile,因此可以很好地解决我们的所有问题。另外,您可以只声明两个volatile变量
boolean in0和
booleanin1,然后更新它们,而不使用布尔数组。



