如何在Java中返回一个线程安全/不可变的集合?

10
在我编写的项目中,我需要从一个函数返回一个线程安全且不可变的视图。然而,我对此并不确定。由于synchronizedList和unmodifiableList只返回列表的视图,我不知道下面这个代码是否能够解决问题: ```Collections.synchronizedList(Collections.unmodifiableList(this.data));```
请问有人能告诉我这是否正确?如果不是,是否存在可能会失败的情况?
感谢任何输入!
6个回答

13

我认为这是JDK中的一个真正差距。幸运的是,由Java集合设计师Joshua Bloch领导的Google团队创建了一个,其中包括真正的不可变集合。

ImmutableList特别是您要查找的实现。 这里是Guava ImmutableCollections一些功能的快速概述。


+1 Ray,你能否提供更多细节或链接来解释你所说的JDK中的真正差距是什么,这个问题一直存在到JDK 6和7吗? - OCB
我不知道JDK 7中是否有这样的添加;我不知道JDK 6中是否有真正的不可变列表。 - Ray
你可以通过复制原始列表并返回不可修改的副本视图来获得相同的结果。因此,它基本上只节省了一行代码 - 但是如果已经在使用Guava,为什么要多写一行代码呢? - Voo
1
@Voo,是的和不是的。你说得对,一个被丢弃的副本的不可修改视图确实是不可变的。但是,假设你是这个列表的消费者。你不知道生产者是否保留了可修改的列表。因此,你必须自己进行防御性复制,这会带来一些开销。如果你使用的列表可以保证它不能通过任何方式被修改,程序员就可以确信可以避免这一步骤。事实上,如果你尝试对Guava的ImmutableList进行防御性复制,它将返回给你原始实例。 - Ray
有没有证据表明Guava Immutable Collection确实是线程安全的?我还不确定。这个问题不是关于并发更改,而是关于Java内存模型并发效应的。 - 30thh

5
Collections.unmodifiableList(this.data) 

上面的语句就足够了,因为它会返回一个视图。对此视图进行任何修改都将导致抛出UnsupportedOperationException。以下是Collections#unmodifiableList文档的摘录。

返回指定列表的不可修改视图。此方法允许模块向用户提供对内部列表的“只读”访问权限。对返回的列表进行的查询操作“透过”到指定的列表,并且尝试修改返回的列表(无论是直接还是通过其迭代器),都会导致UnsupportedOperationException

......

java 8 java.util.Collections javadoc


5
我认为使用“不可修改的”就足够了。这样就无法进行写操作,这是多线程访问时会出现问题的原因。由于它是只读的,所以我认为同步的额外步骤似乎是不必要的。
最好在像这样有疑问的情况下查看源代码。看起来它返回一个“UnmodifiableList”。
/**
 * @serial include
 */
static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                  implements List<E> {
    static final long serialVersionUID = -283967356065247728L;
final List<? extends E> list;

UnmodifiableList(List<? extends E> list) {
    super(list);
    this.list = list;
}

public boolean equals(Object o) {return o == this || list.equals(o);}
public int hashCode()       {return list.hashCode();}

public E get(int index) {return list.get(index);}
public E set(int index, E element) {
    throw new UnsupportedOperationException();
    }
public void add(int index, E element) {
    throw new UnsupportedOperationException();
    }
public E remove(int index) {
    throw new UnsupportedOperationException();
    }
public int indexOf(Object o)            {return list.indexOf(o);}
public int lastIndexOf(Object o)        {return list.lastIndexOf(o);}
public boolean addAll(int index, Collection<? extends E> c) {
    throw new UnsupportedOperationException();
    }
public ListIterator<E> listIterator()   {return listIterator(0);}

public ListIterator<E> listIterator(final int index) {
    return new ListIterator<E>() {
    ListIterator<? extends E> i = list.listIterator(index);

    public boolean hasNext()     {return i.hasNext();}
    public E next()          {return i.next();}
    public boolean hasPrevious() {return i.hasPrevious();}
    public E previous()      {return i.previous();}
    public int nextIndex()       {return i.nextIndex();}
    public int previousIndex()   {return i.previousIndex();}

    public void remove() {
        throw new UnsupportedOperationException();
            }
    public void set(E e) {
        throw new UnsupportedOperationException();
            }
    public void add(E e) {
        throw new UnsupportedOperationException();
            }
    };
}

但是不可修改的只是返回一个视图,获取此视图的线程无法通过它修改列表,是吗?或者我理解错了? - zw324
3
消费者将无法修改列表,但如果保留原始列表,则可对其进行修改。 - Ray
1
没错,只要没有人修改“原始”(未包装)集合。最好将其限制为您自己构建的集合。一旦使用不可修改的内容包装它,就会失去对原始内容的引用,这也是一个好主意。更安全的方法是使用Ray答案中提到的不可变集合,因为没有可变集合会意外泄漏出来。 - Laurence Gonsalves

3

copyOf

这是Java 10及更高版本内置的方法。

每个方法都返回一个独立的集合,其中包含原始集合中的对象。返回的集合与 Collections.unmodifiable… 实用程序类方法不同,它们不是对原始集合的视图。

copyOf 方法复制的是引用(指针),而不是对象本身,因此涉及的内存不多。


从内存使用的角度来看,这不是太昂贵了吗? - Amir Hossain
@AmirHossain copyOf 方法复制的是引用(指针),而不是对象本身。因此,不会涉及太多内存。 - Basil Bourque

1
Java 9+的不可变集合是线程安全的。例如,List.of, Map.of, Map.copyOf(Java 10+)...根据Oracle文档,一个不可变集合的优点就是它自动保证了线程安全性。在创建集合后,您可以将其交给多个线程使用,并且它们都会看到一致的视图。阅读更多信息:oracle docs

0

这些视图不会为您返回真正的线程安全集合。始终存在某人修改支持集合或集合内元素的可能性。

要解决这个问题,您需要使用不可变集合和不可变元素。然后,线程安全就会随之而来。

Clojure包含这样的不可变(或持久)集合

简单地说,添加或删除新元素将返回一个新的集合,该集合通常通过对Trie类型数据结构的巧妙使用重用旧集合的大部分内容。

单独使用这些集合在纯Java中并不适合。

Pure4j是将这些集合(以及Clojure提倡的不可变/基于值的风格)移植到Java语言的尝试。这可能是您想要的。

免责声明:我是Pure4J的开发人员


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接