Thread-Safe List in Java

A regular ArrayList breaks when several threads change it at the same time. Items silently go missing, or the program crashes. This guide shows the problem with a runnable example, then compares the ways to get a thread-safe list in Java and when to use each one.

What does "thread-safe" mean?

A thread is an independent path of execution. Modern programs often run many threads at once, for example to handle several web requests in parallel. Code is thread-safe if it still works correctly when multiple threads use it at the same time.

Picture two people editing the same paper shopping list at the same moment. Both read "3 items", both write item #4 in the same spot, and one item is lost. That's a race condition.

Why isn't ArrayList thread-safe?

Adding to an ArrayList is several steps: check the size, maybe grow the array, store the item, increase the size. If two threads interleave those steps, they can overwrite each other's work. Try it:

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

public class RaceDemo {
    public static void main(String[] args) throws InterruptedException {
        List<Integer> list = new ArrayList<>();

        Runnable addMany = () -> {
            for (int i = 0; i < 10_000; i++) {
                list.add(i);
            }
        };

        Thread t1 = new Thread(addMany);
        Thread t2 = new Thread(addMany);
        t1.start(); t2.start();      // run both at the same time
        t1.join();  t2.join();       // wait for both to finish

        System.out.println("Expected 20000, got " + list.size());
    }
}
Expected 20000, got 12129

That was one real run on a laptop. Five more runs gave 11908, 12256, 12379, 12523, and one crash.

The exact number changes every run, and sometimes the program throws an ArrayIndexOutOfBoundsException instead. Bugs like this are dangerous precisely because they're random: the code can pass every test on your laptop and fail in production.

What are the thread-safe list options?

Compare thread-safe lists

synchronizedList

Wraps an ArrayList so only one thread uses it at a time. Simple and a good general-purpose choice when writes are frequent.

  • ✔ Every method call is safe
  • ✘ You must lock manually while iterating
  • ✘ Threads wait for each other under heavy use
List<String> list = Collections.synchronizedList(new ArrayList<>());

CopyOnWriteArrayList

Copies the whole array on every change. Readers never wait and iteration never fails.

  • ✔ Reads and loops need no locking
  • ✔ Iterators see a snapshot, so no ConcurrentModificationException
  • ✘ Writes are expensive on big lists
List<String> listeners = new CopyOnWriteArrayList<>();

List.of / copyOf

A list that can never change is automatically thread-safe. The simplest option, if it fits.

  • ✔ No locking at all
  • ✔ Fast and memory-efficient
  • ✘ Can't add or remove; build a new list instead
List<String> countries = List.of("LK", "IN", "US");
List<String> snapshot = List.copyOf(mutableList);

Vector (legacy)

Java 1.0 class with every method synchronized. Still works but isn't recommended for new code.

  • ✘ Same manual-iteration problem as synchronizedList
  • ✘ Considered outdated; prefer the options above
List<String> old = new Vector<>();   // avoid in new code
Rule of thumb: if the list never changes, use List.of. If it's read much more often than changed, use CopyOnWriteArrayList. Otherwise use Collections.synchronizedList.

Option 1: Collections.synchronizedList

Wrap any list, and each method call becomes safe. Fixing our demo is a one-line change:

List<Integer> list = Collections.synchronizedList(new ArrayList<>());
// ...same two threads as before...
Expected 20000, got 20000

The catch: iteration. A loop is many separate calls, so another thread could change the list halfway through. The Java documentation requires you to lock the list yourself while looping:

synchronized (list) {             // hold the lock for the whole loop
    for (Integer n : list) {
        System.out.println(n);
    }
}

What does synchronized (list) do?

It makes the thread take list's lock before entering the block. Only one thread can hold a lock at a time, and the synchronized list's own methods use the same lock, so no other thread can change the list until the loop finishes.

Option 2: CopyOnWriteArrayList

From java.util.concurrent. Every change makes a fresh copy of the internal array, so readers always see a complete, unchanging snapshot. Loops need no locking and never throw ConcurrentModificationException:

List<String> names = new CopyOnWriteArrayList<>(List.of("Asha", "Ben"));

for (String n : names) {
    names.add(n + "!");       // safe: the loop keeps reading its original snapshot
}
System.out.println(names);
[Asha, Ben, Asha!, Ben!]

Notice the loop didn't run forever, even though we kept adding. It iterated over the snapshot from when the loop started. Ideal for things like a list of event listeners: read on every event, changed only occasionally.

Option 3: don't share changeable lists at all

The safest shared list is one that never changes. List.of(...) and List.copyOf(...) create unmodifiable lists that any number of threads can read with zero locking. To "change" one, build a new list and publish that instead.

Thread-safe lists compared

synchronizedListCopyOnWriteArrayListList.of / copyOf
ReadsLocked (threads may wait)Lock-free, fastLock-free, fast
WritesLocked, cheapCopies the whole arrayNot allowed
Safe iterationOnly inside synchronized (list)✔ Always (snapshot)✔ Always
Best forFrequent writesMany reads, few writesData that doesn't change

Is a list even the right tool? If threads hand work to each other (producer and consumer), a BlockingQueue such as LinkedBlockingQueue is designed for exactly that. For shared key-value data, use ConcurrentHashMap. See the Collections Framework overview.

Quick recap

  • ArrayList isn't thread-safe; concurrent changes lose data or throw exceptions, at random.
  • Collections.synchronizedList makes each call safe, but lock it yourself while iterating.
  • CopyOnWriteArrayList gives lock-free reads and safe iteration, but costly writes.
  • Immutable lists (List.of) are thread-safe for free.
  • Avoid Vector in new code.

What's next?

You've finished the core Java series. Put it to work with real-world tools: explore Java web frameworks or Java persistence frameworks, or go back to the Java overview.

Frequently Asked Questions

Is ArrayList thread-safe in Java?

No. If two or more threads change the same ArrayList at the same time, items can be lost, the list can end up in a broken state, or you can get exceptions. Reading from several threads is fine only if no thread is changing the list.

What is the best thread-safe list in Java?

It depends on the workload. CopyOnWriteArrayList is best when reads and iteration are frequent and changes are rare, such as a list of listeners. Collections.synchronizedList suits lists that change often. If the list never changes, an immutable List.of(...) is the simplest and fastest.

Do I need to synchronize when iterating a synchronizedList?

Yes. Each individual method call is synchronized, but a loop is many calls. Wrap the loop in synchronized (list) { ... } so no other thread can change the list mid-iteration, otherwise you may get ConcurrentModificationException.

Should I use Vector for thread safety?

No, not in new code. Vector is a legacy class from Java 1.0 whose methods are synchronized, but it has the same iteration problem as synchronizedList and is considered outdated. Use Collections.synchronizedList or CopyOnWriteArrayList instead.

Why is CopyOnWriteArrayList slow for writes?

Every add, set, or remove copies the entire internal array. That makes reads lock-free and iteration completely safe, but writes get more expensive as the list grows. It's a great fit for small lists that are read far more often than they're changed.