You are becoming a more experienced developer and conquering the peaks of multithreading. And today, our guest is the CopyOnWriteArrayList class, the member of the java.util.concurrent package. Look closely at the name: ArrayList plus the copy-on-write technique. Together, they give you a thread-safe representation of the ArrayList class.

CopyOnWriteArrayList

Note that it’s an “old class” — it has existed since Java 5.

The creation of CopyOnWriteArrayList looks the same as with ArrayList:

CopyOnWriteArrayList<String> onWriteArrayList = new CopyOnWriteArrayList<>();

Now let’s look at how its methods allow thread safety. Imagine that our CopyOnWriteArrayList looks like this:

And we would like to add a new element to the end of this list:

onWriteArrayList.add("List!"); 

These are the steps it goes through for it to happen:

First, the lock is set. Second, a copy of our list will be created. Third, the copy is updated with a new element. Then, our list becomes an “updated copy”. And the final step is unlock. This technique is called Copy-On-Write and it ensures thread safety.

All mutative operations (addsetremove etc.) use the copy-on-write technique: they create a cloned copy of the original list. Because of this, performing many update operations can be very costly.

If you are curious about details, you should know that the underlying structure of CopyOnWriteArrayList is an array of Objects.

And now, ladies and gentlemen, the highlight of our program — two threads, but only one list.

Two threads, one list

Here is an example with two threads: main and writer. Both of them add numbers to the same CopyOnWriteArrayList.

public static void main(String[] args) throws InterruptedException {
    CopyOnWriteArrayList<Integer> onWriteArrayList = new CopyOnWriteArrayList<>();

    Thread writer = new Thread(() -> addNumbers(onWriteArrayList));
    writer.start();

    addNumbers(onWriteArrayList); // add numbers from the main thread

    writer.join(); // wait for writer thread to finish

    System.out.println(onWriteArrayList.size()); // the result is always the same
}

private static void addNumbers(CopyOnWriteArrayList<Integer> list) {
    for (int i = 0; i < 100_000; i++) {
        list.add(i);
    }
}

If you try it, the result is always 200_000.

But what if one thread added numbers, but the second thread removed them?

public static void main(String[] args) throws InterruptedException {
    CopyOnWriteArrayList<Integer> onWriteArrayList = new CopyOnWriteArrayList<>();


    Thread writer = new Thread(() -> addNumbers(onWriteArrayList));
    writer.start();

    removeNumbers(onWriteArrayList); // remove numbers from the main thread

    writer.join(); // wait for writer thread to finish

    System.out.println(onWriteArrayList.size()); // the result is always the same
}

private static void addNumbers(CopyOnWriteArrayList<Integer> list) {
    for (int i = 0; i < 100_000; i++) {
        list.add(i);
    }
}

private static void removeNumbers(CopyOnWriteArrayList<Integer> list) {
    int index = 0;
    while (index < 100_000) {
        if (!list.isEmpty()) {
            list.remove(0);
            index++;
        }
    }
}

In this example, we wanted to add 100_000 numbers and remove 100_000 numbers from the same list. Everything works fine thanks to the copy-on-write technique. Once one element is being added, the removing is paused and vice versa. But you can never guarantee that the selected index will exist at the moment. That’s why we were removing only the zero-index elements.

If you perform any read operation while updating the list, you will always get the “old” version of the list, that is, how your list looked before the update started.

What about an iterator?

Let’s consider a simple example:

CopyOnWriteArrayList<Integer> onWriteArrayList = new CopyOnWriteArrayList<>();
onWriteArrayList.add(1);
onWriteArrayList.add(2);
onWriteArrayList.add(3);

Iterator<Integer> iterator = onWriteArrayList.iterator();

onWriteArrayList.add(4);

while(iterator.hasNext()) {
    System.out.print(iterator.next() + " "); // we will see only "1 2 3"
}

Do you think we will see three or four elements? The answer is only three. Since the iterator has been created, it will use the immutable snapshot of the CopyOnWriteArrayList.

Also, because of the immutability, you can’t use iterator.remove().

CopyOnWriteArrayList allows thread-safe iterating over its elements when the underlying list gets modified by other threads.

Conclusion

CopyOnWriteArrayList comes to the rescue when you’d like to use ArrayList but the environment is multithreading.

What to remember:

  • It creates a new internal copy for every update operation (with the copy-on-write technique).
  • Read operations return the “old” version of the list while an update operation is happening.
  • An iterator uses the immutable snapshot of the CopyOnWriteArrayList.
  • It’s very costly to update CopyOnWriteArrayList often.

Leave a Reply

Your email address will not be published.