Lesson Completion
Back to course

Concurrency Best Practices: Writing Thread-Safe Code

Advanced
25 minutes4.9Java

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

  1. Minimize Shared Mutable State
  2. Prefer Immutability
  3. Use Concurrent Collections
  4. Use Executors, Not Threads
  5. Synchronize Minimally
  6. Avoid Nested Locks
  7. Document Thread Safety
  8. Use ThreadLocal for Thread-Specific Data
  9. Prefer Atomic Variables Over Locks
  10. 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:

  1. 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 }
  1. 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
  1. Test with Thread.sleep() injections:
java
// Inject delays to expose race conditions count++; Thread.sleep(1); // Force context switch

Topics Covered

Java FundamentalsConcurrency

Tags

#java#multithreading#threads#concurrency#parallelism

Last Updated

2025-02-01