Lesson 3
CopyOnWrite Collections
Welcome to CopyOnWrite Collections

Building on your foundational knowledge of synchronized and concurrent collections, you're now ready to explore CopyOnWrite collections in Java. This type of collection is particularly effective for scenarios involving frequent reads and infrequent writes, offering thread safety without the performance drawbacks of locking.

What You'll Learn

By the end of this lesson, you will understand:

  • What CopyOnWrite collections are and how they function.
  • Practical applications of CopyOnWriteArrayList and its benefits.
  • How to implement a thread-safe subscriber list using CopyOnWriteArrayList.

By mastering these concepts, you'll be equipped to handle situations that demand high efficiency in reading data while maintaining thread safety.

Understanding CopyOnWrite Collections

Copy-on-Write (CoW) collections, like CopyOnWriteArrayList, are designed to optimize performance in situations where read operations are much more frequent than write operations.

The core principle behind these collections is that every time a modification (such as adding or removing an element) occurs, a new copy of the entire internal array is created. This allows read operations to happen concurrently without synchronization or locking. Since reads don't alter the data, they safely access the original array while modifications occur on a new copy.

CopyOnWriteArrayList Design

The design of the CopyOnWriteArrayList ensures thread safety without requiring synchronization. Each time a method like add() or remove() is called, the current content of the list is copied into a new array. This ensures read consistency because readers use the old array, while modifications are done on the new one.

This approach is ideal for situations that are read-heavy with only occasional writes or updates.

Let's break down how to use CopyOnWriteArrayList with some common scenarios.

Creating and Adding Elements

Creating and adding elements to a CopyOnWriteArrayList is simple. Each time an element is added, a new internal array is created to include the added elements.

Java
1import java.util.List; 2import java.util.concurrent.CopyOnWriteArrayList; 3 4public class SubscriberList { 5 private List<String> subscribers = new CopyOnWriteArrayList<>(); 6 7 public void addSubscriber(String subscriber) { 8 subscribers.add(subscriber); 9 } 10 11 public static void main(String[] args) { 12 SubscriberList list = new SubscriberList(); 13 list.addSubscriber("subscriber1"); 14 list.addSubscriber("subscriber2"); 15 16 System.out.println(list.subscribers); 17 } 18}

In this snippet, we initialize the subscribers list using CopyOnWriteArrayList. This ensures thread safety, meaning multiple threads can read from the list concurrently without locking. When we call subscribers.add(subscriber);, a new array is created behind the scenes to include the new subscriber. This way, the old array remains unchanged for any ongoing read operations, which makes it optimal for read-heavy use cases.

Iterating over the List

Iterating over a CopyOnWriteArrayList is safe and easy. Modifications to the list create a new copy of the array, so iterations remain unaffected by changes.

Java
1public void sendNewsletter(String message) { 2 for (String subscriber : subscribers) { 3 System.out.println("Sending message to " + subscriber); 4 } 5}

In the sendNewsletter() method, we iterate over the subscribers list using a for-each loop. The iteration operates on a snapshot of the list as it was when the iteration began. Even if new elements are added to or removed from the list during this iteration, the changes won’t affect the ongoing iteration, because the iteration works on the original copy of the array. This guarantees safe iteration even in multi-threaded environments.

Adding Elements After Iteration Begins

You can safely add elements to the CopyOnWriteArrayList after starting an iteration. Let's take a look at how this works:

Java
1public static void main(String[] args) { 2 SubscriberList list = new SubscriberList(); 3 list.addSubscriber("subscriber1"); 4 list.addSubscriber("subscriber2"); 5 6 // Start iterating over the list 7 list.sendNewsletter("Welcome to our newsletter!"); 8 9 // Add a new subscriber after the iteration begins 10 list.addSubscriber("newSubscriber@example.com"); 11 12 // Send newsletter again to see the updated list 13 list.sendNewsletter("This is the second newsletter."); 14}

In this example, we iterate through the list to send a newsletter to all current subscribers. After the first newsletter is sent, a new subscriber is added to the list. However, the first iteration only includes the original subscribers. When we send the second newsletter, it includes the new subscriber as well. This demonstrates how iterations are isolated from modifications—the first iteration operates on a copy that doesn’t include the new subscriber, while the second iteration works on the updated list.

Removing Elements During Iteration

While CopyOnWriteArrayList allows you to add elements safely during iteration, removing elements during iteration will throw an UnsupportedOperationException.

Java
1public void sendNewsletter(String message) { 2 for (String subscriber : subscribers) { 3 System.out.println("Sending message to " + subscriber); 4 subscribers.remove(subscriber); // This will throw UnsupportedOperationException 5 } 6}

In this code, trying to remove an element from the list during iteration will result in an UnsupportedOperationException. This happens because the CopyOnWriteArrayList disallows modifications (like removing elements) during iteration to prevent unexpected behavior. The iterator holds a snapshot of the list at the time of its creation, and modifying that snapshot directly isn't supported.

Why CopyOnWrite Collections Matter

Understanding CopyOnWrite collections will help you write efficient and safe concurrent programs. Here’s why they’re important:

  • Thread Safety without Synchronization: By copying data for write operations, CopyOnWriteArrayList ensures thread safety without the need for locks, simplifying multi-threaded code.

  • Ideal for Read-Heavy Workloads: These collections are optimized for environments where reads far outnumber writes, making them perfect for maintaining subscriber lists or configuration settings.

  • Cleaner Concurrent Code: CopyOnWrite collections eliminate the need for manual synchronization, leading to cleaner and more maintainable code.

In conclusion, CopyOnWrite collections provide a simple yet powerful way to manage shared data in read-heavy, multi-threaded environments. Mastering this will help you build efficient and scalable concurrent applications. In the next exercises, you'll have the opportunity to practice implementing these principles to solidify your understanding.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.