同步和并发集合如何是线程安全的但它们的内容不是

How Synchronized and Concurrent Collections are thread-safe but their content not

从这个source可以读到:

It's worth mentioning that synchronized and concurrent collections only make the collection itself thread-safe and not the contents.

我认为如果 Collection 是线程安全的,那么它的内容将隐含地是线程安全的。

我的意思是,如果两个线程无法访问我的 Collection 对象,那么我的 Collection 对象持有的对象将隐式成为线程安全的。

我没抓住要点,有人可以举例说明一下吗?

对元素的访问是thread-safe,而不是对它们的使用。两个线程可以访问同一个元素(一个接一个),并且都在之后调用元素上的方法。 collection 甚至不知道。

如果集合是线程安全的,两个线程仍然可以同时访问集合。如果这不影响集合的连贯性,他们也可以同时进行修改。只保证所有的操作都可能与最少的锁数量保持一致。 这与集合中的对象无关:一个线程可以从集合中获取对象的引用并保留一定时间,因此其他线程可以从集合中获取相同的引用(同时或稍后) 并同时访问对象。

tl;博士

存储在线程安全集合中的对象可以泄漏到外部并以非线程安全方式使用。

详细答案:

I thought if Collection is thread-safe then its content will implicitly be thread-safe, I mean if two threads cannot access my Collection object then the object which my Collection object is holding will implicitly become thread-safe.

I know sure I missing the point, could someone please explain me with an example.

考虑以下使用两个线程将 0 to 10 中的元素添加到同一个 非线程安全列表 的代码。最后,main 线程对该列表的所有元素求和。最终结果应该与 0 + 0 + 1 + 1 + ... 9 + 9 = 90. 相同,但是,如果多次执行代码,您会得到不同的值,有时甚至会出现以下 NPE:

Exception in thread "main" java.lang.NullPointerException
    at java.base/java.util.stream.ReduceOpsReducingSink.accept(ReduceOps.java:80)
    at java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1655)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.reduce(ReferencePipeline.java:553)
    at Z.CollectionThreadSafe.main(CollectionThreadSafe.java:26)

这一切都是调用方法add时竞争条件的结果。

private static void addToList(List<Integer> list) {
    for (int i = 0; i < 10; i++)
        list.add(i);
}

public static void main(String[] arg) throws InterruptedException {
    final int TOTAL_THREADS = 2;
    List<Integer> list = new ArrayList<>();
    ExecutorService pool = Executors.newFixedThreadPool(TOTAL_THREADS);
    for (int i = 0; i < TOTAL_THREADS; i++) {
        pool.submit(() -> addToList(list));
    }
    pool.shutdown();
    pool.awaitTermination(10, TimeUnit.SECONDS);
    System.out.println(list.stream().reduce(0, Integer::sum));
}

让我们通过调用 Collections.synchronizedList 使用线程安全 List 来修复竞争条件。因此,让我们将之前的代码调整为:

List<Integer> list = Collections.synchronizedList(new ArrayList<>());

您可以 运行 多次;最终结果总是相同的90。我们已经知道了这么多。让我们展示一下:

It's worth mentioning that synchronized and concurrent collections only make the collection itself thread-safe and not the contents.

您只需要修改之前的代码:

List<Integer> list = Collections.synchronizedList(new ArrayList<>());

至:

final List<List<Integer>> LIST_THREAD_SAFE = Collections.synchronizedList(new ArrayList<>());
LIST_THREAD_SAFE.add(new ArrayList<>());
List<Integer> list = LIST_THREAD_SAFE.get(0);
...

voilá! 你遇到的情况与我们展示的第一个示例完全相同(即, 竞争条件)。尽管列表 LIST_THREAD_SAFE 是线程安全的,但它的内容不是。因此,

synchronized and concurrent collections only make the collection itself thread-safe and not the contents.