Backend Development 11 min read

Understanding Thread Safety Issues in Java ArrayList and How to Fix Them

This article explains why Java's ArrayList is not thread‑safe, illustrates the two main concurrency problems—array index out‑of‑bounds and element overwriting—through source‑code analysis and multithreaded examples, and presents several practical solutions such as synchronized wrappers, explicit locking, CopyOnWriteArrayList and ThreadLocal.

Top Architect
Top Architect
Top Architect
Understanding Thread Safety Issues in Java ArrayList and How to Fix Them

1. Source Code Analysis

ArrayList is widely known to be non‑thread‑safe, but the exact reasons and manifestations are often unclear; this section examines the implementation.

add Method Source

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return true (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    // Check if the internal array has enough capacity; resize if necessary
    ensureCapacityInternal(size + 1); // Increments modCount!!
    // Store the element in the internal array
    elementData[size++] = e;
    return true;
}

The implementation uses an internal Object[] array ( elementData ) and a size field to track the number of stored elements.

Key Fields and Helper Methods

/** Default initial capacity. */
private static final int DEFAULT_CAPACITY = 10;

/** Internal storage array. */
transient Object[] elementData;

/** Current number of elements. */
private int size;

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

From the source we can see that add performs two main steps: capacity check (which may trigger a resize) and element insertion.

2. Two Manifestations of Non‑Thread‑Safety

2.1 ArrayIndexOutOfBoundsException

Because the capacity check and the actual insertion are not atomic, concurrent add calls can cause the internal array to be accessed out of bounds.

Assume the list size is 9 (size = 9).

Thread A reads size = 9 and calls ensureCapacityInternal .

Thread B does the same and also sees size = 9.

Thread A determines that a capacity of 10 is sufficient and returns without resizing.

Thread B makes the same determination.

Thread A executes elementData[size++] = e , increasing size to 10.

Thread B then tries to write to elementData[10] , but the array length is still 10 (last valid index 9), resulting in ArrayIndexOutOfBoundsException .

2.2 Element Overwrite and Null Gaps

The statement elementData[size++] = e is actually two operations: writing the element and then incrementing size . In a multithreaded scenario these can interleave, causing one thread to overwrite another's value and leaving gaps (null entries) in the list.

elementData[size] = e;
size = size + 1;

Initial size = 0.

Thread A writes element A to index 0.

Thread B, seeing size still 0, also writes element B to index 0.

Thread A increments size to 1.

Thread B increments size to 2.

After both threads finish, size is 2, but index 0 holds B, index 1 is null, and the original A is lost.

3. Code Demonstration

The following program creates two threads that concurrently add numbers to a shared ArrayList , reproducing the two problems described above.

import java.util.ArrayList;
import java.util.List;

public class ArrayListSafeTest {
    public static void main(String[] args) throws InterruptedException {
        final List
list = new ArrayList<>();
        // Thread A adds 1‑999
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 1000; i++) {
                    list.add(i);
                    try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            }
        }).start();
        // Thread B adds 1001‑1999
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1001; i < 2000; i++) {
                    list.add(i);
                    try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
                }
            }
        }).start();
        Thread.sleep(1000);
        // Print results
        for (int i = 0; i < list.size(); i++) {
            System.out.println("Element " + (i + 1) + ": " + list.get(i));
        }
    }
}

Running the program typically yields either an ArrayIndexOutOfBoundsException or a list with missing/overwritten elements, as shown in the screenshots below.

4. Making ArrayList Thread‑Safe

4.1 Collections.synchronizedList

The most common approach is to wrap the list with Collections.synchronizedList :

List
list = Collections.synchronizedList(new ArrayList
());

4.2 Explicit Synchronization

synchronized(list) { list.add(model); }

4.3 CopyOnWriteArrayList

Replace ArrayList with the thread‑safe CopyOnWriteArrayList :

List
list1 = new CopyOnWriteArrayList<>();

4.4 ThreadLocal

Use a ThreadLocal variable to give each thread its own list instance, ensuring thread confinement:

ThreadLocal
> threadList = new ThreadLocal
>() {
    @Override
    protected List
initialValue() {
        return new ArrayList
();
    }
};

These techniques eliminate the race conditions demonstrated earlier.

Note: The article also contains promotional material for a WeChat public account and a developer community, which is unrelated to the technical content.

JavaConcurrencyThread SafetyCollectionsthreadlocalArrayListcopyonwritearraylist
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.