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
131: Testing Concurrent Code
Imagine you're managing a busy coffee shop with two baristas. Most of the time, they work perfectly. But every once in a while, they both reach for the same milk pitcher at the exact same millisecond, bump into each other, and spill latte art all over the counter. If you stand there and watch them for five minutes, you might see nothing wrong. The shop looks efficient. But if you watch them for ten hours during the morning rush, that collision is inevitable.
Testing concurrent code is exactly like watching those baristas. Your code might pass a unit test 99 times out of 100 because the threads happened to execute in a "safe" order. But in production, under heavy load, the timing shifts, the threads collide, and you get a Heisenbug—a bug that disappears the moment you try to observe it or put a debugger on it.
Here is how that analogy maps to your Java code:
- The Baristas: These are your threads.
- The Milk Pitcher: This is your shared state (a variable, a Map, or a database record).
- The Spill: This is your race condition or data corruption.
- The 5-minute observation: This is a standard JUnit test that runs once and passes by sheer luck.
The Lie of the Passing Test
I've seen countless developers write a test for a concurrent class, run it once, see a green checkmark, and push to production. The problem is that concurrent bugs are non-deterministic. If you're testing a thread-safe counter, a simple loop might not actually create enough "contention" to trigger a race condition.
// This test is dangerous because it often passes even if the code is BROKEN
@Test
public void testConcurrentIncrement() {
SharedCounter counter = new SharedCounter();
Runnable task = () -> {
for(int i = 0; i < 1000; i++) counter.increment();
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
assertEquals(2000, counter.getCount());
}
In the example above, t1 might finish its entire loop before t2 even fully wakes up. There's no overlap, no "collision," and the test passes. But that doesn't mean your code is thread-safe; it just means your test wasn't aggressive enough.
Forcing the Collision with CountDownLatch
To actually test concurrency, you have to force the threads to start at the exact same moment. I usually reach for a CountDownLatch for this. Think of it like a starting pistol at a race. You make all your threads wait at the line, and then you fire the pistol, releasing them all simultaneously to maximize the chance of a collision.
Look at how this changes the game:
@Test
public void testConcurrentIncrementWithLatch() throws InterruptedException {
SharedCounter counter = new SharedCounter();
int threadCount = 10;
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startSignal.await(); // Wait for the pistol
for(int j = 0; j < 1000; j++) counter.increment();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
doneSignal.countDown();
}
}).start();
}
startSignal.countDown(); // FIRE! All threads start now.
doneSignal.await(); // Wait for everyone to finish
assertEquals(10000, counter.getCount());
}
By using startSignal.await(), we ensure that no thread starts incrementing until we explicitly call countDown(). This creates a massive spike of contention on the counter object, which is exactly where race conditions love to hide.
Stop Using Thread.sleep() for Coordination
One thing I want to emphasize: please, stop using Thread.sleep() to "give the other thread time to finish." It's a gamble. On your fast developer laptop, 50ms might be enough. On a congested CI/CD server, 50ms might be an eternity too short. Always use synchronization primitives like CountDownLatch, CyclicBarrier, or CompletableFuture.allOf(). They are deterministic; sleep() is just a prayer.
📋 Practical Task
Exercise: Detecting Race Conditions in a Concurrent Credit Wallet
You have been given a CreditWallet class that is supposed to be thread-safe, but the previous developer used a plain int for the balance and a non-synchronized method for withdrawals. Your task is to prove that this class is broken by writing a high-contention stress test.
The Setup:
public class CreditWallet {
private int balance = 1000;
public void withdraw(int amount) {
if (balance >= amount) {
// Simulate a tiny processing delay to make the race condition easier to hit
try { Thread.sleep(1); } catch (InterruptedException e) {}
balance -= amount;
}
}
public int getBalance() {
return balance;
}
}
Your Goal:
- Create a JUnit test case.
- Use a
CountDownLatchto launch 10 concurrent threads. - Each thread should attempt to withdraw 100 credits from a wallet that starts with 1000.
- If the code were thread-safe, the final balance should be 0. However, because of the race condition, you will likely see a balance greater than 0 (because multiple threads read the same balance before any of them updated it).
- Print the final balance to the console and assert that it is 0. The test should fail, proving the bug exists.
There are no comments for now.