Concurrency Models: Actor Model vs. Communicating Sequential Processes (CSP)
An in-depth mathematical and practical comparison of the Actor Model and CSP concurrency patterns in Java.

Abstract Algorithms
Helping engineers master software engineering topics.
TLDR: Shared-memory multithreading using manual locks is notoriously difficult to scale and debug. To avoid race conditions and deadlocks, modern platforms use message-passing concurrency models. This guide compares the Actor Model (used in Erlang and Akka/Pekko) with Communicating Sequential Processes (CSP, used in Go) and provides full Java implementations for both.
π Concept: The Limitations of Shared-Memory Concurrency
In the classic multi-threaded programming model, threads communicate by sharing access to the same heap memory space. To prevent concurrent threads from overwriting each other's data (race conditions) or reading partial writes (inconsistent states), developers must use synchronization primitives like locks, mutexes, or semaphores.
However, manual shared-memory locking introduces major architectural challenges:
- Deadlocks: If Thread 1 holds Lock A and waits for Lock B, while Thread 2 holds Lock B and waits for Lock A, both threads will hang indefinitely.
- Thread Contention: When many threads compete for the same lock, the OS wastes massive CPU cycles context-switching between threads, degrading system throughput.
- Lack of Isolation: A memory corruption or uncaught exception in one thread can corrupt shared state, crashing the entire application process.
To resolve these lock-based bottlenecks, modern systems apply the famous Go proverb: "Do not communicate by sharing memory; instead, share memory by communicating."
By replacing shared memory with Message Passing, we eliminate manual lock contention and ensure that threads remain completely isolated from each other.
βοΈ Mechanics: Mailboxes, Processes, and Message Passing
To implement message passing, we choose between two primary concurrency models:
1. The Actor Model
In the Actor Model, the fundamental unit of computation is the Actor. An actor is a self-contained object that encapsulates state, behavior, and a private message queue called a Mailbox.
- Actors communicate exclusively by sending asynchronous messages to other actors' mailboxes.
- An actor processes messages from its mailbox sequentially (one at a time), ensuring that its internal state is never accessed concurrently.
- Addressing: Messages are sent to an actor's specific address (or reference).
2. Communicating Sequential Processes (CSP)
In the CSP model, the fundamental units are Processes (lightweight execution threads) and Channels (thread-safe message queues).
- Processes do not know about other processes. They communicate exclusively by writing to and reading from shared Channels.
- Addressing: Messages are sent to a Channel. Any process listening to that channel can retrieve the message.
- Synchronization: In classic CSP, channels are unbuffered (synchronous). A write block until another process reads, coordinating execution timing automatically.
π Flow: Message Passing Sequence
The diagram below visualizes the architectural differences between the Actor Model's address-based routing and CSP's channel-based routing:
flowchart TD
subgraph Actor Model: Address-Based
Actor1[Actor 1] -->|Send Message to Address| Mailbox2[Mailbox 2]
Mailbox2 -->|Sequentially Process| Actor2[Actor 2]
end
subgraph CSP Model: Channel-Based
Proc1[Process 1] -->|Write to Channel| Channel[Shared Channel]
Channel -->|Read from Channel| Proc2[Process 2]
end
The table below summarizes the key structural differences between the two models:
| Attribute | Actor Model | Communicating Sequential Processes (CSP) |
| Primary Abstraction | The Actor (encapsulates state & queue) | Process + Channel (queues are decoupled) |
| Addressing Target | Actor Address (reference) | Shared Channel |
| Queue Coupling | Mailbox is tightly coupled to the Actor | Channel is independent of the Processes |
| Message Timing | Asynchronous (fire-and-forget) | Synchronous (blocks writer until reader arrives) |
| Fault Tolerance | Built-in Supervision Trees | Manual channel error handling |
π§ Deep Dive: Structural Isolation & Thread Context Switches
Let us analyze the internal queue mechanics, mathematical models, and performance constraints of message-passing concurrency.
Thread Isolation and Queue Internals
In both models, message passing relies on thread-safe queues.
- In the Actor Model, mailboxes are typically implemented using non-blocking queues (like Java's
ConcurrentLinkedQueue) to allow publishers to push messages without blocking. - In CSP, channels are implemented using blocking queues (like Java's
ArrayBlockingQueuewith a capacity boundary) to enforce backpressure. If a channel fills up, publisher threads block, preventing memory exhaustion under spikes.
Mathematical Model of Actor and CSP Execution
We can model the state transitions of these systems mathematically.
1. Actor Model Representation
An Actor $A$ is represented as a tuple: $$ A = \langle S, B, M \rangle $$ where $S$ is the private state, $B$ is the behavior function, and $M$ is the mailbox queue. When $A$ receives a message $m \in M$, it computes: $$ B(S, m) \to \langle S', B', \text{Actions} \rangle $$ The behavior modifies the state to $S'$, updates the behavior to $B'$, and triggers actions (such as sending messages to other actors or spawning new actors). Because $S$ is only modified by $B$ sequentially, there is no concurrent access.
2. CSP Representation
A CSP system is represented as a set of sequential processes $P_i$ communicating via channels $C_j$. A process write operation $P \xrightarrow{C!v} P'$ writes value $v$ to channel $C$. A process read operation $Q \xrightarrow{C?x} Q'$ reads value $x$ from channel $C$. In a synchronous channel, the transition occurs simultaneously: $$ (P \xrightarrow{C!v} P') \parallel (Q \xrightarrow{C?x} Q') \implies P' \parallel Q'[x \gets v] $$ This synchronization ensures that data transfer is immediate and coordinated.
Performance Analysis of Context Switching
Traditional OS threads are heavy. Context-switching between them requires saving CPU registers, flushing translation lookaside buffers (TLBs), and jumping execution paths, taking up to 1-2 microseconds.
To make Actors and CSP scale, runtimes (like Go's goroutines or Java 21's Virtual Threads) execute lightweight green threads multiplexed over a small pool of carrier OS threads. This allows context switches to run in CPU cache memory, dropping the switch cost to nanoseconds.
ποΈ Advanced Concepts: Supervision Trees and Channel Buffering
A key advantage of the Actor Model is its Supervision Model (pioneered by Erlang). Actors are organized in a hierarchy. If an actor throws an uncaught exception, its parent supervisor detects the failure and applies a recovery policy:
- One-for-One: Restart only the failed actor.
- One-for-All: Restart all child actors in the supervisor group. This structural fault tolerance allows systems to heal themselves automatically ("let it crash" philosophy).
In CSP, we can introduce Buffered Channels. A buffered channel has a capacity $K$. Writes to a buffered channel do not block until the queue contains $K$ elements, allowing processes to operate asynchronously within queue boundaries.
π Applications: From Erlang Systems to Go Channels
- High-Frequency Chat Backends (Erlang/Akka): Chat servers represent each user connection as a dedicated actor, managing state and messaging in isolation.
- Go Web Server Pipelines (CSP): HTTP requests are handled by lightweight goroutines communicating via channels to execute logging, authentication, and database updates in parallel.
- Telecommunication Switches: Managing thousands of simultaneous calls using isolated actors to prevent call failures from affecting the system.
βοΈ Trade-offs and Failure Modes
- Mailbox Overflow: If an actor processes messages slower than they arrive, its mailbox will grow indefinitely, leading to an
OutOfMemoryError. - Mitigation: Use bounded mailboxes and configure drop-oldest or dead-letter queue policies.
- Channel Deadlocks (CSP): If all processes block waiting to write or read from unbuffered channels, the system will lock up.
- Mitigation: Always verify that every channel write has a corresponding active reader thread.
π§ Decision Guide: Shared Memory vs. Actors vs. CSP
| Metric | Shared Memory (Locks) | Actor Model | CSP (Channels) |
| Concurrency Safety | Manual (error-prone) | High (isolated state) | High (shared channels) |
| Routing Topology | Point-to-Point | Dynamic Address routing | Pipeline / Flow routing |
| Fault Tolerance | Low (manual try-catch) | High (Supervision Trees) | Moderate (manual channel close) |
| Primary Language | Java (legacy), C++ | Scala (Akka), Erlang | Go, Clojure |
π§ͺ Practical Implementation: Java Actor vs. CSP Reference Code
Let us implement both patterns in Java.
1. Lightweight Actor System in Java
This implementation defines an actor structure using Java's ExecutorService and ConcurrentLinkedQueue.
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class SimpleActor<T> implements Runnable {
private final Queue<T> mailbox = new ConcurrentLinkedQueue<>();
private final ExecutorService executor;
private final AtomicBoolean isScheduled = new AtomicBoolean(false);
public SimpleActor(ExecutorService executor) {
this.executor = executor;
}
public void send(T message) {
mailbox.offer(message);
schedule();
}
private void schedule() {
if (isScheduled.compareAndSet(false, true)) {
executor.submit(this);
}
}
@Override
public void run() {
try {
T message = mailbox.poll();
if (message != null) {
onReceive(message);
}
} finally {
isScheduled.set(false);
if (!mailbox.isEmpty()) {
schedule();
}
}
}
protected abstract void onReceive(T message);
}
// Example usage
class PrinterActor extends SimpleActor<String> {
public PrinterActor(ExecutorService executor) {
super(executor);
}
@Override
protected void onReceive(String message) {
System.out.println("Actor received: " + message);
}
}
2. CSP Model using BlockingQueue Channels
This implementation defines a Producer-Consumer pipeline using Java's ArrayBlockingQueue to represent a CSP channel.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
// Shared Channel Abstraction
class Channel<T> {
private final BlockingQueue<T> queue;
public Channel(int capacity) {
this.queue = new ArrayBlockingQueue<>(capacity);
}
public void write(T value) throws InterruptedException {
queue.put(value); // Blocks if channel is full
}
public T read() throws InterruptedException {
return queue.take(); // Blocks if channel is empty
}
}
// CSP Processes
class Producer implements Runnable {
private final Channel<String> channel;
public Producer(Channel<String> channel) {
this.channel = channel;
}
@Override
public void run() {
try {
channel.write("Message from Producer 1");
channel.write("Message from Producer 2");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
private final Channel<String> channel;
public Consumer(Channel<String> channel) {
this.channel = channel;
}
@Override
public void run() {
try {
System.out.println("Consumer read: " + channel.read());
System.out.println("Consumer read: " + channel.read());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
π Lessons Learned: Common Concurrency Pitfalls
- Passing Mutable Objects in Messages: Message-passing models only guarantee thread safety if messages are immutable. If Actor 1 sends a mutable Java
Listto Actor 2, both threads will hold a reference to that list, restoring shared-memory concurrent write bugs. Always copy collections or use immutable value records. - Blocking Inside Actor Receives: If an actor thread executes a blocking database call inside
onReceive(), it will block the executor pool, preventing other actors from running. All external I/O inside actors must be executed asynchronously. - Forgetting to Close Channels (CSP): In Go or Clojure CSP implementations, forgetting to close a channel can leave consumer goroutines blocked waiting for input, leading to goroutine leaks. Always close channels when data transmission is finished.
π Summary: The Concurrency Models Cheatsheet
- Concurrency Proverb: Share memory by communicating, rather than communicating by sharing memory.
- Actor Model: Encapsulates state and mailbox. Messages are sent to an Actor's Address asynchronously.
- CSP: Processes communicate via independent Channels. Writing/reading blocks by default to enforce coordination.
- Immutability: Messages passed between threads must be completely immutable to prevent race conditions.
- Context Scaling: Virtual threads or green threads scale context switching by executing context transfers in memory.
AI-generated article quiz
Test your understanding
Ready to test what you just learned?
Generate four focused questions from this article. Answers include immediate explanations.
Guided series path
Software Engineering Principles
Reader feedback
Was this article useful?
Rate it if it helped, then continue with the next deep dive when you are ready.
Sign in to save your rating.
Article metadata