Why Thread‑Safe Collections Still Fail with Non‑Thread‑Safe Objects in Java
Through a series of Java demos, this article examines how storing non‑thread‑safe objects inside thread‑safe collections like ConcurrentHashMap or CopyOnWriteArrayList can still cause concurrency bugs, explains the underlying remove() implementation, and shows how switching to a thread‑safe Vector resolves the issue.
Background
The JDK includes built‑in thread‑safe collection classes such as CopyOnWriteArrayList and ConcurrentHashMap. They can be created directly or via the Collections utility:
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
Map<String, String> concMap = new ConcurrentHashMap<>();Problem
Even when a thread‑safe collection stores a non‑thread‑safe object, concurrent operations on that object remain unsafe. The following demo stores an ArrayList inside a ConcurrentHashMap and lets multiple threads remove elements from the list.
package com.fun;
import com.fun.base.constaint.ThreadLimitTimesCount;
import com.fun.frame.SourceCode;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.ArrayList;
class TSSS extends SourceCode {
static ConcurrentHashMap<Integer, List<Integer>> map = new ConcurrentHashMap<>();
static List<Integer> list = new ArrayList<>();
public static void main(String[] args) {
map.put(1, list);
// pre‑populate the list
for (int i = 0; i < 30; i++) {
list.add(4);
}
// start 5 threads, each removing element at index 3 five times
ThreadLimitTimesCount tt = new TT(5);
new com.fun.frame.excute.Concurrent(tt * 5).start();
System.out.println(map.get(1).size());
}
static class TT extends ThreadLimitTimesCount {
public TT(int time) { super(null, time, null); }
@Override
protected void doing() throws Exception { list.remove(3); }
public TT clone() { return new TT(times); }
}
}Running the program repeatedly prints a final size of 8, not the expected 5. This demonstrates that the collection’s thread‑safety does not protect the internal state of the stored ArrayList.
Root Cause
The ArrayList.remove(int) method is not synchronized. Its implementation updates size and shifts the backing array without atomic coordination:
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}When several threads invoke this method concurrently, they may read the same size and index values, copy the array, and then decrement size independently. The interleaving leads to lost updates and data corruption, which explains the unexpected size.
Solution
Replace the non‑thread‑safe list with a thread‑safe implementation. The demo below uses Vector, which synchronizes all its public methods. The same concurrent removal now yields a consistent final size of 5:
package com.fun;
import com.fun.base.constaint.ThreadLimitTimesCount;
import com.fun.frame.SourceCode;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.Vector;
class TSSS extends SourceCode {
static ConcurrentHashMap<Integer, List<Integer>> map = new ConcurrentHashMap<>();
static List<Integer> list = new Vector<>();
public static void main(String[] args) {
map.put(1, list);
for (int i = 0; i < 30; i++) {
list.add(4);
}
ThreadLimitTimesCount tt = new TT(5);
new com.fun.frame.excute.Concurrent(tt * 5).start();
System.out.println(map.get(1).size());
}
static class TT extends ThreadLimitTimesCount {
public TT(int time) { super(null, time, null); }
@Override
protected void doing() throws Exception { map.get(1).remove(3); }
public TT clone() { return new TT(times); }
}
}Other thread‑safe alternatives include CopyOnWriteArrayList or wrapping the list with Collections.synchronizedList. The key is that the stored object itself must provide safe concurrent semantics.
Conclusion
Thread‑safe collections guarantee safety only for the operations they implement. Objects placed inside them inherit no additional synchronization. To avoid race conditions, ensure that the elements themselves are thread‑safe—either by using synchronized classes (e.g., Vector, CopyOnWriteArrayList) or by external coordination such as explicit locks.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
