此代码使用深度 Java Memory Model voodoo,因为它混合了锁和易失性。
但是,此代码中的锁用法很容易省去。锁定在使用相同锁定的线程之间提供内存排序。具体来说,此方法末尾的 unlock 为获取相同锁的其他线程提供了发生在之前(happens-before) 语义。但是,通过此类的其他代码路径根本不使用此锁。因此,锁的内存模型含义与这些代码路径无关。
这些其他代码路径确实使用易失性读取和写入,特别是对字段的读取和写入。该方法对此字段执行易失性读取,方法方法对此字段执行易失性写入。array
getArray
setArray
此代码调用的原因(即使它显然是不必要的)是因为它为此方法建立了一个不变量,以便它始终对此数组执行易失性写入。这将与从此数组执行易失性读取的其他线程建立发生在之前的语义。这很重要,因为易失性写读语义适用于易失性字段本身的读写以外的读写。具体而言,在易失性写入发生之前写入其他(非易失性)字段 - 在对同一易失性变量进行易失性读取之后从这些其他字段读取之前。有关说明,请参阅 JMM 常见问题解答。setArray
下面是一个示例:
// initial conditions
int nonVolatileField = 0;
CopyOnWriteArrayList<String> list = /* a single String */
// Thread 1
nonVolatileField = 1; // (1)
list.set(0, "x"); // (2)
// Thread 2
String s = list.get(0); // (3)
if (s == "x") {
int localVar = nonVolatileField; // (4)
}
假设第 (3) 行获取由行 (2) 设置的值,即中间字符串 。(在本例中,我们使用实习字符串的标识语义。假设这是真的,那么内存模型保证在第(4)行读取的值将是1,由行(1)设置。这是因为在 (2) 处的易失性写入,以及每个较早的写操作,都发生在 (3) 行的易失性读取之前,以及每个后续读取之前。"x"
现在,假设初始条件是列表已经包含单个元素,即中间字符串 。并进一步假设该方法的子句没有进行调用。现在,根据列表的初始内容,第(2)行的调用可能会或可能不会执行易失性写入,因此(4)行的读取可能具有也可能没有任何可见性保证!"x"
set()
else
setArray
list.set()
显然,您不希望这些内存可见性保证依赖于列表的当前内容。要在所有情况下建立保证,需要在所有情况下进行不稳定的写入,这就是为什么即使它本身没有进行任何写入,它也会调用。set()
setArray()
编辑 2022-07-13
Holger在评论中提出了一个有趣的问题:
如果到那时,线程1确实,第一个元素已经是“x”,我们正在谈论的场景,那么线程2不能假设证明线程1确实执行了,因为无论线程2的读取是否在线程1的写入之后,条件总是满足的。因此,如果元素没有变化,则此处(1)和(4)之间没有发生之前的关系。冗余 setArray 调用也没有强制实施内存可见性,因为读取器线程可能在写入之前读取了数组引用。list.set(0, "x");
list.get(0) == "x"
list.set(0, "x");
的确,仅单独查看此代码,无法保证 at (2) 在 at (3) 之前执行。但是,这些是对变量的操作,因此,它们是同步操作。在 JMM 下,同步操作具有总顺序。也就是说,它们将按某种顺序发生,但我们不知道是哪一个。操作可以按顺序 (2)->(3) 或 (3)->(2) 发生;没有其他可能性。set
get
volatile
如果顺序是 (3)->(2), 则 Holger 是正确的,不存在发生前关系,随后的读取(如 (4) 可能会得到一个过时的值。
但是,如果顺序为 (2)->(3),则存在发生前关系,并且读取处 (4) 保证在 (1) 处看到写音。
但是,这难道不是毫无意义,因为我们不能保证同步操作的执行顺序吗?为了建立这种顺序,我们通常会在线程之间使用一些同步操作,这将提供必要的内存可见性保证。这难道不会使(2)处的无条件易失性写入变得无用吗?
不一定。系统外部有一些机制,例如计时器、网络消息或用户交互,它们可以清楚地在某些操作之间建立顺序,但不能建立内存可见性。例如,假设线程 1 频繁执行其操作(例如,每秒一次),而线程 2 执行其操作(例如,每分钟一次)。我们的应用程序可能希望线程 2 获取一些最新值,但不一定是绝对的最新值。线程 1 重复执行的易失性写入(以及线程 2 重复执行的相应易失性写入)可确保线程 2 看到线程 1 的第 59 次或第 60 次更新。如果线程 1 未执行任何易失性写入,则线程 2 可能会看到任意旧的值。
这是一个非常狭窄的边缘情况,但我认为它确定了无条件执行其易失性写入的必要性。CopyOnWriteArrayList::set