15 · Best Practices & Patterns
Level: Advanced
Pre-reading: All previous sections
Design Patterns for Concurrency
1. Producer-Consumer
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
BlockingQueue<Task> queue = new LinkedBlockingQueue<>();
// Producer
new Thread(() -> {
for (int i = 0; i < 100; i++) {
try {
queue.put(new Task(i));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
// Consumers
for (int i = 0; i < 4; i++) {
new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Task task = queue.take();
process(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
}
2. Worker Pool
ExecutorService pool = Executors.newFixedThreadPool(10);
// Submit tasks
for (Task task : tasks) {
pool.submit(() -> processTask(task));
}
// Graceful shutdown
pool.shutdown();
try {
if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
pool.shutdownNow();
}
} catch (InterruptedException e) {
pool.shutdownNow();
}
3. Future for Result Aggregation
List<Future<Integer>> futures = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
final int taskId = i;
futures.add(executor.submit(() -> compute(taskId)));
}
// Aggregate results
int total = 0;
for (Future<Integer> future : futures) {
try {
total += future.get();
} catch (ExecutionException e) {
System.err.println("Task failed: " + e.getCause());
}
}
4. Read-Write Separation
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class Cache<K, V> {
private final Map<K, V> data = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
V get(K key) {
lock.readLock().lock();
try {
return data.get(key); // Many readers
} finally {
lock.readLock().unlock();
}
}
void put(K key, V value) {
lock.writeLock().lock();
try {
data.put(key, value); // Single writer
} finally {
lock.writeLock().unlock();
}
}
}
5. Fire-and-Forget Tasks
// Submit without waiting for result
executor.submit(() -> sendNotification(user));
// Fire-and-forget, but with exception handling
executor.submit(() -> {
try {
dangerousOperation();
} catch (Exception e) {
logger.error("Operation failed", e);
}
});
Anti-Patterns (What NOT to Do)
❌ Anti-Pattern 1: Creating Threads in a Loop
// ❌ BAD: Creates too many threads
for (int i = 0; i < 1000; i++) {
new Thread(() -> task(i)).start(); // 1000 threads!
}
// ✅ GOOD: Use thread pool
ExecutorService pool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.submit(() -> task(i)); // 10 threads handle all
}
❌ Anti-Pattern 2: Relying on Thread.sleep()
// ❌ BAD: Busy-waiting with sleep
while (!done) {
Thread.sleep(100); // CPU churn
}
// ✅ GOOD: Use CountDownLatch
CountDownLatch latch = new CountDownLatch(1);
// ... task calls latch.countDown() when done
latch.await(); // Blocks efficiently
❌ Anti-Pattern 3: Synchronizing Large Methods
// ❌ BAD: Entire method locked
synchronized void process() {
readFromDisk(); // Slow, lock held entire time
updateMemory();
writeToDisk(); // Others blocked during all I/O
}
// ✅ GOOD: Lock only critical sections
void process() {
readFromDisk(); // No lock
synchronized (THIS) {
updateMemory(); // Lock only here
}
writeToDisk(); // No lock
}
❌ Anti-Pattern 4: Catching InterruptedException Silently
// ❌ BAD: Swallows interruption
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// ❌ Do nothing — thread won't know it was interrupted
}
// ✅ GOOD: Restore interrupt flag
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Restore
// or throw RuntimeException
}
❌ Anti-Pattern 5: Using Thread.stop()
// ❌ BAD: Deprecated and dangerous
thread.stop(); // Leaves resources uncleaned, unpredictable
// ✅ GOOD: Use interrupt + flag
thread.interrupt();
// ... in thread: check isInterrupted() and clean up
Performance Optimization
Choose Right Synchronization
Low Contention: synchronized > ReentrantLock > Atomic
High Contention: Atomic > ReentrantLock > synchronized
Read-Heavy: ReadWriteLock > synchronized
Just Visibility: volatile > synchronized
Lock Granularity Trade-Off
Coarse Locks: Simple ┌─── Choose Based on ──┐ Less Lock Contention
Easy │ Your Access │ Complexity
Wrong │ Patterns │ Possible Scalability
Fine Locks: Complex└────────────────────┘
Collection Choice
Few Threads: synchronizedMap / synchronizedList
Many Readers: CopyOnWriteArrayList
Many Writers: ConcurrentHashMap
Queue Operations: BlockingQueue / ConcurrentLinkedQueue
Common Pitfalls & Solutions
| Pitfall | Problem | Solution |
|---|---|---|
| Lost notifies | notify() called before wait() | Use Lock + Condition |
| Spurious wakeups | wait() returns unexpectedly | Check condition in loop: while (!condition) |
| Stale reads | Cached value not updated | Use volatile or synchronize |
| Deadlock | Circular lock dependency | Use consistent lock ordering |
| Memory leaks | ThreadLocal not cleaned | Use ScopedValue or manual cleanup |
| Thundering herd | All threads wake, only one proceeds | Use notifyOne() or Condition.signal() |
Concurrency Testing
Use jcstress for Concurrency Tests
# Java Concurrency Stress test framework
mvn archetype:generate -DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jcstress \
-DarchetypeArtifactId=jcstress-test-archetype
Basic Stress Test Pattern
@JCStressTest
@Outcome(id = "1", expect = ACCEPTABLE, desc = "Counter incremented once")
@Outcome(id = "0", expect = FORBIDDEN, desc = "No increment (race condition)")
@State
public class CounterRaceTest {
private int counter = 0;
@Actor
public void actor1() { counter++; }
@Actor
public void actor2() { counter++; }
@Arbiter
public void arbiter(I_Result r) {
r.r1 = counter;
}
}
Key Takeaways
| Principle | Application |
|---|---|
| Use executors | Not raw threads |
| Preferred: minimal locks | Immutability, volatile, ConcurrentHashMap |
| Control scope | StructuredTaskScope |
| Interrupt properly | Check/restore interrupt flag |
| Test concurrency | Use jcstress or stress testing |
| Monitor production | Thread count, deadlocks, GC pauses |
📚 Read the Original Blog Post
For more details and examples, read:
- Best Practices & Patterns — Production-ready patterns and anti-patterns
What's the best way to stop a thread?
Use interrupt() and check isInterrupted(). The thread checks the flag and cleans up voluntarily.
Should I always lock at method level?
No. Lock only the minimum code needed (critical sections). Fine-grained locking allows better concurrency.
Why use executors instead of new Thread()?
Automatic pooling, reuse, resource limits, easy shutdown, exception handling, Future support.