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

时间:2015-02-27 19:11:14

标签: java collections locks java-memory-model

在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中,这意味着我们没有修改元素数组,那么为什么我们需要刷新变量数组的值呢?

5 个答案:

答案 0 :(得分:6)

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

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

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

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

以下是一个例子:

// 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)行,即实习字符串"x"设置的值。 (为了这个例子,我们使用内部字符串的标识语义。)假设这是真的,那么内存模型保证第(4)行读取的值将是1(由行(1)设置)。这是因为在(2)和每次更早写入时的易失性写入发生在第(3)行的易失性读取之前以及每次后续读取之前。

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

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

答案 1 :(得分:2)

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


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

  

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

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

正如Java Memory Model specification in the JLS所述:

  

写入易失性字段(§8.3.1.4)发生在之后的每一次   阅读该领域。

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

更新说明

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

  1. 将对象放入CopyOnWriteArrayList
  2. 之前的主题A中的操作
  3. 放置并将对象放入CopyOnWriteArrayList(可能是在线程A上,尽管Javadoc可能更清楚这一点)
  4. 从线程B上的CopyOnWriteArrayList访问[读取]元素
  5. 假设步骤2将一个元素放入已经存在的列表中,我们看到代码进入了这个分支:

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

    对setArray的这一调用确保了对来自线程A的字段array的易失性写入。由于线程B将对字段array执行易失性读取,因此发生在关系之前在线程A和线程B之间创建,如果没有else分支,则不会创建它。

答案 2 :(得分:1)

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

答案 3 :(得分:1)

在JDK 11中,该无用的操作已从源代码中删除。参见下面的代码。

//code from JDK 11.0.1
public E set(int index, E element) {
    synchronized (lock) {
        Object[] es = getArray();
        E oldValue = elementAt(es, index);

        if (oldValue != element) {
            es = es.clone();
            es[index] = element;
            setArray(es);
        }
        return oldValue;
    }
}

答案 4 :(得分:0)

不需要AFAICS。这有两个原因。

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

方法

lock.unlock()

始终呼叫

private volatile int state;

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

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