Fundamentals 16 min read

Understanding Thread Safety Issues in Java Collections: ArrayList, HashSet, HashMap and Their Solutions

This article explains why core Java collection classes such as ArrayList, HashSet, and HashMap are not thread‑safe, demonstrates the underlying mechanisms of their initialization and resizing, and presents multiple approaches—including Vector, synchronized wrappers, and CopyOnWrite variants—to achieve safe concurrent access.

Wukong Talks Architecture
Wukong Talks Architecture
Wukong Talks Architecture
Understanding Thread Safety Issues in Java Collections: ArrayList, HashSet, HashMap and Their Solutions

The article analyzes the thread‑safety problems of common Java collection classes and provides source‑level explanations and code examples.

1. Thread‑unsafe ArrayList

ArrayList belongs to the java.util collection framework. Its internal structure consists of an array elementData and a size counter. When a new ArrayList<Integer>() is created, the array is initialized as an empty array ( DEFAULTCAPACITY_EMPTY_ELEMENTDATA ), not with the documented capacity of 10.

new ArrayList<Integer>();

During the first add operation, ensureCapacityInternal calculates the required capacity (10) and triggers the first growth from 0 to 10 using grow() and Arrays.copyOf . Subsequent additions increase the size until the capacity is exceeded, at which point the array grows by 1.5× (e.g., from 10 to 15, then to 22, etc.).

public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

In a single‑threaded environment the operations appear safe, but they are not atomic: the increment of size and the assignment to elementData are separate steps, leading to possible race conditions.

Single‑thread safety demonstration

A custom BuildingBlockWithName class is used to add five elements to an ArrayList . The resulting order matches the insertion order, confirming correct behavior when only one thread accesses the list.

class BuildingBlockWithName {
    String shape;
    String name;
    public BuildingBlockWithName(String shape, String name) {
        this.shape = shape;
        this.name = name;
    }
    @Override
    public String toString() {
        return "BuildingBlockWithName{" + "shape='" + shape + ",name=" + name + '}';
    }
}
ArrayList
arrayList = new ArrayList<>();
arrayList.add(new BuildingBlockWithName("三角形", "A"));
arrayList.add(new BuildingBlockWithName("四边形", "B"));
// ...

Multi‑threaded failure

When 20 threads concurrently add random blocks to the same ArrayList , a java.util.ConcurrentModificationException is often thrown because the internal array is resized without proper synchronization.

Exception in thread "10" java.util.ConcurrentModificationException
Exception in thread "13" java.util.ConcurrentModificationException

Solutions for ArrayList

Replace ArrayList with Vector (methods are synchronized).

Wrap the list with Collections.synchronizedList(new ArrayList<>()) .

Use CopyOnWriteArrayList , which copies the array on each write.

2. Thread‑unsafe HashSet

HashSet is backed by a HashMap . Its add method stores the element as a key with a constant dummy value ( PRESENT ), so the underlying map’s put is not synchronized.

private static final Object PRESENT = new Object();
public boolean add(E e) {
    return map.put(e, PRESENT) == null;
}

Because the add operation lacks atomicity, concurrent modifications can corrupt the set.

Thread‑safe alternatives

Collections.synchronizedSet(new HashSet<>())

CopyOnWriteArraySet (internally uses CopyOnWriteArrayList )

3. Thread‑unsafe HashMap

Like HashSet, HashMap is not safe for concurrent writes. Adding entries from multiple threads can cause data loss or infinite loops.

Map
map = new HashMap<>();
map.put("A", new BuildingBlockWithName("三角形", "A"));

Thread‑safe alternatives

Collections.synchronizedMap(new HashMap<>())

ConcurrentHashMap , which partitions the map into segments and locks only the affected segment during updates.

4. Comparison of Lock Mechanisms

The article also compares ReentrantLock and the synchronized keyword:

Both provide mutual exclusion and are re‑entrant.

ReentrantLock offers explicit lock/unlock, interruptibility, fairness policies, and multiple Condition objects.

synchronized is simpler, managed by the JVM, and automatically releases the lock on exceptions.

Conclusion

Core Java collections such as ArrayList , HashSet , and HashMap are not thread‑safe by default. To use them safely in concurrent programs, developers can choose synchronized wrappers, the legacy Vector , or modern copy‑on‑write classes, while understanding the performance trade‑offs of each approach.

JavaconcurrencyThread SafetyHashMapArrayListHashSet
Wukong Talks Architecture
Written by

Wukong Talks Architecture

Explaining distributed systems and architecture through stories. Author of the "JVM Performance Tuning in Practice" column, open-source author of "Spring Cloud in Practice PassJava", and independently developed a PMP practice quiz mini-program.

0 followers
Reader feedback

How this landed with the community

login 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.