tl;dr
The objects stored in a thread-safe collection can be leaked outside and used in a non-thread-safe manner.
Detailed Answer:
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.
Consider the following code that uses two threads to add to the same non-thread-safe list the elements from 0 to 10. In the end, the main thread sums all the elements of that list. The final result should be same as 0 + 0 + 1 + 1 + ... 9 + 9 = 90. However, if you execute the code a couple of times you get different values, and sometimes even the following NPE:
Exception in thread "main" java.lang.NullPointerException
at java.base/java.util.stream.ReduceOps$1ReducingSink.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)
All this is the result of the race-condition during the call of the method 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));
}
Let us fix the race-condition by using a thread-Safe List by calling Collections.synchronizedList. So let us adapt the previous code to:
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
You can run it as many times as you want; the final result is always the same i.e., 90. That much we knew already. Let us showcase the:
It's worth mentioning that synchronized and concurrent collections
only make the collection itself thread-safe and not the contents.
You just need to adapt the previous code from:
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
to:
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);
...
and voilá! you have exactly the same situation as the first example that we have showcased (i.e., race-condition). Even though the list LIST_THREAD_SAFE is thread-safe its content is not. Hence,
synchronized and concurrent collections only make the collection
itself thread-safe and not the contents.