Lesson 3
Creating a Thread-Safe Channel with CopyOnWriteArrayList
Creating a Thread-Safe Messaging Channel with CopyOnWriteArrayList

Welcome back! In our last lesson, we explored the use of ConcurrentSkipListMap for a game leaderboard. Today, we're taking another step forward in concurrency by building a thread-safe communication channel using CopyOnWriteArrayList. This approach is particularly useful when you need to manage frequent reads and occasional writes, making it a perfect fit for messaging systems or notification services.

What You'll Learn

By the end of this lesson, you will:

  • Learn how to build a thread-safe messaging channel using concurrent collections.
  • Understand how to efficiently manage concurrent reads and writes in your application.
  • Implement a system where multiple threads can post and read messages without additional locking mechanisms.

These concepts will help you build highly responsive and thread-safe communication systems that are essential in real-world applications like live feeds or notification systems.

Recap: Thread-Safe Collections

Before we dive into the implementation, let's quickly revisit CopyOnWriteArrayList. This collection is a thread-safe variant of ArrayList where each modification (like adding or removing an item) creates a new copy of the underlying array. It shines in scenarios where reads vastly outnumber writes because reads occur directly on the array, while writes involve a fresh copy.

  • High Read Efficiency: Since reads happen on a stable snapshot of the array, they don’t require locking, making the collection ideal for cases where multiple threads frequently read data.
  • Copy-on-Write Behavior: Modifications such as adding or removing items result in creating a new array, ensuring that readers don’t encounter data inconsistencies during concurrent writes.

This behavior provides a safe and simple way to manage concurrent access in applications that prioritize read performance.

Implementing the Channel Class

Now that we're clear on how CopyOnWriteArrayList works, let’s implement the Channel class, which will act as our messaging platform where threads can post and read messages.

Java
1import java.util.List; 2import java.util.concurrent.CopyOnWriteArrayList; 3 4public class Channel { 5 private final List<String> messages = new CopyOnWriteArrayList<>();

The Channel class uses a CopyOnWriteArrayList to store messages. This ensures that even if multiple threads post or read messages simultaneously, they won’t interfere with each other.

Posting a Message

In the postMessage() method, the CopyOnWriteArrayList creates a new copy of the array whenever a message is added.

Java
1 public void postMessage(String message) { 2 messages.add(message); 3 System.out.println(Thread.currentThread().getName() + " posted: " + message); 4 }

This ensures thread safety by allowing readers to access a consistent snapshot of the list, even during modifications.

While making a new copy for each write can be costly, it’s efficient for scenarios with frequent reads and fewer writes. This design avoids locking, allowing multiple threads to safely read messages without waiting, making it ideal for a messaging system.

Removing a Message
Java
1 public boolean removeMessage(String message) { 2 boolean removed = messages.remove(message); 3 if (removed) { 4 System.out.println(Thread.currentThread().getName() + " removed: " + message); 5 } 6 return removed; 7 }

The removeMessage() method safely removes a message from the channel if it exists. Since CopyOnWriteArrayList handles all concurrency concerns internally, multiple threads can call this method without interfering with one another. The method also prints the name of the thread that removed the message for better traceability during concurrent operations.

Retrieving Messages
Java
1 public List<String> getMessages() { 2 return messages; 3 } 4}

The getMessages() method returns a list of messages. Since CopyOnWriteArrayList allows safe concurrent reads without needing synchronization, subscribers can read the messages concurrently without worrying about race conditions.

Building the Main Class with Threads

Now that our Channel class is set up, let’s create a main application where multiple threads interact with the channel. One thread will act as the publisher, posting messages, while two other threads will act as subscribers, reading those messages.

Java
1public class Main { 2 public static void main(String[] args) throws InterruptedException { 3 Channel channel = new Channel(); 4 5 // Publisher thread 6 Thread publisher = new Thread(() -> { 7 channel.postMessage("Welcome!"); 8 channel.postMessage("Today's topic is Java Concurrency."); 9 }, "Publisher");

In this section, we create a publisher thread that posts two messages to the channel. By giving the thread a name ("Publisher"), we can easily track which thread posted the messages in the output.

Subscriber Threads
Java
1 // Subscriber 1 thread 2 Thread subscriber1 = new Thread(() -> { 3 for (String message : channel.getMessages()) { 4 System.out.println("Subscriber1 reads: " + message); 5 } 6 }, "Subscriber1"); 7 8 // Subscriber 2 thread 9 Thread subscriber2 = new Thread(() -> { 10 for (String message : channel.getMessages()) { 11 System.out.println("Subscriber2 reads: " + message); 12 } 13 }, "Subscriber2");

We create two subscriber threads, each of which reads all messages posted to the channel. Since CopyOnWriteArrayList guarantees thread safety for concurrent reads, both subscribers can read the messages without causing data inconsistencies.

Thread Coordination
Java
1 // Start publisher thread 2 publisher.start(); 3 publisher.join(); // Ensures the publisher posts messages before subscribers read 4 5 // Start subscriber threads 6 subscriber1.start(); 7 subscriber2.start(); 8 9 subscriber1.join(); 10 subscriber2.join(); 11 } 12}

Here, the publisher.join() ensures that the subscribers only start reading once the publisher has finished posting the messages. This sequencing guarantees that all messages are available before subscribers access them. After the publisher completes, both subscribers read the messages simultaneously.

Why It Matters

Designing a thread-safe messaging system using CopyOnWriteArrayList offers several important benefits:

  • Efficiency for Reads: Since the collection optimizes reads, subscribers can access messages quickly, even when multiple threads are interacting with the channel.
  • Simplicity: Using CopyOnWriteArrayList removes the need for explicit locking mechanisms, which simplifies the code and avoids potential concurrency issues.
  • Real-World Use Cases: This design pattern is applicable in many real-world systems, such as notification services, real-time messaging platforms, and any system where read-heavy operations dominate.

Now that you’ve learned how to build a thread-safe channel, you can apply these concepts to more complex messaging systems. Let's move on to some hands-on exercises where you'll put these concepts into practice!

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