问题 Collections.unmodifiableList(list)是否需要锁定?


我有一个 productList 维护在一个名为的文件中 Products.java

private List<String> productList = Collections.synchronizedList(new ArrayList());

现在创建一个同步列表,将确保add / remove之类的操作将具有隐式锁定,并且我不需要显式锁定这些操作。

我有一个暴露的函数返回一个 unmodifiableList 这个清单。

public List getProductList(){

 return Collections.unmodifiableList(productList);
}

在我的应用程序中,各种线程可以同时调用此函数。所以我需要放一个 synchronized 将List转换为不可修改的List时阻塞,或者由于我使用的是sychronizedList,这是否已经处理好了?

TIA。


6628
2018-02-09 08:51


起源

相关问题: stackoverflow.com/questions/35281056/... - Stuart Marks


答案:


它不需要同步,因为不可修改的列表包装同步的列表。但是,除了迭代的目的之外,在不可修改的列表上进行同步没有多大用处 要求 手动同步无论:

用户必须手动同步返回的内容   迭代时列出:

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

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

编辑:正如Ferrybig指出的那样,实际上不可能与不可修改的包装器安全地同步。您可能需要考虑另一种线程安全解决方案,例如 CopyOnWriteArrayList


5
2018-02-09 08:54



“但是在不可修改的列表上进行同步并没有多大用处”,这种说法充其量是误导性的,最坏的是错误的。写入共享数据的代码不仅仅是必须处理锁定的代码。读取共享数据的代码必须使用锁或确保安全发布的机制。 - Raedwald


应该使用synchronized的唯一地方是当你循环它时,如下所述 的javadoc

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

但是,一旦将其包裹在一个中,就无法做到这一点 unmodifiableList,使返回结果不安全。在并发访问的情况下,它可能返回损坏的数据。

返回后端列表可能更好,而不是返回后端列表,因此调用不需要担心同步性能。

public List getProductList(){
    synchronized (productList) {
       return new ArrayList<>(productList);
    }
}

4
2018-02-09 08:57



如果我采用返回新ArrayList的方法,那么在我访问返回列表的过程中,可能会在原始列表中添加或删除一些我不想错过的元素。在我的情况下,我总是需要原始列表。 - thedarkpassenger
在这种情况下,您可能希望将后端列表设为a CopyOnWriteArrayList 就像@shmosel所说的那样,这些列表不需要包含在 synchronizedList 阻止,不要受苦 ConcurrentModificationException 循环时。我建议你在接受任何答复之前至少等待一个小时,我看到这个问题正在变得越来越流行。 - Ferrybig


无需放置同步块。


0
2018-02-09 08:56



正如你在shmosel和Ferrybig的答案中看到的那样,这是不正确的。由于您没有提供任何信息来备份您的陈述,因此它也是无用的答案。 - Erwin Bolwidt


最简单的解决方案是每次都获取快照,

list = get snapshot
{
    work on the list
}
discard list // GC it

由于快照是一个不变的,冻结的数据结构,客户端可以自由访问它。

但是,如果客户端访问可能被另一方修改的数据结构,则会出现问题。忘掉并发问题;考虑下面代码的语义

{
    n = list.size();
    list.get(n-1);
}

get(n-1) 可能会失败,因为列表可能会在调用时缩小。

为了获得某种一致性保证,客户端必须在访问会话期间提供明确的事务划分,例如

acquire lock // preferably a read-lock
{
    work on the list
}
release lock

请注意,此代码并不比快照解决方案简单。客户端仍然可以“错过”更新,就像快照解决方案一样。

您必须决定是否要强制客户端代码执行此类锁定。

当然,这并非没有优点;如果列表很大并且更新很少,那么它的性能可能比快照解决方案更好。

如果这种方法更适合应用程序,我们可能会设计类似的东西

interface CloseableList extends List, Closeable {}

public CloseableList readProducts() // acquire read-lock when called

-- client code
try{CloseableList list = readProducts())
{
    ....
}   
// on close(), release the read-lock

如果客户端只需要迭代产品,我们就可以使用java8 Stream

final ReadWriteLock lock = ...

private ArrayList productList = new ArrayList();
// modifications to `productList` must be protected by write lock

// javadoc: reader must close the stream!
public Stream readProducts()
{
    lock.readLock().lock();
    return productList.stream().onClose( lock.readLock()::unlock );
}

-- client code
try(Stream products = readProducts())
{
    ...
}

我们还可以设计API以接收客户端代码,以便我们可以通过保护来包围它

public void readProducts(Consumer<List> action)
{
    lock read

    action.accept(productList);

    finally unlock read
}

-- client code
readProducts(list->
{
    ...
});

0
2018-02-09 13:16