为什么在CopyOnWriteArrayList中需要setArray()方法调用

在CopyOnWriteArrayList.java中,在方法集(int index,E element)下面

public E set(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); Object oldValue = elements[index]; if (oldValue != element) { int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len); newElements[index] = element; setArray(newElements); } else { // Not quite a no-op; ensures volatile write semantics setArray(elements);----? Why this call required? } return (E)oldValue; } finally { lock.unlock(); } } 

为什么需要调用setArray()? 我无法理解上面写的方法调用的注释。 是因为我们没有使用synchronized块,我们必须手动刷新我们使用的所有变量吗? 在上面的方法中,他们使用了重入锁。 如果他们使用了synchronized语句,他们仍然需要调用setArray()方法吗? 我想不是。

问题2:如果我们最终在else中,这意味着我们没有修改元素数组,那么为什么我们需要刷新变量数组的值?

此代码使用深度Java内存模型voodoo,因为它混合了锁和挥发物。

但是,此代码中的锁定使用很容易省略。 锁定在使用相同锁的线程之间提供内存排序。 具体来说,此方法结束时的解锁提供了与其他线程获取相同锁定的语义之前发生。 但是,通过此类的其他代码路径根本不使用此锁。 因此,锁的内存模型含义与那些代码路径无关。

那些其他代码路径确实使用易失性读写,特别是array字段。 getArray方法对此字段执行易失性读取, setArray方法方法对此字段执行易失性写入。

这个代码调用setArray的原因,即使它显然是不必要的,因此它为这个方法建立了一个不变量,它始终对这个数组执行易失性写操作。 这与其他执行此数组的易失性读取的线程的语义之前建立了。 这很重要,因为易失性写 – 读语义适用于除volatile字段本身之外的读写操作。 具体而言,在易失性写入发生之前写入其他(非易失性)字段 – 在对相同易失性变量的易失性读取之后从那些其他字段读取之前。 有关说明,请参阅JMM常见问题解答 。

这是一个例子:

 // initial conditions int nonVolatileField = 0; CopyOnWriteArrayList 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) } 

假设line(3)得到由行(2)设置的值,即实习字符串"x" 。 (为了这个例子,我们使用内部字符串的身份语义。)假设这是真的,那么内存模型保证第(4)行读取的值将是1(由行(1)设置)。 这是因为在(2)处的易失性写入以及每次更早的写入发生在第(3)行的易失性读取之前以及每次后续读取之前。

现在,假设初始条件是列表已经包含单个元素,即实习字符串"x" 。 并进一步假设set()方法的else子句没有进行setArray调用。 现在,根据列表的初始内容,第(2)行的list.set()调用可能会也可能不会执行易失性写入,因此第(4)行的读取可能有也可能没有任何可见性保证!

显然,您不希望这些内存可见性保证依赖于列表的当前内容。 为了在所有情况下建立保证, set()需要在所有情况下执行易失性写入,这就是为什么它调用setArray()即使它没有自己写任何文件。

TLDR; 需要调用setArray来提供CopyOnWriteArrayList的Javadoc中指定的保证 (即使未更改列表的内容)


CopyOnWriteArrayList具有Javadoc中指定的内存一致性保证:

内存一致性效果:与其他并发集合一样,在将对象放入CopyOnWriteArrayList之前,线程中的操作发生在从另一个线程中的CopyOnWriteArrayList访问或删除该元素之后的操作之前

调用setArray是强制执行此保证所必需的。

正如JLS中的Java内存模型规范所述 :

写入易失性字段(第8.3.1.4节) – 在每次后续读取该字段之前发生

所以写入array (使用setArray )方法是必要的,以确保从列表中读取的其他线程现在具有与调用set方法的线程之前发生的 (或者更确切地说,发生在 – 之后)关系,即使在元素中也是如此在set方法中已经与该位置列表中已有的元素相同(使用== )。

更新说明

回到Javadoc的保证。 有这样的事情顺序(假设访问,而不是删除,作为最后一个操作 – 由于使用lock ,已删除已被处理,但访问不使用lock ):

  1. 将对象放入CopyOnWriteArrayList之前的线程A中的操作
  2. 放置和对象到CopyOnWriteArrayList (可能在线程A上,尽管Javadoc可能更清楚)
  3. 从线程B上的CopyOnWriteArrayList访问[读取]元素

假设步骤2将一个元素放入已经存在的列表中,我们看到代码进入了这个分支:

 } else { // Not quite a no-op; ensures volatile write semantics setArray(elements); } 

对setArray的这一调用确保了从线程A对字段array进行易失性写入。由于线程B将对字段array执行易失性读取,因此在线程A和线程B之间创建了一个先发生关系,如果存在,则不会创建该关系。别的分支不存在。

我相信这是因为读取数组的其他方法没有获得锁定,所以在订购之前无法保证。 保持这种排序的方法是更新确保这种排序的volatile字段。 (这是它所指的写语义)

它不是AFAICS所必需的。 这有两个原因。

  • 只有在执行写操作时才需要写语义,这不是。
  • lock.unlock()不可避免地在finally块中执行写语义。

方法

 lock.unlock() 

总是打电话给

 private volatile int state; protected final void setState(int newState) { state = newState; } 

并且这给出了在语义之前发生的事情,因为setArray()使得set数组变得多余。 您可能声称您不想依赖ReentrantLock的实现,但如果您担心ReentrantLock的未来版本不是线程安全的,那么如果是这种情况,您可能会遇到更大的问题。