Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
205: The Actor Model Concept in Java
If you've spent any time with multi-threaded Java, you know the drill: you create a shared object, you slap synchronized on every method, and you pray you didn't create a deadlock that will freeze your production server at 3 AM. I've been there, and frankly, I'm tired of it. That's why I want to show you the Actor Model.
The core philosophy here is simple: don't share state. Instead, you treat "Actors" as little independent islands. If Actor A wants Actor B to do something, it doesn't call a method on Actor B. It sends a message to Actor B's mailbox. Actor B then processes those messages one by one, in its own time, on its own thread. No locks, no ConcurrentModificationException, no madness.
Defining our Message Protocol
To build this, we first need a way to represent "work." I like to use a sealed interface for this in modern Java; it keeps the message types strict so the Actor knows exactly what it's handling. Let's build a simple Bank Account system. In a traditional app, you'd just call account.deposit(100). Here, we'll send a Deposit message.
public sealed interface AccountMessage {
record Deposit(double amount) implements AccountMessage {}
record Withdraw(double amount) implements AccountMessage {}
record GetBalance(java.util.function.Consumer<Double> callback) implements AccountMessage {}
}
Building the Actor Mailbox
Now we need the Actor itself. A real Actor needs two things: a private state and a queue (the mailbox). I'm going to use a ConcurrentLinkedQueue to hold the messages and a single-threaded ExecutorService to ensure that only one message is processed at a time. This is the "magic" part—because only one thread ever touches the account balance, we don't need a single synchronized keyword.
import java.util.concurrent.*;
import java.util.function.Consumer;
public class BankAccountActor {
private double balance = 0; // Private state!
private final Queue<AccountMessage> mailbox = new ConcurrentLinkedQueue<>();
private final ExecutorService dispatcher = Executors.newSingleThreadExecutor();
public void send(AccountMessage message) {
mailbox.offer(message);
dispatcher.submit(this::processNextMessage);
}
private void processNextMessage() {
AccountMessage msg = mailbox.poll();
if (msg == null) return;
if (msg instanceof AccountMessage.Deposit d) {
balance += d.amount();
System.out.println("Deposited " + d.amount() + ". New balance: " + balance);
} else if (msg instanceof AccountMessage.Withdraw w) {
if (balance >= w.amount()) {
balance -= w.amount();
System.out.println("Withdrew " + w.amount() + ". New balance: " + balance);
} else {
System.out.println("Insufficient funds for withdrawal of " + w.amount());
}
} else if (msg instanceof AccountMessage.GetBalance g) {
g.callback().accept(balance);
}
}
}
Oops, I almost cheated the model
When I first wrote a version of this, I made a classic mistake. I added a public double getBalance() method to the class because I thought, "It's just a getter, it's fine."
But that's a leak. If I call getBalance() from the main thread while the dispatcher thread is updating the balance, I'm back to square one: shared state and potential visibility issues. To stay true to the Actor Model, everything must be a message. That's why I added the GetBalance message with a callback. You don't "get" a value from an actor; you ask the actor to send the value back to you.
Putting the Actor to Work
Now we can throw a bunch of requests at this account from different threads, and we don't have to worry about race conditions. The mailbox handles the sequencing for us.
public class Main {
public static void main(String[] args) {
BankAccountActor account = new BankAccountActor();
// Simulate multiple threads hitting the account
Runnable task = () -> {
account.send(new AccountMessage.Deposit(100));
account.send(new AccountMessage.Withdraw(50));
};
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
// Ask for the final balance via a callback
account.send(new AccountMessage.GetBalance(bal > System.out.println("Final Balance: " + bal)));
}
}
Notice how the send method returns immediately. The main thread isn't waiting for the deposit to happen; it's just dropping a note in the mailbox and moving on. This is how you build highly scalable systems—by decoupling the request from the execution.
📋 Practical Task
Build a Stock Price Ticker Actor
To practice this pattern, your task is to build a StockTickerActor. Instead of a bank account, this actor will manage the current price of a specific stock symbol.
Requirements:
- Create a sealed interface
TickerMessagewith three record types:UpdatePrice(double newPrice),GetPrice(Consumer<Double> callback), andResetPrice(). - Implement the
StockTickerActorusing aConcurrentLinkedQueueand a single-threadedExecutorService. - Ensure that the price state is strictly private and can only be modified or read via the
send()method. - Write a
mainmethod that simulates three different threads updating the price simultaneously, followed by a final request to print the price using the callback.
There are no comments for now.