Welcome back! In Part 1, we talked about the history of OOP, the challenges of multithreading in Java, and some common misconceptions about asynchronous programming. We saw that the core problem often comes down to shared mutable state – data that multiple threads can access and change.
In this part, we’re going to dig into the specific problems that shared state can cause. We’ll look at:
Deadlocks: When threads get stuck waiting for each other.
Livelocks: Like deadlocks, but the threads are still running (just not making progress).
Race Conditions: When the timing of threads leads to incorrect results.
Starvation: When a thread never gets a chance to run.
To illustrate these problems, we’ll use a running example of multiple customers (Arthur and Maria) trying to buy concert tickets online. We’ll see how seemingly simple code can go wrong in a multithreaded environment, and we’ll explore how to fix these issues using traditional Java concurrency tools. We’ll also discuss how caching, while essential for performance, adds another layer of complexity to the mix. Finally, we’ll briefly introduce the Java Memory Model and the built-in tools Java provides for managing shared state.
Shared Resources: When Things Get Crowded
When multiple threads try to use the same shared resources (data, files, or hardware), problems can appear. Let’s look at some examples, using a story about Arthur and Maria buying concert tickets.
Deadlock: A Polite Impasse
Imagine Arthur and Maria both want to buy tickets. Arthur wants to pay first and then reserve his seat. Maria wants to reserve her seat first and then pay. If Arthur grabs the “payment lock” (like waiting in line for the credit card machine) and Maria grabs the “reservation lock” (like holding a ticket), they’re stuck! Arthur is waiting for Maria to release the reservation, and Maria is waiting for Arthur to finish paying. This is a deadlock – it’s like everyone is waiting for someone else, and nobody can do anything. Locks are useful to keep things organized, but if we’re not careful, they can cause these kinds of problems.
import java.util.Random;
// PaymentGateway.java
public class PaymentGateway {
private final Object paymentLock = new Object();
public void processPayment() {
synchronized (paymentLock) {
System.out.println(Thread.currentThread().getName() + " interacting with payment gateway");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// TicketInventory.java
public class TicketInventory {
private int availableTickets = 2;
private final Object inventoryLock = new Object();
public boolean reserveTicket() {
synchronized (inventoryLock) {
System.out.println(Thread.currentThread().getName() + " reserving ticket");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (availableTickets > 0) {
availableTickets--;
System.out.println(Thread.currentThread().getName() + " got a ticket.");
return true;
} else {
System.out.println(Thread.currentThread().getName() + ": No tickets left!");
return false;
}
}
}
}
// TicketPurchaseOrchestrator.java (Deadlock version)
import java.util.Random;
public class TicketPurchaseOrchestrator {
private final PaymentGateway paymentGateway;
private final TicketInventory ticketInventory;
public TicketPurchaseOrchestrator() {
this.paymentGateway = new PaymentGateway();
this.ticketInventory = new TicketInventory();
}
public void purchase() {
Random random = new Random();
boolean paymentFirst = random.nextBoolean();
if (paymentFirst) {
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
synchronized (ticketInventory.inventoryLock) {
ticketInventory.reserveTicket();
}
}
} else {
synchronized (ticketInventory.inventoryLock) {
ticketInventory.reserveTicket();
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
}
}
}
}
}
// TicketPurchaseSimulation.java (Main class)
public class TicketPurchaseSimulation {
public static void main(String[] args) {
TicketPurchaseOrchestrator orchestrator = new TicketPurchaseOrchestrator();
Thread customer1 = new Thread(orchestrator::purchase, "Customer 1");
Thread customer2 = new Thread(orchestrator::purchase, "Customer 2");
customer1.start();
customer2.start();
}
}Expected Output (Deadlock - program hangs):
Customer 1 interacting with payment gateway
Customer 2 reserving ticket
(program hangs indefinitely)But, you know what would a smart ticket seller do? They’d say, “Hold on! Only one person can use the checkout at a time.” So, they set up a single “purchase lock” – imagine a velvet rope. Now, even if Arthur wants to pay first and Maria wants to reserve first, only one of them can go past the rope at a time. This way, nobody gets stuck, and everyone gets a fair chance to buy tickets.
// ... (PaymentGateway, TicketInventory remain the same)
// TicketPurchaseOrchestrator.java (Fixed - no deadlock)
import java.util.Random;
public class TicketPurchaseOrchestrator {
// ... (paymentGateway, ticketInventory as before)
private final Object purchaseLock = new Object(); // Single lock to prevent deadlock
public void purchase() {
Random random = new Random();
boolean paymentFirst = random.nextBoolean();
synchronized (purchaseLock) { // Use the single lock here
if (paymentFirst) {
paymentGateway.processPayment();
ticketInventory.reserveTicket();
} else {
ticketInventory.reserveTicket();
paymentGateway.processPayment();
}
}
}
}
// ... (TicketPurchaseSimulation.java and main method remain the same)Expected Output (Deadlock Solution):
Customer 1 interacting with payment gateway
Customer 1 got a ticket.
Customer 2 reserving ticket
Customer 2: No tickets left!
//OR
Customer 2 reserving ticket
Customer 2 got a ticket.
Customer 1 interacting with payment gateway
Customer 1: No tickets left!Understanding the solution
A single purchaseLock is added. The purchase() method now uses this lock, making sure only one customer can go through the whole process at a time. This prevents the circular dependency (Arthur waiting for Maria, Maria waiting for Arthur) that caused the deadlock.
Livelock: The Endless “After You”
Arthur and Maria are very polite. Arthur says, “After you, Maria!” Maria says, “No, no, after you, Arthur!” They keep letting each other go first, but neither of them ever actually buys the tickets. That’s a livelock – they’re not blocked, but they’re also not making any progress. It’s like a polite deadlock. If they also wait a little bit randomly before being polite, it makes the problem even more likely.
import java.util.Random;
// ... (PaymentGateway and TicketInventory remain the same)
// TicketPurchaseOrchestrator.java (Livelock version)
public class TicketPurchaseOrchestrator {
// ... (paymentGateway, ticketInventory as before)
private final Random random = new Random();
public void purchase() {
boolean paymentFirst = random.nextBoolean();
if (paymentFirst) {
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Small delay
synchronized (ticketInventory.inventoryLock) {
ticketInventory.reserveTicket();
}
}
} else {
synchronized (ticketInventory.inventoryLock) {
ticketInventory.reserveTicket();
try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // Small delay
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
}
}
}
}
}
// ... (TicketPurchaseSimulation.java and main method remain the same)Our polite ticket-buyers, Arthur and Maria, are stuck being polite, sometimes waiting a bit before being polite again. How do we fix this? They need to be a little less polite! Instead of always letting the other person go first, they sometimes need to just go for it. We add if (random.nextDouble() < 0.2) which means each thread has a 20% chance to “be assertive” and try to grab the ticket, even if the other thread is also trying to be polite. This helps break the livelock. The ticketAvailable = false part is also very important. After one of them gets the ticket, the other one can’t get it anymore.
// ... (PaymentGateway remains the same)
// TicketInventory.java (Livelock Solution)
public class TicketInventory {
private int availableTickets = 2;
private boolean ticketAvailable = true;
private final Object inventoryLock = new Object();
public synchronized boolean reserveTicket() {
System.out.println(Thread.currentThread().getName() + " reserving ticket");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
if (availableTickets > 0 && ticketAvailable) {
availableTickets--;
ticketAvailable = false;
System.out.println(Thread.currentThread().getName() + " got a ticket.");
return true;
} else {
System.out.println(Thread.currentThread().getName() + ": No tickets left!");
return false;
}
}
}
// TicketPurchaseOrchestrator.java (Livelock Solution)
import java.util.Random;
public class TicketPurchaseOrchestrator {
private final PaymentGateway paymentGateway;
private final TicketInventory ticketInventory;
private final Random random = new Random();
public TicketPurchaseOrchestrator() {
this.paymentGateway = new PaymentGateway();
this.ticketInventory = new TicketInventory();
}
public void purchase() {
if (random.nextDouble() < 0.2) { // 20% chance to be assertive
synchronized (ticketInventory.inventoryLock) {
if (ticketInventory.reserveTicket()) {
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
}
return; // Exit if ticket was taken
}
}
}
boolean paymentFirst = random.nextBoolean();
if (paymentFirst) {
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (ticketInventory.inventoryLock) {
if (ticketInventory.reserveTicket()) { // Check if reservation was successful
return; // Exit if ticket was reserved
}
}
}
} else {
synchronized (ticketInventory.inventoryLock) {
if (ticketInventory.reserveTicket()) { // Check if reservation was successful
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
return; // Exit if ticket was reserved
}
}
try { Thread.sleep(random.nextInt(10)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
}
}
}
}
}
// ... (TicketPurchaseSimulation.java and main method remain the same)
Understanding the solution
Assertiveness (Probability): The
if (random.nextDouble() < 0.2)line gives each thread a 20% chance to try to reserve a ticket directly, without waiting. This “assertiveness” helps break the cycle.Ticket Availability Check and
return: The biggest change is in thereserveTicketmethod inTicketInventory, it now gives back the value ofticketAvailable. Also, inside thesynchronizedblocks there are the these new lines:if (ticketInventory.reserveTicket()) { return; }. After a thread reserves a ticket (or if there are no tickets), thereserveTicketmethod now makesticketAvailablefalseso that the other customer cannot get a ticket. Thepurchase()method checks the result ofreserveTicket(). If the reservation was successful (a ticket was taken), the thread immediately exits thepurchase()method usingreturn;. This stops the thread from being polite and potentially getting stuck in the livelock.
Race Condition: The Ticket Vanishes
Arthur and Maria are both clicking “Buy!” very fast. The system checks if there are tickets. Arthur sees one, but before he can buy it, Maria checks and also sees one! They both try to buy, but there was only one ticket! That’s a race condition – the timing of the threads causes a problem. It’s like two people reaching for the same cookie at the exact same time.
// TicketPurchaseOrchestrator.java (Race condition version)
public class TicketPurchaseOrchestrator {
// ... (paymentGateway, ticketInventory as before)
public void purchase() {
if (ticketInventory.availableTickets > 0) { // Check-then-act - potential race condition
System.out.println(Thread.currentThread().getName() + " sees ticket available");
ticketInventory.availableTickets--; // Decrement - potential race condition
System.out.println(Thread.currentThread().getName() + " bought a ticket");
} else {
System.out.println(Thread.currentThread().getName() + " no tickets left");
}
}
}
// ... (PaymentGateway, TicketInventory, TicketPurchaseSimulation.java and main method remain the same)Expected Output (Race Condition - incorrect ticket count):
Customer 1 sees ticket available
Customer 2 sees ticket available
Customer 1 bought a ticket
Customer 2 bought a ticket // Should not happen!Arthur and Maria are still clicking, creating that race condition. How do we fix it? We need to make the “check if tickets are available” and “reduce the count” steps happen together, as one single action. No checking and then changing; it has to be at the same time. We need to lock the whole process.
// TicketPurchaseOrchestrator.java (Race condition solution)
public class TicketPurchaseOrchestrator {
// ... (paymentGateway, ticketInventory as before)
private final Object lock = new Object(); // Lock for synchronization
public void purchase() {
synchronized (lock) { // Synchronize on the lock
if (ticketInventory.availableTickets > 0) {
System.out.println(Thread.currentThread().getName() + " sees ticket available");
ticketInventory.availableTickets--;
System.out.println(Thread.currentThread().getName() + " bought a ticket");
} else {
System.out.println(Thread.currentThread().getName() + " no tickets left");
}
}
}
}
// ... (PaymentGateway, TicketInventory, TicketPurchaseSimulation.java and main method remain the same)Expected Output (Race Condition Solution - correct ticket count):
Customer 1 sees ticket available
Customer 1 bought a ticket
Customer 2 no tickets left
// OR
Customer 2 sees ticket available
Customer 2 bought a ticket
Customer 1 no tickets leftUnderstanding the solution
A lock object is added. Now, the purchase() method is inside a synchronized (lock) block. This means only one thread can run the code inside that block at a time. The check and the count reduction are now done together, preventing the race condition. Even if the system uses a cache, the lock makes sure the value in the cache is correct when a thread buys the ticket. The synchronized block makes sure only one thread can use the availableTickets variable at a time, so no other thread can interfere.
Starvation: The VIP Line
The ticket website has a VIP line. Arthur, a regular customer, has been waiting for a long time. But every time a ticket becomes available, a VIP customer gets it first. That’s starvation. Arthur isn’t blocked, but he never gets a ticket because the VIPs always have priority. It’s like waiting in line at a club where the bouncer always lets the VIPs in first. This happens because the priority queue always chooses VIPs. We can’t completely fix this just by changing the code that handles buying tickets. Real systems use special “schedulers” to make sure everyone gets a fair shot, even if they’re not VIPs. These schedulers work around the priority queue to prevent starvation.
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
// PaymentGateway.java (remains the same)
// TicketInventory.java (remains the same)
// TicketPurchaseOrchestrator.java (Starvation Version)
public class TicketPurchaseOrchestrator implements Runnable {
private final String name;
private final int priority;
private final PriorityBlockingQueue<TicketPurchaseOrchestrator> queue;
private final PaymentGateway paymentGateway;
private final TicketInventory ticketInventory;
public TicketPurchaseOrchestrator(String name, int priority, PriorityBlockingQueue<TicketPurchaseOrchestrator> queue, PaymentGateway paymentGateway, TicketInventory ticketInventory) {
this.name = name;
this.priority = priority;
this.queue = queue;
this.paymentGateway = paymentGateway;
this.ticketInventory = ticketInventory;
}
@Override
public void run() {
try {
queue.put(this); // Add customer to the queue
while (true) {
TicketPurchaseOrchestrator nextCustomer = queue.take(); // Take the next customer (priority-based)
if (nextCustomer == this) { // Is it our turn?
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
synchronized (ticketInventory.inventoryLock) {
if (ticketInventory.reserveTicket()) {
System.out.println(name + " got a ticket!");
break; // Customer got a ticket, exit loop
} else {
System.out.println(name + ": No tickets left!");
break; // No tickets left, exit loop
}
}
}
} else {
queue.put(nextCustomer); // Put the customer back in the queue
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public String toString() {
return name + " (Priority: " + priority + ")";
}
}
// TicketPurchaseSimulation.java (Main class - Starvation version)
import java.util.concurrent.PriorityBlockingQueue;
public class TicketPurchaseSimulation {
public static void main(String[] args) throws InterruptedException {
PriorityBlockingQueue<TicketPurchaseOrchestrator> queue = new PriorityBlockingQueue<>((c1, c2) -> c2.priority - c1.priority); // Higher priority first
PaymentGateway paymentGateway = new PaymentGateway();
TicketInventory ticketInventory = new TicketInventory();
// Create some regular customers
Thread regularCustomer1 = new Thread(new TicketPurchaseOrchestrator("Arthur", 1, queue, paymentGateway, ticketInventory));
Thread regularCustomer2 = new Thread(new TicketPurchaseOrchestrator("Regular Customer 2", 1, queue, paymentGateway, ticketInventory));
// Create some VIP customers (higher priority)
Thread vipCustomer1 = new Thread(new TicketPurchaseOrchestrator("VIP Customer 1", 5, queue, paymentGateway, ticketInventory));
Thread vipCustomer2 = new Thread(new TicketPurchaseOrchestrator("VIP Customer 2", 5, queue, paymentGateway, ticketInventory));
Thread vipCustomer3 = new Thread(new TicketPurchaseOrchestrator("VIP Customer 3", 5, queue, paymentGateway, ticketInventory));
regularCustomer1.start();
regularCustomer2.start();
vipCustomer1.start();
vipCustomer2.start();
vipCustomer3.start();
Thread.sleep(2000); // Let the threads run for a while
regularCustomer1.interrupt();
regularCustomer2.interrupt();
vipCustomer1.interrupt();
vipCustomer2.interrupt();
vipCustomer3.interrupt();
}
}Expected Output (Starvation - Regular customers likely don’t get tickets):
You’ll probably see that the VIP customers get tickets, and Arthur (the regular customer) keeps getting skipped. It’s possible Arthur might get a ticket before the VIPs show up, but it’s not likely. The important thing is that starvation is very probable.
VIP Customer 1 got a ticket!
VIP Customer 3 got a ticket!
VIP Customer 2: No tickets left!
// ... (Arthur likely never gets a chance)Simplified Starvation Mitigation:
Now, let’s add a simple way to reduce starvation (but not completely eliminate it). This shows the idea of using something outside of the priority queue.
// TicketPurchaseOrchestrator.java (with Mitigation)
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
public class TicketPurchaseOrchestrator implements Runnable {
private final String name;
private final int priority;
private final PriorityBlockingQueue<TicketPurchaseOrchestrator> queue;
private final PaymentGateway paymentGateway;
private final TicketInventory ticketInventory;
private final AtomicInteger vipServicedCount = new AtomicInteger(0); // Count VIPs served
private static final int MAX_VIP_BEFORE_REGULAR = 3; // Max VIPs before a regular customer
public TicketPurchaseOrchestrator(String name, int priority, PriorityBlockingQueue<TicketPurchaseOrchestrator> queue, PaymentGateway paymentGateway, TicketInventory ticketInventory) {
this.name = name;
this.priority = priority;
this.queue = queue;
this.paymentGateway = paymentGateway;
this.ticketInventory = ticketInventory;
}
@Override
public void run() {
try {
queue.put(this); // Add to the queue
while (true) {
TicketPurchaseOrchestrator nextCustomer = queue.peek(); // Peek, don't remove yet
// Mitigation Logic: If we've served too many VIPs, and this is a regular customer,
// let them go next, even if there are VIPs waiting.
if (vipServicedCount.get() >= MAX_VIP_BEFORE_REGULAR && this.priority == 1) {
nextCustomer = queue.take(); // Now we take it
vipServicedCount.set(0); // Reset the VIP count
} else {
nextCustomer = queue.take(); // Take based on priority
if (nextCustomer.priority > 1) {
vipServicedCount.incrementAndGet(); // Increment if it was a VIP
}
}
if (nextCustomer == this) { // Is it our turn?
synchronized (paymentGateway.paymentLock) {
paymentGateway.processPayment();
synchronized (ticketInventory.inventoryLock) {
if (ticketInventory.reserveTicket()) {
System.out.println(name + " got a ticket!");
break;
} else {
System.out.println(name + ": No tickets left!");
break;
}
}
}
} else {
queue.put(nextCustomer); // Put the customer back
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public String toString() {
return name + " (Priority: " + priority + ")";
}
}
// TicketPurchaseSimulation.java remains the sameUnderstanding the solution (Mitigation)
vipServicedCountandMAX_VIP_BEFORE_REGULAR: We add a counter (vipServicedCount) to keep track of how many VIP customers have been served in a row.MAX_VIP_BEFORE_REGULARsets a limit.queue.peek(): Instead of taking the next customer right away withqueue.take(), we usequeue.peek()to look at the next customer without removing them. This is important for our fix.Mitigation Check: We check
if (vipServicedCount.get() >= MAX_VIP_BEFORE_REGULAR && this.priority == 1). This means, “If we’ve served the maximum number of VIPs in a row, and the current thread is a regular customer, then…”.Force Regular Customer: If the condition is true, we now use
queue.take()to take the customer (which we know is the regular customer) and reset thevipServicedCount. This makes sure the regular customer gets served, even if VIPs are waiting.Standard Priority: If the condition isn’t true, we do what we did before: take the highest-priority customer with
queue.take()and increasevipServicedCountif it was a VIP.
Expected Output (with Mitigation):
Now, Arthur (or other regular customers) should have a better chance of getting a ticket. It’s still not guaranteed (because it’s still a priority queue), but it’s much more likely.
VIP Customer 1 got a ticket!
VIP Customer 3 got a ticket!
VIP Customer 2 got a ticket!
Arthur got a ticket! // Much more likely now!
Regular Customer 2: No tickets left!This example shows how to reduce starvation using a simple counter. Real schedulers use much more complex methods, but this shows the main idea: you need something outside of the priority queue itself to make things fair. This fix isn’t directly part of the buying process, but rather how the queue is managed, which highlights that it’s an external solution.
The Perils of Shared Mutable State and Caching
So, what’s the main cause of all these concurrency problems (deadlocks, livelocks, race conditions)? It’s shared mutable state. That’s a fancy way of saying “data that multiple threads can access and change.” When multiple threads or async tasks can change the same data, it’s very hard to keep the data correct and consistent. Caching, which makes programs faster, adds another layer of difficulty.
Why Shared Mutable State is Problematic?
Unpredictability: It’s hard to know what’s happening in your program when multiple parts can change the data at the same time. The order things happen in becomes unpredictable.
Race Conditions: Like we saw before, if multiple threads change the same data at the same time, you can get incorrect results.
Debugging Nightmares: Finding the cause of concurrency bugs is extremely difficult. It is like chasing ghosts, and the bugs can be super sneaky (these are called heisenbugs, a funny name for a serious problem!)
The Caching Conundrum
Caching is a way to make programs run faster. Instead of getting data from a slow place (like a database) every time, we keep a copy in a faster place (like memory). But caching creates a new problem: cache coherence.
Levels of Caching (Simplified):
CPU Caches (L1, L2, L3): These are very fast caches inside the CPU itself. Each core (part of the CPU) often has its own L1 and L2 caches, and the L3 cache is shared between all the cores.
Main Memory (RAM): Slower than CPU caches, but much faster than your hard drive.
Disk Cache: Your operating system (like Windows or macOS) often keeps copies of frequently used data from your hard drive in RAM.
Database Caches: Databases also have their own ways of caching data to make things faster.
Application-Level Caches: Your own program might have its own caches in memory (for example, using libraries like Guava Cache or Caffeine in Java).
Distributed Caches: In systems where you have many computers working together, you might use a distributed cache like Redis or Memcached.
Cache Coherence Issues:
Stale Data: Imagine one thread changes some data in the database. But another thread has an old copy of that data in its cache. The second thread will be using the wrong information!
Write Conflicts: If multiple threads try to update the same cached value at the same time, you can get race conditions, even if the database itself is protected by locks.
Java Memory Model and Tools for Managing Shared State
Java gives us tools to help manage shared data and make sure caches are consistent:
synchronizedKeyword: This creates a lock. Only one thread can run code inside asynchronizedblock at a time. It also makes sure that changes made by one thread are visible to others (this is called establishing happens-before relationships).volatileKeyword: This makes sure that when you read or write a variable, it goes directly to the main memory, skipping the CPU caches. This guarantees that other threads will see the latest value (visibility), but it doesn’t make operations like adding to a number atomic (happening all at once).Atomic Variables (e.g.,
AtomicInteger,AtomicLong,AtomicReference): These provide special operations (like “compare and swap”) that guarantee both visibility and atomicity for single variables. So, you can increment a counter safely, for example.Locks (e.g.,
ReentrantLock,ReadWriteLock): These give you more control over locking thansynchronized.ReadWriteLockis useful because it lets multiple threads read at the same time, but only one thread write at a time.Concurrent Collections (e.g.,
ConcurrentHashMap,CopyOnWriteArrayList): These are special data structures (like lists and maps) that are designed to be used safely by multiple threads.
Happens-Before Relationship: The Java Memory Model has a rule called the happens-before relationship. It’s a way of saying that if one action happens-before another, the first action is guaranteed to be visible to the second. synchronized, volatile, and atomic operations all create these happens-before relationships. This guarantee helps deal with the fact that the CPU and compiler might reorder instructions to make things faster.
Conclusion
So, handling shared data and caching in concurrent programs is difficult, but it’s necessary. Java is based on objects, but it still has the problem of sharing resources between threads. OOP tries to keep things separate using encapsulation. But in real, fast programs with multiple CPU cores, we often have to share data between objects, which is different from the original idea of objects being completely separate.
Java provides many tools to help – synchronized, volatile, atomics, locks, and concurrent collections. But developers must understand the Java Memory Model, it’s especially important to have both the official spec and a more approachable explanation to use these tools correctly and ( perhaps also a more beginner-friendly explanation, e.g., Java Concurrency in Practice) to avoid difficult bugs. If you don’t handle concurrency correctly, you lose the benefits of OOP: code that is easy to maintain, predictable, and reliable.
So, is there a better way? In Part 3, we’ll introduce the Actor Model, a different approach to concurrency that avoids many of these problems by design. Instead of sharing data and using locks, actors communicate by sending messages, leading to a simpler and more robust way to build concurrent applications.





