在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中,这意味着我们没有修改元素数组,那么为什么我们需要刷新变量数组的值呢?答案 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
):
CopyOnWriteArrayList
CopyOnWriteArrayList
(可能是在线程A上,尽管Javadoc可能更清楚这一点)CopyOnWriteArrayList
访问[读取]元素假设步骤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。这有两个原因。
finally
块中执行写语义。方法
lock.unlock()
始终呼叫
private volatile int state;
protected final void setState(int newState) {
state = newState;
}
这使得在语义之前发生了setArray()
,使得set数组变得多余。您可能声称您不想依赖ReentrantLock的实现,但如果您担心ReentrantLock的未来版本不是线程安全的,那么如果是这种情况,您可能会遇到更大的问题。