【问题标题】:Difference between CopyOnWriteArrayList and synchronizedListCopyOnWriteArrayList 和 synchronizedList 之间的区别
【发布时间】:2015-03-11 05:50:10
【问题描述】:

根据我的理解,并发集合类优于同步集合,因为并发集合类不会锁定完整的集合对象。相反,它们会锁定集合对象的一小部分。

但是当我检查CopyOnWriteArrayListadd 方法时,我们正在获取对完整集合对象的锁定。那为什么CopyOnWriteArrayListCollections.synchronizedList 返回的列表好呢?我在CopyOnWriteArrayListadd 方法中看到的唯一区别是,每次调用add 方法时,我们都会创建该数组的副本。

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

【问题讨论】:

标签: java collections


【解决方案1】:

根据我的理解,并发集合类优于同步集合,因为并发集合类不会锁定完整的集合对象。相反,它会锁定集合对象的一小部分。

这适用于某些系列,但并非全部。 Collections.synchronizedMap 返回的 map 会围绕每个操作锁定整个 map,而 ConcurrentHashMap 只为某些操作锁定一个哈希桶,或者它可能对其他操作使用非阻塞算法。

对于其他集合,使用的算法以及权衡取舍是不同的。与CopyOnWriteArrayList 相比,Collections.synchronizedList 返回的列表尤其如此。正如您所指出的,synchronizedListCopyOnWriteArrayList 在写入操作期间都会锁定整个数组。那么为什么会有所不同呢?

如果您查看其他操作,例如迭代集合的每个元素,就会发现差异。 Collections.synchronizedList 的文档说,

用户在迭代返回的列表时必须手动同步它:

    List list = Collections.synchronizedList(new ArrayList());
    ...
    synchronized (list) {
        Iterator i = list.iterator(); // Must be in synchronized block
        while (i.hasNext())
            foo(i.next());
    }

不遵循此建议可能会导致不确定的行为。

换句话说,遍历synchronizedList不是线程安全的,除非您手动锁定。请注意,使用此技术时,此列表中其他线程的所有操作,包括迭代、获取、设置、添加和删除,都将被阻止。一次只有一个线程可以对这个集合做任何事情。

相比之下,CopyOnWriteArrayList 的文档说,

“快照”样式的迭代器方法使用对创建迭代器时数组状态的引用。这个数组在迭代器的生命周期内永远不会改变,所以干扰是不可能的并且迭代器保证不会抛出ConcurrentModificationException。自创建迭代器以来,迭代器不会反映对列表的添加、删除或更改。

此列表中其他线程的操作可以同时进行,但迭代不受任何其他线程所做更改的影响。因此,即使写操作锁定了整个列表,CopyOnWriteArrayList 仍然可以提供比普通synchronizedList 更高的吞吐量。 (前提是读取和遍历写入的比例很高。)

【讨论】:

  • 很棒的答案
【解决方案2】:

对于写入(添加)操作,CopyOnWriteArrayList 使用ReentrantLock 并创建数据的备份副本,并且底层易失性数组引用仅通过 setArray 更新(setArray 之前对列表的任何读取操作将返回之前的旧数据add)。此外,CopyOnWriteArrayList 提供快照故障安全迭代器,并且不会在写入/添加时抛出 ConcurrentModifficationException。

但是当我检查 CopyOnWriteArrayList.class 的 add 方法时,我们正在获取对完整集合对象的锁定。那么为什么 CopyOnWriteArrayList 比 synchronizedList 好。我在 CopyOnWriteArrayList 的 add 方法中看到的唯一区别是,每次调用 add 方法时我们都会创建该数组的副本。

  1. 不,锁不在整个 Collection 对象上。如上所述,它是一个ReentrantLock,它不同于内部对象锁。
  2. add 方法将始终创建现有数组的副本并对副本进行修改,然后最后更新数组的 volatile 引用以指向这个新数组。这就是为什么我们有名称“CopyOnWriteArrayList” - 当你写入它时会复制。这也避免了 ConcurrentModificationException

【讨论】:

  • 另外,CopyOnWriteArrayList不能使用Iterator修改列表,Collections.synchronizedList()可以。
  • 如果我错了,请纠正我,在 CopyOnWriteArrayList 的情况下并发添加将不起作用,因为添加方法正在使用完整列表上的锁定。一旦第一个线程完成添加操作并释放锁,那么只有第二个线程可以开始添加操作。
  • ReentrantLock 是不同的(在一般意义上),因为它不进行内部对象锁定,但它是另一种在 java 中实现资源锁定的机制。在 CopyOnWriteArrayList 的 add 方法里面,可以看到锁是通过调用 ReentrantLock 的 lock() 方法获得的。摘自 java doc “如果锁被另一个线程持有,那么当前线程将被禁用以用于线程调度目的”。简而言之,是的,第二个线程会一直等到第一个线程释放锁。
【解决方案3】:

1) 对CopyOnWriteArrayList 的获取和其他读取操作不同步。

2) CopyOnWriteArrayList 的迭代器永远不会 throws ConcurrentModificationExceptionCollections.synchronizedList 的迭代器可能会抛出它。

【讨论】:

  • 我同意上面提到的两点,因为读取是易失性读取,但是想知道 synchronizedList 的 add 方法和 CopyOnWriteArrayList 的 add 方法有什么区别吗?在这两种情况下,我们都在获取完整集合对象的锁定。
  • CopyOnWriteArrayList 在每次添加时创建底层数组的副本,非常昂贵
  • CopyOnWriteArrayList 适用于读取次数明显高于写入次数的情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-12-25
  • 2020-05-10
  • 2014-09-20
  • 2010-10-28
  • 2015-10-04
  • 2012-08-12
  • 2011-02-18
相关资源
最近更新 更多