Skip to content

Chapter 6 of 8

Concurrency and the Java Memory Model

Java supports concurrent execution of multiple threads within a single program. Threads can be created by extending Thread and overriding run(), by implementing the Runnable interface, or by submitting tasks to an ExecutorService—now the preferred approach in production code. Calling thread.start() hands the new thread off to the OS scheduler; calling thread.run() directly executes the body in the current thread and defeats the purpose. The Runnable and Callable interfaces look similar but differ in two important ways: Runnable.run() returns void and cannot throw checked exceptions, while Callable<V>.call() returns a value and is allowed to throw checked exceptions, returning a Future<V> when submitted to an executor. The functional trio Runnable/Supplier/Callable can also be summarized: Runnable is no-args/no-return/throws-anything, Supplier is no-args/returns-T/no-checked-exceptions, Callable is no-args/returns-V/throws-checked.

Shared state is the central hazard of multithreading. The synchronized keyword ensures that only one thread at a time can execute a method or block by acquiring the object's intrinsic lock (monitor). The volatile keyword provides a different guarantee: a volatile variable is always read from and written to main memory rather than a thread's local CPU cache, giving cross-thread visibility without acquiring a lock. volatile is appropriate for simple flags such as private volatile boolean running = true; it does not, however, guarantee atomicity for compound operations like counter++. For those, use AtomicInteger, AtomicLong, or AtomicReference, which leverage CPU compare-and-swap (CAS) instructions to provide lock-free, thread-safe updates via incrementAndGet(), compareAndSet(), and Java 8's accumulateAndGet(). Note that writes and reads of long and double are not guaranteed atomic without a modifier—the JVM may split a 64-bit value into two 32-bit operations, allowing a thread to read a torn value—so declare them volatile or guard them under synchronized.

The Java Memory Model (JSR-133) defines how threads interact through memory and is the foundation on which all this rests. Three guarantees matter: atomicity (reads and writes of 32-bit values and references are atomic, but long/double may be torn unless declared volatile or guarded by synchronized); visibility (writes by one thread must be seen by another, established by volatile, synchronized, or final fields under proper construction); and ordering (the happens-before relationship, which states that if action A happens-before B, then A appears to execute before B and the result is visible to B). Sources of happens-before include a thread's own actions before its Thread.join() returns, volatile writes before subsequent reads of the same variable, monitor unlocks before subsequent locks of the same monitor, and actions in a thread before any thread returning from Thread.start() on that thread.

For more sophisticated locking, the java.util.concurrent.locks package offers ReentrantLock with tryLock(), timed acquisition, and multiple Condition variables; ReentrantReadWriteLock allows concurrent readers but exclusive writers, ideal for read-mostly data; and StampedLock (Java 8+) adds an optimistic read mode that returns a stamp to validate later, providing extremely fast lock-free reads (with the caveat that StampedLock is not reentrant). For coordination among many threads, java.util.concurrent ships higher-level utilities: CountDownLatch waits for a fixed number of operations to complete and is single-use, CyclicBarrier makes N threads meet before proceeding and is reusable, Phaser supports dynamic party counts across phases, BlockingQueue implements the classic producer-consumer pattern via put()/take() that block on capacity (ArrayBlockingQueue is bounded and array-backed, LinkedBlockingQueue is optionally bounded with linked nodes, PriorityBlockingQueue is unbounded and ordered, SynchronousQueue hands off directly between threads with zero capacity, and DelayQueue releases elements only after a delay), and Semaphore maintains permits for resource pooling or mutual exclusion (one permit ≈ a lock, but acquisition/release can cross threads since the semaphore has no ownership).

The producer-consumer pattern, where producers block on a full queue and consumers block on an empty queue, is the backbone of work distribution in the JVM. It naturally provides backpressure and is exactly how ExecutorService work queues operate. The Fork/Join framework (Java 7+) uses a work-stealing ForkJoinPool to parallelize divide-and-conquer tasks; extending RecursiveTask<V> or RecursiveAction and calling fork() on subtasks and join() on results is the standard recipe, and parallelStream() uses the common pool by default. ExecutorService is the higher-level replacement for manual thread management: it manages a thread pool (FixedThreadPool, CachedThreadPool, SingleThreadExecutor via the Executors factory), and the configurable ThreadPoolExecutor exposes the seven core parameters (corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler) that govern how tasks are accepted and rejected. CompletableFuture<T> extends Future<T> and implements CompletionStage, supporting fully composable asynchronous pipelines: supplyAsync for kicking off work, thenApply/thenAccept/thenRun for transformations, thenCompose for flattening nested futures, thenCombine for joining two independent futures, exceptionally and handle for error recovery, and allOf/anyOf for aggregating many futures. Best practice dictates always shutting down ExecutorServices gracefully (shutdown, awaitTermination, shutdownNow, awaitTermination again) to release non-daemon threads, and prefer higher-level utilities like BlockingQueue over raw wait()/notify()—which require monitor ownership, are easy to misuse with lost wakeups, and lack fairness. Concurrent collections deserve the final note: CopyOnWriteArrayList rebuilds the array on every mutation and is wait-free for readers (best for read-mostly lists like listener registries), Collections.synchronizedList wraps a list with coarse-grained locking, and ConcurrentLinkedQueue provides a non-blocking MPMC queue for producer/consumer throughput.

All chapters
  1. 1Java Platform and Memory Model
  2. 2Core Language Features
  3. 3Object-Oriented Programming
  4. 4Generics and the Collections Framework
  5. 5Functional Programming and the Streams API
  6. 6Concurrency and the Java Memory Model
  7. 7Exception Handling, I/O, and Serialization
  8. 8Modern Java, Design Patterns, and Best Practices

Drill it

Reading is not remembering. These come from the Java Programming deck:

Q

What is the JVM?

The Java Virtual Machine (JVM) is an abstract computing machine that executes Java bytecode. It provides platform independence by translating bytecode into nati...

Q

What is the difference between JDK, JRE, and JVM?

JDK (Java Development Kit) includes tools for developing Java programs.JRE (Java Runtime Environment) provides libraries and JVM to run Java applications.JVM (J...

Q

What are the four pillars of OOP in Java?

Encapsulation – bundling data and methods, restricting direct accessInheritance – a class derives from another classPolymorphism – one interface, many implement...

Q

What is encapsulation in Java?

Encapsulation is the practice of keeping fields private and providing access through public getter and setter methods. This protects the internal state of an ob...