Loading learning content...
Java occupies a unique position in the threading landscape: a high-level, object-oriented language running on a virtual machine, yet providing direct access to operating system threads with rich concurrency primitives. Since Java 1.0 (1996), multithreading has been a first-class language feature, not a library addition—threads are built into the language specification and memory model.
Understanding Java threads requires grasping three interconnected layers:
This page provides comprehensive coverage of Java threading, from fundamental constructs through modern concurrency utilities, with deep attention to the memory model semantics that distinguish Java threads from native threading.
By the end of this page, you will understand the complete Java threading model: creating and managing threads, the Thread class API, synchronization with monitors and locks, the Java Memory Model's visibility guarantees, and how the JVM implements threads across different platforms. You will be prepared to write correct, efficient concurrent Java code.
Java's threading model integrates threads directly into the object-oriented paradigm. Every thread is represented by an instance of java.lang.Thread (or a subclass), and thread behavior is defined by implementing the Runnable interface.
Unlike C's POSIX threads (opaque identifiers) or Windows threads (handles), Java threads are objects:
This object-oriented approach makes thread management more intuitive for Java developers but obscures the underlying native thread mechanics.
| State | Description | Transition Triggers |
|---|---|---|
| NEW | Thread created but not started | Thread t = new Thread() |
| RUNNABLE | Running or ready to run | t.start() called; may be executing or waiting for CPU |
| BLOCKED | Waiting to acquire monitor lock | Entering synchronized block held by another thread |
| WAITING | Waiting indefinitely for another thread | Object.wait(), Thread.join(), LockSupport.park() |
| TIMED_WAITING | Waiting with timeout | Thread.sleep(ms), wait(ms), join(ms) |
| TERMINATED | Thread has completed execution | run() method returned or uncaught exception |
Modern JVM implementations (HotSpot, OpenJ9, GraalVM) use a 1:1 mapping between Java threads and native operating system threads:
This means Java threads enjoy true parallelism on multiprocessor systems and are scheduled by the OS kernel, not by the JVM. The JVM's role is to:
Early JVMs (Java 1.0-1.2 on some platforms) used 'green threads'—user-level threads scheduled by the JVM itself. This approach couldn't exploit multiple CPUs and had blocking I/O problems. All modern JVMs use native threads. The term 'green threads' now refers to user-level threading in general (like Go goroutines before multiplexing).
Java provides three primary patterns for creating threads, each with distinct use cases and trade-offs. Understanding when to use each pattern is fundamental to Java concurrent programming.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Running in: " + getName());
}
}
// Usage
new MyThread().start();
Pros: Direct access to Thread methods (getName, setName, etc.) Cons: Can't extend another class; tight coupling
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Running in: " +
Thread.currentThread().getName());
}
}
// Usage
new Thread(new MyTask()).start();
Pros: Can extend another class; separates task from execution Cons: No return value, no checked exceptions
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return computeResult();
}
}
// Usage
Future<Integer> future = executor.submit(new MyCallable());
Integer result = future.get();
Pros: Return values, checked exceptions, integrates with executors Cons: Requires executor framework; more complex
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
import java.util.concurrent.*;import java.util.List;import java.util.ArrayList; /** * Comprehensive Thread Creation Patterns in Java */public class ThreadCreationPatterns { /** * Pattern 1: Thread Subclass * Best for: Simple cases where you need Thread methods directly */ static class WorkerThread extends Thread { private final int workerId; public WorkerThread(int id) { super("Worker-" + id); // Set thread name this.workerId = id; } @Override public void run() { System.out.println(getName() + " starting work"); try { // Simulate work Thread.sleep(1000); } catch (InterruptedException e) { System.out.println(getName() + " interrupted"); Thread.currentThread().interrupt(); // Preserve interrupt status return; } System.out.println(getName() + " completed"); } } /** * Pattern 2: Runnable Implementation * Best for: Tasks that need to extend another class, or be submitted to executors */ static class DataProcessor implements Runnable { private final String data; private volatile boolean completed = false; // Visibility! public DataProcessor(String data) { this.data = data; } @Override public void run() { System.out.println("Processing: " + data + " on " + Thread.currentThread().getName()); // Process data... try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } completed = true; } public boolean isCompleted() { return completed; } } /** * Pattern 3: Callable with Future * Best for: Tasks that return results or throw exceptions */ static class ComputeTask implements Callable<Long> { private final int n; public ComputeTask(int n) { this.n = n; } @Override public Long call() throws Exception { // Compute factorial (just an example) if (n < 0) { throw new IllegalArgumentException("n must be >= 0"); } long result = 1; for (int i = 2; i <= n; i++) { result *= i; // Check for interruption in long computations if (Thread.currentThread().isInterrupted()) { throw new InterruptedException("Computation cancelled"); } } return result; } } /** * Pattern 4: Lambda/Method Reference (Java 8+) * Best for: Short tasks, inline definitions */ public static void lambdaPatterns() { // Lambda implementing Runnable Thread t1 = new Thread(() -> { System.out.println("Lambda thread running"); }); t1.start(); // Method reference Thread t2 = new Thread(ThreadCreationPatterns::doWork); t2.start(); // With ExecutorService ExecutorService executor = Executors.newFixedThreadPool(4); // Submit Runnable lambda executor.submit(() -> System.out.println("Runnable task")); // Submit Callable lambda (note: returns value) Future<String> future = executor.submit(() -> { Thread.sleep(100); return "Callable result"; }); try { String result = future.get(1, TimeUnit.SECONDS); System.out.println("Got: " + result); } catch (Exception e) { e.printStackTrace(); } executor.shutdown(); } private static void doWork() { System.out.println("Method reference thread running"); } /** * Demonstration: Proper Thread Creation and Joining */ public static void main(String[] args) throws Exception { // Create and start multiple threads List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 5; i++) { Thread t = new WorkerThread(i); threads.add(t); t.start(); // Starts the thread! } // Wait for all threads to complete for (Thread t : threads) { t.join(); // Blocks until t terminates } System.out.println("All workers completed"); // Callable example with timeout ExecutorService executor = Executors.newCachedThreadPool(); Future<Long> future = executor.submit(new ComputeTask(20)); try { Long result = future.get(5, TimeUnit.SECONDS); System.out.println("20! = " + result); } catch (TimeoutException e) { future.cancel(true); // Interrupt the task System.out.println("Computation timed out"); } executor.shutdown(); }}A common beginner mistake is calling run() directly instead of start(). Calling run() executes the method in the CURRENT thread—no new thread is created! Always call start(), which creates a new native thread and invokes run() on that thread. The Thread can only be started once; calling start() twice throws IllegalThreadStateException.
The java.lang.Thread class provides a comprehensive API for thread management. Understanding these methods—and their subtle behaviors—is essential for effective concurrent programming.
Every Java thread has a name and a unique ID:
thread.setPriority(Thread.NORM_PRIORITY); // 1-10, default 5
thread.setDaemon(true); // Must be set BEFORE start()
Priorities are hints to the scheduler; behavior is platform-dependent. Windows maps priorities to Windows priority levels; Linux may ignore them entirely depending on scheduler configuration.
Daemon threads don't prevent JVM shutdown. When all non-daemon threads terminate, the JVM exits—daemon threads are abruptly terminated without running finally blocks.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
/** * Comprehensive Thread API Usage Examples */public class ThreadAPIExample { /** * Proper interrupt handling pattern * This is the correct way to handle InterruptedException */ static class InterruptibleWorker implements Runnable { @Override public void run() { System.out.println("Worker started"); try { while (!Thread.currentThread().isInterrupted()) { // Do some work doWork(); // Sleep (interruptible operation) Thread.sleep(100); } } catch (InterruptedException e) { // InterruptedException clears interrupt flag! // We must restore it for callers to see Thread.currentThread().interrupt(); System.out.println("Worker interrupted during sleep"); } System.out.println("Worker terminating gracefully"); } private void doWork() { // CPU-bound work should periodically check interrupt flag for (int i = 0; i < 1000; i++) { if (Thread.currentThread().isInterrupted()) { return; // Exit early if interrupted } // ... computation ... } } } /** * Joining with timeout */ public static void joinWithTimeout() throws InterruptedException { Thread worker = new Thread(() -> { try { Thread.sleep(5000); // Simulate long work } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); worker.start(); // Wait at most 2 seconds worker.join(2000); if (worker.isAlive()) { System.out.println("Worker still running, interrupting..."); worker.interrupt(); worker.join(); // Wait for it to actually finish } System.out.println("Worker state: " + worker.getState()); } /** * Thread information and debugging */ public static void threadInfo() { Thread current = Thread.currentThread(); System.out.println("Thread Information:"); System.out.println(" Name: " + current.getName()); System.out.println(" ID: " + current.getId()); System.out.println(" Priority: " + current.getPriority()); System.out.println(" State: " + current.getState()); System.out.println(" Is Daemon: " + current.isDaemon()); System.out.println(" Is Alive: " + current.isAlive()); System.out.println(" Thread Group: " + current.getThreadGroup().getName()); // Stack trace System.out.println(" Stack trace:"); for (StackTraceElement element : current.getStackTrace()) { System.out.println(" " + element); } } /** * Daemon threads example */ public static void daemonThreadExample() { Thread daemon = new Thread(() -> { while (true) { System.out.println("Daemon running..."); try { Thread.sleep(500); } catch (InterruptedException e) { break; } } }); // MUST set daemon BEFORE start() daemon.setDaemon(true); daemon.setName("BackgroundDaemon"); daemon.start(); // When main exits, daemon is killed abruptly System.out.println("Main thread exiting, daemon will be killed"); } /** * All threads dump (for debugging) */ public static void dumpAllThreads() { System.out.println("\n=== All Thread Stack Traces ==="); Thread.getAllStackTraces().forEach((thread, stackTrace) -> { System.out.println("\n" + thread.getName() + " (id=" + thread.getId() + ", state=" + thread.getState() + "):"); for (StackTraceElement element : stackTrace) { System.out.println(" " + element); } }); }}Thread.stop(), suspend(), and resume() are deprecated and should NEVER be used. stop() can leave objects in inconsistent states (it releases all monitors abruptly). suspend() can cause deadlocks (thread holds lock while suspended). Use interrupt() and cooperative termination patterns instead.
Java provides built-in synchronization through the synchronized keyword, which implements monitor locks (mutexes). Every Java object can serve as a lock, and the synchronized keyword provides both mutual exclusion and memory visibility guarantees.
Each Java object has an associated monitor (also called intrinsic lock):
// Synchronized instance method: locks 'this'
public synchronized void instanceMethod() { ... }
// Synchronized static method: locks Class object
public static synchronized void staticMethod() { ... }
// Synchronized block: locks specified object
public void blockMethod() {
synchronized(lockObject) { ... }
}
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
import java.util.ArrayList;import java.util.List; /** * Monitor Synchronization Patterns in Java */public class SynchronizationPatterns { /** * Basic synchronized method pattern * Every method shares object's single lock */ static class Counter { private int count = 0; public synchronized void increment() { count++; // Atomic under lock } public synchronized int get() { return count; } // These two methods share the same lock! // Only one can execute at a time on same instance } /** * Synchronized block with minimal scope * Better performance: lock held for shorter duration */ static class ImprovedCounter { private final Object lock = new Object(); // Private lock object private int count = 0; public void increment() { // Pre-computation outside lock int incrementBy = calculateIncrement(); synchronized(lock) { // Lock only what's needed count += incrementBy; } // Post-processing outside lock logIncrement(); } private int calculateIncrement() { return 1; } private void logIncrement() { /* ... */ } } /** * Multiple lock objects for independent data * Increases concurrency by reducing contention */ static class MultiLockExample { private final Object readLock = new Object(); private final Object writeLock = new Object(); private int readCount = 0; private int writeCount = 0; public void recordRead() { synchronized(readLock) { // Doesn't block writers readCount++; } } public void recordWrite() { synchronized(writeLock) { // Doesn't block readers writeCount++; } } } /** * wait()/notify() for thread coordination * Classic producer-consumer pattern */ static class BoundedBuffer<T> { private final List<T> items = new ArrayList<>(); private final int capacity; public BoundedBuffer(int capacity) { this.capacity = capacity; } public synchronized void put(T item) throws InterruptedException { // Wait while buffer is full while (items.size() == capacity) { wait(); // Releases lock, waits for notify } items.add(item); notifyAll(); // Wake waiting consumers } public synchronized T take() throws InterruptedException { // Wait while buffer is empty while (items.isEmpty()) { wait(); // Releases lock, waits for notify } T item = items.remove(0); notifyAll(); // Wake waiting producers return item; } public synchronized int size() { return items.size(); } } /** * CRITICAL: Always use while loop with wait()! * Spurious wakeups can occur; condition may not be true */ static class CorrectWaitPattern { private boolean conditionMet = false; public synchronized void waitForCondition() throws InterruptedException { // CORRECT: while loop while (!conditionMet) { wait(); } // Condition is definitely true here /* * WRONG: * if (!conditionMet) { wait(); } * * After spurious wakeup, condition might still be false! */ } public synchronized void signalCondition() { conditionMet = true; notifyAll(); // Prefer notifyAll unless specific reason } } /** * Static synchronized method locks the Class object */ static class StaticSyncExample { private static int sharedCounter = 0; // Locks StaticSyncExample.class public static synchronized void incrementStatic() { sharedCounter++; } // Equivalent: public static void incrementStaticEquivalent() { synchronized(StaticSyncExample.class) { sharedCounter++; } } }}Prefer notifyAll() over notify(). notify() wakes only ONE waiting thread, chosen arbitrarily. If that thread can't proceed (condition not met for it), all threads remain blocked. notifyAll() is safer: all threads wake, check their conditions, and proceed or re-wait appropriately. Use notify() only when all waiting threads are equivalent (e.g., worker pool).
The Java Memory Model (JMM), specified in JSR-133 (Java 5) and refined since, defines how threads interact through memory. It's one of the most important—and most misunderstood—aspects of Java concurrency.
Without proper synchronization, writes made by one thread may never be visible to other threads:
The JMM defines when writes by one thread are guaranteed to be visible to reads in another thread.
The core concept in JMM is happens-before: if action A happens-before action B, then A's effects are visible to B. Key happens-before rules:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
/** * Java Memory Model: Visibility and Ordering */public class JavaMemoryModel { /** * BROKEN: No visibility guarantee * Thread 2 may never see ready=true, even if Thread 1 sets it */ static class BrokenVisibility { private boolean ready = false; private int number = 0; public void writer() { number = 42; ready = true; // May be reordered before number=42! } public void reader() { while (!ready) { // May loop forever! CPU may cache ready=false Thread.yield(); } // May print 0 even if we see ready=true (due to reordering)! System.out.println(number); } } /** * FIXED with volatile: Full visibility * volatile prevents reordering and ensures visibility */ static class VolatileVisibility { private volatile boolean ready = false; private int number = 0; public void writer() { number = 42; ready = true; // Volatile write: cannot be reordered // Also flushes all preceding writes } public void reader() { while (!ready) { Thread.yield(); } // Guaranteed to see 42 // Volatile read creates happens-before with volatile write System.out.println(number); // Always prints 42 } } /** * FIXED with synchronization: Mutual exclusion + visibility */ static class SynchronizedVisibility { private boolean ready = false; private int number = 0; private final Object lock = new Object(); public void writer() { synchronized(lock) { number = 42; ready = true; } // Lock release flushes writes } public void reader() { synchronized(lock) { // Lock acquire reads fresh values while (!ready) { try { lock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } System.out.println(number); // Sees 42 } } } /** * Double-Checked Locking: The classic JMM pitfall */ static class Singleton { // MUST be volatile for safe double-checked locking! private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { // First check (no lock) synchronized(Singleton.class) { if (instance == null) { // Second check (with lock) instance = new Singleton(); } } } return instance; } /* * Without volatile, this is BROKEN! * * Why? The constructor and assignment can be reordered: * 1. Allocate memory * 2. Assign reference to instance <- Other thread sees non-null! * 3. Call constructor <- But object isn't initialized! * * With volatile, step 2 cannot happen before step 3. */ } /** * Safe publication idioms */ static class SafePublication { // 1. Static initializer (JVM guarantees safety) private static final Object STATIC_INIT = createObject(); // 2. Volatile field private volatile Object volatileField; // 3. Final field (if object is effectively immutable) private final Object finalField; // 4. Guarded by lock private Object guardedField; private final Object lock = new Object(); public SafePublication() { this.finalField = createObject(); } public void publishSafely(Object obj) { // Option 1: volatile this.volatileField = obj; // Option 2: synchronized synchronized(lock) { this.guardedField = obj; } } private static Object createObject() { return new Object(); } }}While volatile ensures visibility and ordering, it does NOT provide atomicity for compound operations. 'count++' on a volatile int is still a race condition (read-modify-write). For atomic compound operations, use synchronized blocks or java.util.concurrent.atomic classes (AtomicInteger, AtomicReference, etc.).
Java 5 introduced the java.util.concurrent package, revolutionizing Java concurrent programming with high-level utilities that are safer, faster, and more scalable than synchronized blocks. Understanding these utilities is essential for modern Java development.
The package provides:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
import java.util.concurrent.*;import java.util.concurrent.atomic.*;import java.util.concurrent.locks.*; /** * Modern Java Concurrency Utilities (java.util.concurrent) */public class ModernConcurrency { /** * ReentrantLock: More flexible than synchronized * - Interruptible lock acquisition * - Timeout-based acquisition * - Non-block try-lock * - Condition variables */ static class ReentrantLockExample { private final ReentrantLock lock = new ReentrantLock(); private int value; public void update(int delta) throws InterruptedException { // Can be interrupted while waiting for lock lock.lockInterruptibly(); try { value += delta; } finally { lock.unlock(); // Always in finally! } } public boolean tryUpdate(int delta, long timeout, TimeUnit unit) throws InterruptedException { // Timeout-based lock acquisition if (lock.tryLock(timeout, unit)) { try { value += delta; return true; } finally { lock.unlock(); } } return false; // Couldn't get lock in time } } /** * ReadWriteLock: Multiple readers OR single writer */ static class CachedData { private final ReadWriteLock lock = new ReentrantReadWriteLock(); private Object cachedValue; private volatile boolean valid = false; public Object read() { lock.readLock().lock(); // Multiple readers allowed try { if (valid) { return cachedValue; } } finally { lock.readLock().unlock(); } // Must refresh - need write lock lock.writeLock().lock(); // Exclusive access try { if (!valid) { // Double-check cachedValue = loadValueFromDatabase(); valid = true; } return cachedValue; } finally { lock.writeLock().unlock(); } } private Object loadValueFromDatabase() { return new Object(); // Simulated } } /** * Atomic Variables: Lock-free thread safety */ static class AtomicExample { private final AtomicInteger counter = new AtomicInteger(0); private final AtomicReference<String> lastValue = new AtomicReference<>(); public void incrementAndGet() { // Atomic read-modify-write int newValue = counter.incrementAndGet(); System.out.println("Counter: " + newValue); } public void compareAndSet(String expected, String update) { // CAS operation - foundation of lock-free programming boolean success = lastValue.compareAndSet(expected, update); if (!success) { System.out.println("CAS failed, value was changed"); } } public void atomicUpdate() { // updateAndGet with lambda (Java 8+) int result = counter.updateAndGet(current -> current * 2); System.out.println("Doubled to: " + result); } } /** * CountDownLatch: Wait for N events */ static class LatchExample { public void waitForAll() throws InterruptedException { int numWorkers = 5; CountDownLatch latch = new CountDownLatch(numWorkers); for (int i = 0; i < numWorkers; i++) { final int id = i; new Thread(() -> { System.out.println("Worker " + id + " starting"); // Do work... latch.countDown(); // Signal completion }).start(); } System.out.println("Main waiting for workers..."); latch.await(); // Block until count reaches 0 System.out.println("All workers complete!"); } } /** * CompletableFuture: Composable async operations (Java 8+) */ static class CompletableFutureExample { private final ExecutorService executor = Executors.newFixedThreadPool(4); public void asyncPipeline() { CompletableFuture.supplyAsync(() -> fetchData(), executor) .thenApply(data -> processData(data)) .thenApply(result -> formatResult(result)) .thenAccept(output -> System.out.println("Result: " + output)) .exceptionally(e -> { System.err.println("Error: " + e.getMessage()); return null; }); } public void combineResults() { CompletableFuture<String> future1 = CompletableFuture.supplyAsync( () -> "Hello", executor ); CompletableFuture<String> future2 = CompletableFuture.supplyAsync( () -> "World", executor ); // Combine two futures future1.thenCombine(future2, (a, b) -> a + " " + b) .thenAccept(System.out::println); // "Hello World" // Wait for all futures CompletableFuture.allOf(future1, future2) .thenRun(() -> System.out.println("All done")); } private String fetchData() { return "data"; } private String processData(String d) { return d.toUpperCase(); } private String formatResult(String r) { return "[" + r + "]"; } }}Java 21 introduced Virtual Threads (Project Loom)—lightweight threads managed by the JVM rather than the OS. Millions of virtual threads can run simultaneously, making blocking I/O operations efficient again. Create with Thread.ofVirtual().start(runnable) or Executors.newVirtualThreadPerTaskExecutor(). This represents a paradigm shift for Java concurrency.
Designing thread-safe classes requires applying proven patterns consistently. These patterns, documented extensively in Brian Goetz's "Java Concurrency in Practice," form the foundation of reliable concurrent programming.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
import java.util.*;import java.util.concurrent.*; /** * Thread Safety Design Patterns */public class ThreadSafetyPatterns { /** * Pattern 1: Immutable Objects * Absolutely thread-safe - no synchronization needed */ public static final class ImmutablePoint { private final int x; private final int y; public ImmutablePoint(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public int getY() { return y; } // Return new object for "modifications" public ImmutablePoint translate(int dx, int dy) { return new ImmutablePoint(x + dx, y + dy); } } /** * Pattern 2: Thread Confinement with ThreadLocal * Each thread has its own instance */ public static class UserContext { private static final ThreadLocal<UserContext> context = ThreadLocal.withInitial(UserContext::new); private String userId; private String sessionId; public static UserContext get() { return context.get(); } public static void clear() { context.remove(); // Important for thread pools! } public void setUserId(String id) { this.userId = id; } public String getUserId() { return userId; } } /** * Pattern 3: Monitor Pattern (Guarded State) * All state guarded by single lock */ public static class MonitorVehicleTracker { // @GuardedBy("this") private final Map<String, MutablePoint> locations = new HashMap<>(); public MonitorVehicleTracker() { // Initialize locations } public synchronized MutablePoint getLocation(String id) { MutablePoint loc = locations.get(id); return loc == null ? null : new MutablePoint(loc); // Defensive copy! } public synchronized void setLocation(String id, int x, int y) { MutablePoint loc = locations.get(id); if (loc != null) { loc.x = x; loc.y = y; } } public synchronized Map<String, MutablePoint> getLocations() { // Must return deep copy Map<String, MutablePoint> result = new HashMap<>(); for (Map.Entry<String, MutablePoint> entry : locations.entrySet()) { result.put(entry.getKey(), new MutablePoint(entry.getValue())); } return result; } } static class MutablePoint { public int x, y; public MutablePoint(int x, int y) { this.x = x; this.y = y; } public MutablePoint(MutablePoint p) { this.x = p.x; this.y = p.y; } } /** * Pattern 4: Safe Publication with volatile */ public static class SafePublisher { private volatile Config config; public void updateConfig(Config newConfig) { // Volatile write publishes the object safely this.config = newConfig; } public Config getConfig() { // Volatile read sees fully constructed object return config; } } static class Config { private final String setting1; private final int setting2; public Config(String s1, int s2) { setting1 = s1; setting2 = s2; } } /** * Pattern 5: Delegating Thread Safety * Use thread-safe components */ public static class DelegatingVehicleTracker { // ConcurrentHashMap is thread-safe private final ConcurrentMap<String, ImmutablePoint> locations; public DelegatingVehicleTracker(Map<String, ImmutablePoint> points) { this.locations = new ConcurrentHashMap<>(points); } public ImmutablePoint getLocation(String id) { return locations.get(id); // Safe, returns immutable } public void setLocation(String id, int x, int y) { locations.put(id, new ImmutablePoint(x, y)); } public Map<String, ImmutablePoint> getLocations() { // Unmodifiable view of thread-safe map return Collections.unmodifiableMap( new HashMap<>(locations) // Snapshot ); } } /** * Pattern 6: Copy-on-Write * For read-heavy workloads */ public static class CopyOnWriteRegistry { private volatile List<Listener> listeners = new ArrayList<>(); public void addListener(Listener l) { synchronized (this) { List<Listener> newList = new ArrayList<>(listeners); newList.add(l); listeners = newList; // Volatile write publishes } } public void notifyListeners(String event) { // No synchronization for reads (very fast) for (Listener l : listeners) { // Volatile read l.onEvent(event); } } } interface Listener { void onEvent(String event); }}When designing concurrent systems, start with immutability. Immutable objects are inherently thread-safe, require no synchronization, and can be freely shared. Only make objects mutable if there's a compelling reason. The functional programming approach of transforming immutable data is often simpler than managing mutable shared state.
Java provides one of the most comprehensive and well-specified threading models of any language. The combination of language-level constructs, a rigorous memory model, and rich library support makes Java an excellent platform for concurrent programming—when used correctly.
Java threading combines:
The key insight is that Java's memory model makes visibility guarantees explicit. Without proper synchronization, there are NO guarantees about when (or if) writes become visible. This is more restrictive than some developers expect, but it enables aggressive JVM optimizations.
Master the JMM, use high-level utilities, design for immutability, and document your thread-safety assumptions. Java concurrent programming rewards discipline and punishes carelessness.
You now have comprehensive knowledge of Java threading—from the Thread class API through the Java Memory Model to modern concurrency utilities. Next, we'll examine the mechanics of thread creation across all these platforms, understanding the lifecycle and resource allocation involved in bringing a new thread to life.