Fundamentals 8 min read

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.

FunTester
FunTester
FunTester
Why Thread‑Safe Collections Still Fail with Non‑Thread‑Safe Objects in Java

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javaconcurrencythread safetyCollectionsConcurrentHashMapVectorCopyOnWriteArrayList
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.