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
203: Designing Thread-Safe Classes
A few years ago, I was reviewing code for a teammate who had built a custom SessionCache for a high-traffic API. On his local machine, everything was lightning fast and worked perfectly. But the moment we pushed it to the staging environment—where we actually simulated a few hundred concurrent users—the system started behaving like it was possessed. Sessions were disappearing, user IDs were getting swapped, and we saw the dreaded ConcurrentModificationException popping up in the logs every few minutes. He had used a standard HashMap and a simple integer counter, assuming that because the methods were "short," the race conditions wouldn't be a problem. He learned the hard way that in a multi-threaded environment, "short" doesn't mean "atomic."
Designing a thread-safe class isn't about sprinkling the synchronized keyword everywhere and hoping for the best. That's a recipe for deadlocks and terrible performance. Instead, it's about making a conscious decision about how state is managed and exposed. When you design a class for concurrency, you're essentially creating a contract that guarantees the object remains in a consistent state regardless of how many threads are hammering it at once.
The Elegance of Immutability
The absolute easiest way to make a class thread-safe is to make it immutable. If an object's state cannot change after it's constructed, you don't need locks, you don't need volatile variables, and you'll never have a race condition. I always suggest starting here. If you can represent your data as an immutable value object, do it.
To do this in Java, you mark your fields as final and ensure that any mutable objects held by the class are not leaked or modified. For example, if your class has a List, don't just make the list reference final; wrap it in Collections.unmodifiableList(). If a thread wants to "change" the state, it doesn't modify the existing object—it creates a new one with the updated value. It feels like a waste of memory at first, but the JVM is incredibly efficient at garbage collecting short-lived objects, and the mental overhead you save is worth every byte.
Guarding State with Synchronization
Sometimes, immutability isn't practical. If you're building something like a ConnectionPool or a TaskQueue, the whole point is to manage a changing state. In these cases, you have to protect your "critical sections"—the parts of the code where state is read and then modified based on that read.
The most common mistake I see is synchronizing the wrong thing or synchronizing too much. If you synchronize every method in your class, you've essentially turned your multi-threaded application back into a single-threaded one. Instead, identify the specific shared resource. If you're using a private lock object, you avoid "lock contention" from external code that might try to synchronize on your instance from the outside.
public class TransactionManager { private final Object lock = new Object(); private double balance; public void deposit(double amount) { synchronized(lock) { balance += amount; } } public double getBalance() { synchronized(lock) { return balance; } } }Notice how I'm synchronizing both the read and the write. A common trap is thinking you only need to synchronize the setter. But without synchronizing the getter, a thread might read a "stale" version of the balance that is cached in the CPU core's local memory rather than the main RAM.
Leveraging Atomic Primitives
If your thread-safety needs are limited to simple counters or flags,
synchronizedis overkill. It's like using a sledgehammer to hang a picture frame. Java provides thejava.util.concurrent.atomicpackage, which uses low-level CPU instructions called Compare-And-Swap (CAS). These are non-blocking and significantly faster for simple operations.Instead of an
intprotected by a lock, use anAtomicInteger. Instead of aboolean, use anAtomicBoolean. These classes ensure that operations like incrementing a value happen atomically—meaning the read, update, and write happen as a single, indivisible unit. I've seen performance jump by 20-30% just by swapping a synchronized counter for anAtomicLongin a logging system.
📋 Practical Task
Fixing the Concurrent Ticket Booking System
You have been handed a TicketBooth class used by a cinema chain. The current implementation is failing under load, allowing the system to sell more tickets than are actually available in the theater (overselling). Your task is to make this class thread-safe.
Requirements:
- Ensure that
sellTicket()never allows theavailableTicketscount to drop below zero. - Ensure that
getAvailableTickets()always returns the most current, accurate count. - Prevent "lost updates" where two threads sell a ticket simultaneously but only one decrement is recorded.
// BUGGY CODE: Make this thread-safe public class TicketBooth { private int availableTickets; public TicketBooth(int totalTickets) { this.availableTickets = totalTickets; } public boolean sellTicket() { if (availableTickets > 0) { // Simulate a tiny delay in processing try { Thread.sleep(10); } catch (InterruptedException e) {} availableTickets--; return true; } return false; } public int getAvailableTickets() { return availableTickets; } }Instructions: Rewrite the
TicketBoothclass. You may choose to usesynchronizedblocks, aReentrantLock, orAtomicInteger. Consider which approach is most efficient for this specific use case.
There are no comments for now.