Java CopyOnWriteArraySet: Thread-Safe Set With Examples

Java CopyOnWriteArraySet explained with examples, snapshot iteration, O(n) costs, a chat room use case and a comparison with ConcurrentHashMap.newKeySet().

Flow of CopyOnWriteArraySet add() scanning the array, rejecting duplicates and copying the array for new elements

The CopyOnWriteArraySet class is a thread-safe Set from java.util.concurrent that stores its elements in a CopyOnWriteArrayList and copies the internal array on every change. It rejects duplicates, keeps insertion order, and gives every iterator a snapshot that never throws ConcurrentModificationException.

We use CopyOnWriteArraySet for small sets that many threads read often and change rarely, such as the members of a chat room, registered callbacks or a list of enabled feature flags.

The following example shows the operations of CopyOnWriteArraySet that the article covers, with each result as a comment.

CopyOnWriteArraySet<String> members = new CopyOnWriteArraySet<>();
boolean added = members.add("alice");                // true
boolean addedAgain = members.add("alice");           // false, duplicate
members.add("bob");
boolean hasBob = members.contains("bob");            // true, linear scan
boolean removed = members.remove("alice");           // true, copies the array
Set<String> current = members;                       // [bob]

Notice that every change returns a boolean that says whether the set changed. We look at how the set stores elements, its methods, the snapshot iterator, a chat room example, and how it compares with ConcurrentHashMap.newKeySet() and the other thread-safe sets.

1. How CopyOnWriteArraySet Stores Elements

The class has no hash table and no tree. It wraps a CopyOnWriteArrayList, so the elements sit in one array in the order they were added. Uniqueness comes from equals(), which add() calls on each existing element before it adds a new one.

Flow of CopyOnWriteArraySet add() scanning the array, rejecting duplicates and copying the array for new elements
The add() method scans the whole array with equals(), and only a new element causes an array copy

The steps of each operation follow from that design.

  • The add() method scans the array. If an equal element exists, the set stays unchanged and add() returns false. Otherwise it copies the array with the new element at the end and returns true.
  • The remove() method copies the array without the removed element, and the new array replaces the old one.
  • The contains() method scans the array from the start, which takes O(n) time, unlike the O(1) lookup of a HashSet.
  • An iterator reads the array that existed when the iterator was created, and later changes never reach it.

Since add() and contains() scan the whole array and every change copies it, CopyOnWriteArraySet suits sets with a few dozen or a few hundred elements, not large data sets.

2. Creating a CopyOnWriteArraySet

The class has two constructors, an empty one and one that copies another collection. The copy constructor drops duplicates and keeps the first occurrence of each element in the source order.

CopyOnWriteArraySet<String> empty = new CopyOnWriteArraySet<>();                             // []
CopyOnWriteArraySet<String> flags = new CopyOnWriteArraySet<>(List.of("dark", "beta", "dark")); // [dark, beta]

We declare the variable as CopyOnWriteArraySet or as Set, depending on whether the code needs to show readers that the set is shared between threads. The class belongs to the java.util.concurrent package, so the import is java.util.concurrent.CopyOnWriteArraySet.

3. Common Methods

The class implements the regular Set methods and adds nothing of its own. The costs differ from a HashSet, which matters when we pick it.

MethodWhat it doesCost
add(E e)Adds the element if no equal element existsO(n) scan, plus an array copy when added
addAll(Collection c)Adds every element that is not present yetOne array copy for the whole batch
remove(Object o)Removes the element if presentO(n), plus an array copy
removeIf(Predicate p)Removes all matching elementsOne array copy
contains(Object o)Checks if an equal element existsO(n) scan, no lock
size(), isEmpty()Reads the array lengthO(1), no lock
iterator()Returns a snapshot iterator in insertion orderO(1) to create, no copy
clear()Replaces the array with an empty oneO(1)

4. CopyOnWriteArraySet Example

The following example builds a set of notification channels, adds a batch of channels with one call and removes the channels that match a condition. Because addAll() and removeIf() are single writes, each of them copies the array once.

CopyOnWriteArraySet<String> channels = new CopyOnWriteArraySet<>(List.of("email"));
boolean changed = channels.addAll(List.of("sms", "email", "push")); // true
Set<String> afterAdd = channels;                                    // [email, sms, push]
boolean dropped = channels.removeIf(c -> c.startsWith("s"));        // true
Set<String> afterRemove = channels;                                 // [email, push]
int size = channels.size();                                         // 2

We can see that addAll() skipped the second “email”, and that the remaining elements keep the order in which they were first added.

5. No ConcurrentModificationException During Iteration

An iterator of CopyOnWriteArraySet keeps a reference to the array it started with. Other threads can add or remove elements while it runs, and the iterator neither sees those changes nor throws an exception. In return, the iterator does not support remove().

The following example takes an iterator, lets a second thread change the set, waits for that thread with join(), and reads the iterator afterwards.

CopyOnWriteArraySet<Integer> ids = new CopyOnWriteArraySet<>(List.of(1, 2, 3));
Iterator<Integer> before = ids.iterator();
Thread writer = new Thread(() -> {
    ids.add(4);
    ids.remove(1);
});
writer.start();
writer.join();
List<Integer> seen = new ArrayList<>();
before.forEachRemaining(seen::add);
List<Integer> snapshot = seen;                       // [1, 2, 3]
Set<Integer> latest = ids;                           // [2, 3, 4]

The iterator still returns 1, 2 and 3, because it reads the array from before the writer thread ran. A new iterator, or printing the set, shows the current content. Calling remove() on the iterator fails, so we remove through the set itself.

CopyOnWriteArraySet<Integer> codes = new CopyOnWriteArraySet<>(List.of(7, 8));
Iterator<Integer> codeIt = codes.iterator();
Integer firstCode = codeIt.next();                   // 7
codeIt.remove();                                     // UnsupportedOperationException
boolean gone = codes.remove(firstCode);              // true

6. When to Use CopyOnWriteArraySet

The set is a good fit when it stays small and when reads and iterations far outnumber changes. A chat room is a good example. Members join and leave a few times per minute, while every message is sent to all members, which can happen hundreds of times per second.

The following example models a chat room. A member is a record, so two Member objects with the same name are equal, and a second join with the same name does nothing.

record Member(String name) {}
class ChatRoom {
    private final Set<Member> members = new CopyOnWriteArraySet<>();

    boolean join(Member m) {
        return members.add(m);
    }

    boolean leave(Member m) {
        return members.remove(m);
    }

    List<String> broadcast(String text) {
        return members.stream()
                .map(m -> m.name() + " <- " + text)
                .toList();
    }
}
ChatRoom room = new ChatRoom();
boolean joined = room.join(new Member("ana"));       // true
boolean joinedTwice = room.join(new Member("ana"));  // false
room.join(new Member("raj"));
List<String> sent = room.broadcast("hi");            // [ana <- hi, raj <- hi]

The broadcast() method needs no synchronized block, and a member who leaves during a broadcast does not break the stream, because the stream also runs over a snapshot. A record generates equals() and hashCode() from its components, and a set needs both to detect duplicates.

For write-heavy sets, or sets with thousands of elements, the copying and the linear scans cost too much. In those cases we use a hash-based concurrent set, as the next section shows.

7. Comparison with Other Thread-Safe Sets

Java has four common ways to get a thread-safe set, and each one makes a different trade-off between read cost, write cost and ordering.

FeatureCopyOnWriteArraySetConcurrentHashMap.newKeySet()Collections.synchronizedSet()ConcurrentSkipListSet
OrderInsertion orderNo orderOrder of the wrapped setSorted
contains()O(n), no lockO(1), no lockO(1) for a HashSet, one shared lockO(log n), no lock
add() / remove()O(n) and an array copyO(1)O(1), one shared lockO(log n)
Iteration while others writeSnapshot, never failsWeakly consistent, never failsMust lock by hand, else ConcurrentModificationExceptionWeakly consistent, never fails
null elementsAllowedNot allowedDepends on the wrapped setNot allowed
Best forSmall, read-mostly setsLarge sets with many writesSimple cases with low contentionSorted concurrent data

For most large shared sets, ConcurrentHashMap.newKeySet() is the default choice, and the ConcurrentMap article explains the map behind it. A plain HashSet stays the right type when only one thread uses the set.

8. CopyOnWriteArraySet FAQs

Developers ask these questions most often when they pick a thread-safe set.

8.1. Does CopyOnWriteArraySet keep insertion order?

Yes. Its iterator returns elements in the order in which they were added, because they are stored in one array. A re-added element that was removed earlier goes to the end.

8.2. Does CopyOnWriteArraySet allow null?

Yes, it allows one null element, like a HashSet. The hash-based ConcurrentHashMap.newKeySet() and the sorted ConcurrentSkipListSet reject null with a NullPointerException.

8.3. Is CopyOnWriteArraySet sorted?

No. It keeps insertion order, not sorted order. For a sorted thread-safe set, we use ConcurrentSkipListSet, and for a sorted set in one thread, a TreeSet.

8.4. What is the difference between CopyOnWriteArraySet and CopyOnWriteArrayList?

The set rejects duplicates and has no index-based methods such as get(int). Internally it calls addIfAbsent() on a CopyOnWriteArrayList, so both share the same snapshot iterators and the same copy cost on writes.

9. Conclusion

The CopyOnWriteArraySet class is a thread-safe set backed by an array that is copied on every change. Reads and iterations take no lock and never fail, while add(), remove() and contains() scan the array in O(n) time.

We pick it for small, read-mostly sets such as chat room members or registered callbacks, where snapshot iteration and insertion order help. For large or write-heavy sets, ConcurrentHashMap.newKeySet() performs better, and the Java Collections guide lists the other options.

10. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.