1. The Hook (The "Byte-Sized" Intro)
- In a Nutshell: Best practices for concurrency: (1) Minimize shared state (immutability first), (2) Use higher-level utilities (ExecutorService, Concurrent collections), (3) Synchronize minimally (reduce lock scope), (4) Avoid deadlocks (lock ordering), (5) Prefer immutable objects, (6) Document thread safety (@ThreadSafe, @NotThreadSafe), (7) Test thoroughly (stress tests, race condition detection).
- Golden rule: Shared mutable state = source of all concurrency bugs!
Think of highway traffic. Best practices = traffic rules prevent chaos. Lane discipline (threads stay in lane), merge properly (synchronization), no gridlock (deadlock prevention), clear signage (documentation)!
2. Conceptual Clarity (The "Simple" Tier)
💡 The Analogy: The Shared Kitchen
- Immutability: Pre-packaged meals (no modification needed)
- Synchronization: One chef at stove at a time (mutual exclusion)
- Thread-local: Each chef has personal cutting board (no sharing)
3. Technical Mastery (The "Deep Dive")
10 Golden Rules
- Minimize Shared Mutable State
- Prefer Immutability
- Use Concurrent Collections
- Use Executors, Not Threads
- Synchronize Minimally
- Avoid Nested Locks
- Document Thread Safety
- Use ThreadLocal for Thread-Specific Data
- Prefer Atomic Variables Over Locks
- Test Concurrency Thoroughly
4. Interactive & Applied Code
java
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.*;
public class BestPracticesDemo {
// ✅ BEST PRACTICE 1: Immutable objects
final class ImmutableUser {
private final String name;
private final int age;
public ImmutableUser(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
// All fields final, no setters = thread-safe!
}
// ✅ BEST PRACTICE 2: Use concurrent collections
static class SafeCache {
// ❌ BAD
// private Map<String, String> cache = new HashMap<>();
// ✅ GOOD
private ConcurrentHashMap<String, String> cache = new ConcurrentHashMap<>();
public String get(String key) {
return cache.computeIfAbsent(key, k -> expensiveOperation(k));
}
private String expensiveOperation(String key) {
return "Value for " + key;
}
}
// ✅ BEST PRACTICE 3: Minimize synchronized scope
static class Counter {
private int count = 0;
// ❌ BAD: Entire method locked
public synchronized void incrementBad() {
doExpensivePreWork(); // Doesn't need lock!
count++;
doExpensivePostWork(); // Doesn't need lock!
}
// ✅ GOOD: Only critical section locked
public void incrementGood() {
doExpensivePreWork();
synchronized(this) {
count++; // Minimal lock scope
}
doExpensivePostWork();
}
private void doExpensivePreWork() {}
private void doExpensivePostWork() {}
}
// ✅ BEST PRACTICE 4: Use ThreadLocal
static class DateFormatHolder {
// ❌ BAD: SimpleDateFormat is NOT thread-safe
// private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
// ✅ GOOD: ThreadLocal gives each thread its own instance
private static final ThreadLocal<java.text.SimpleDateFormat> format =
ThreadLocal.withInitial(() -> new java.text.SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return format.get().format(date);
}
}
// ✅ BEST PRACTICE 5: Document thread safety
/**
* Thread-safe counter using AtomicInteger.
*
* @ThreadSafe
*/
static class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int get() {
return count.get();
}
}
// ✅ BEST PRACTICE 6: Use Executors
static class TaskProcessor {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void processTask(Runnable task) {
// ❌ BAD: new Thread(task).start();
// ✅ GOOD: Reuse threads
executor.submit(task);
}
public void shutdown() {
executor.shutdown();
}
}
// ✅ BEST PRACTICE 7: Avoid deadlock with lock ordering
static class BankTransfer {
static class Account {
private int balance;
private final int id;
Account(int id, int balance) {
this.id = id;
this.balance = balance;
}
}
// ❌ BAD: Can deadlock
public void transferBad(Account from, Account to, int amount) {
synchronized(from) {
synchronized(to) {
from.balance -= amount;
to.balance += amount;
}
}
}
// ✅ GOOD: Always lock in same order (by ID)
public void transferGood(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized(first) {
synchronized(second) {
from.balance -= amount;
to.balance += amount;
}
}
}
}
public static void main(String[] args) {
System.out.println("Best practices demonstrated in code above");
}
}
// Common pitfalls to avoid
class CommonPitfalls {
// ❌ PITFALL 1: Double-checked locking without volatile
private static Object instanceBad;
public static Object getInstanceBad() {
if (instanceBad == null) { // Check 1
synchronized(CommonPitfalls.class) {
if (instanceBad == null) { // Check 2
instanceBad = new Object(); // ❌ Can see partially constructed object!
}
}
}
return instanceBad;
}
// ✅ FIX: Use volatile
private static volatile Object instanceGood;
public static Object getInstanceGood() {
if (instanceGood == null) {
synchronized(CommonPitfalls.class) {
if (instanceGood == null) {
instanceGood = new Object(); // ✅ volatile prevents reordering
}
}
}
return instanceGood;
}
// ❌ PITFALL 2: Forgetting to shutdown executor
public void pitfall2() {
ExecutorService executor = Executors.newFixedThreadPool(10);
executor.submit(() -> System.out.println("Task"));
// ❌ Forgot shutdown - threads leak!
}
// ✅ FIX: Always shutdown
public void fix2() {
ExecutorService executor = Executors.newFixedThreadPool(10);
try {
executor.submit(() -> System.out.println("Task"));
} finally {
executor.shutdown(); // ✅ Cleanup
}
}
}5. The Comparison & Decision Layer
Concurrency Strategy Decision Tree
graph TD
A{Need concurrency?}
A -- No shared state --> B[Use Immutability]
A -- Shared state --> C{Read-heavy?}
C -- Yes --> D[CopyOnWriteArrayList]
C -- No --> E{Simple counter?}
E -- Yes --> F[AtomicInteger]
E -- No --> G{Complex state?}
G -- Yes --> H[synchronized / Lock]
G -- No --> I[ConcurrentHashMap]
6. The "Interview Corner" (The Edge)
The "Killer" Interview Question: "Why is this code broken?"
java
class LazyInit {
private ExpensiveObject instance;
public ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject(); // ❌ Not thread-safe!
}
return instance;
}
}Answer: Race condition—two threads can both see null and create two instances!
Solutions:
java
// Solution 1: Eager initialization (simplest)
private static final ExpensiveObject instance = new ExpensiveObject();
// Solution 2: Synchronized method (thread-safe, slower)
public synchronized ExpensiveObject getInstance() { ... }
// Solution 3: Double-checked locking (fast, correct)
private volatile ExpensiveObject instance;
public ExpensiveObject getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new ExpensiveObject();
}
}
}
return instance;
}
// Solution 4: Holder pattern (best!)
private static class Holder {
static final ExpensiveObject instance = new ExpensiveObject();
}
public static ExpensiveObject getInstance() {
return Holder.instance;
}Pro-Tips:
- Favor composition over inheritance with synchronized classes:
java
// ❌ BAD: Exposes all Vector methods
class MyList extends Vector {
// What if someone calls add() directly?
}
// ✅ GOOD: Encapsulate
class MyList {
private final List list = Collections.synchronizedList(new ArrayList<>());
// Expose only what you need
}- Use annotations to document:
java
@ThreadSafe // Immut or properly synchronized
@NotThreadSafe // Requires external synchronization
@Immutable // All fields final
@GuardedBy("lock") // Must hold lock to access- Test with Thread.sleep() injections:
java
// Inject delays to expose race conditions
count++;
Thread.sleep(1); // Force context switch