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
88: Atomic Variables
I've seen this a dozen times in code reviews: a developer is building a multi-threaded counter, they realize they're having race conditions, and their "fix" is to simply slap the volatile keyword on the integer. They assume that because volatile ensures every thread sees the most recent value, the operation is now thread-safe. It's a logical leap, but it's a dangerous one.
The Volatile Trap: Thinking Visibility Equals Atomicity
Here is the core of the problem. Let's look at a simple request counter. You might think this is safe because of volatile:
public class RequestCounter {
private volatile int count = 0;
public void increment() {
count++; // This is where the disaster happens
}
public int getCount() {
return count;
}
}
At first glance, this looks fine. But count++ isn't one operation; it's three: read the value, add one to it, and write it back. If two threads read the value 10 at the exact same time, they both calculate 11, and they both write 11 back. You just lost an update. volatile ensures that the threads see the latest value, but it does absolutely nothing to stop them from stepping on each other's toes during that read-modify-write cycle.
Leveraging AtomicInteger for Lock-Free Updates
To fix this without the heavy overhead of synchronized blocks—which essentially put your threads in a waiting line—Java gives us the java.util.concurrent.atomic package. The most common tool here is AtomicInteger.
When you use AtomicInteger, you aren't just wrapping an int; you're using a set of methods designed to be "atomic," meaning they happen as a single, indivisible unit of work. Here is how I would actually write that counter:
import java.util.concurrent.atomic.AtomicInteger;
public class RequestCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // This is the atomic equivalent of count++
}
public int getCount() {
return count.get();
}
}
Now, no matter how many threads hit increment(), you will never lose a count. You don't have to worry about manual locking or memory barriers because the heavy lifting is handled at the hardware level.
The Secret Sauce: Compare-And-Swap (CAS)
You might be wondering how Java does this without using synchronized. It uses a mechanism called Compare-And-Swap (CAS). Instead of locking the variable, the CPU does something like this:
- "I think the current value is 10."
- "I want to change it to 11."
- "If the value is still 10 when I actually go to write it, go ahead and change it. If it's not 10 anymore, someone beat me to it—fail the operation and let me try again."
This is called "optimistic locking." It assumes that most of the time, there won't be a collision. If there is, it just loops and tries again. This is significantly faster than putting a thread to sleep and waking it up, which is what happens when you use traditional locks.
Beyond AtomicInteger, you'll find AtomicLong for larger numbers, AtomicBoolean for flags, and AtomicReference when you need to atomically update an entire object. Use them whenever you have a single shared variable that needs to be updated across threads, and you'll save yourself from a world of debugging pain.
📋 Practical Task
Implementing a Thread-Safe Global Request Tracker
You are building a monitoring system for a high-traffic web server. You need to create a class called GlobalRequestTracker that tracks two things across multiple concurrent threads: the total number of requests received (which could be very large) and the current number of active requests (which fluctuates as requests start and finish).
Requirements:
- Use
AtomicLongfor the total request count to prevent overflow. - Use
AtomicIntegerfor the active request count. - Implement a method
recordRequestStart()that increments both the total and the active counts. - Implement a method
recordRequestEnd()that decrements only the active count. - Implement a method
getStats()that returns a string summarizing the current totals.
Ensure that your implementation is entirely lock-free by avoiding the synchronized keyword.
There are no comments for now.