Skip to content

Repository files navigation

Java multithreading and concurrency patterns and antipatterns

Java CI with Gradle License: MIT Java

Runnable examples of the concurrency primitives in java.util.concurrent, the classic multithreading patterns built on top of them, and the antipatterns they exist to prevent. Every antipattern is paired with the code that fixes it, so you can read the broken version and the corrected version side by side.

75 pattern examples across 32 topics, 44 antipattern examples across 15 topics, and 53 JUnit 5 test classes that assert the concurrency property in question instead of sleeping and hoping.

Contents

Getting started

Requires a JDK 21 or newer. The Gradle wrapper is checked in, so nothing else needs installing.

git clone https://github.com/alxkm/java-concurrency-patterns.git
cd java-concurrency-patterns

./gradlew build          # compile everything and run the test suite
./gradlew test           # run the tests on their own

Most examples carry a main method and are meant to be read and run one at a time. Run one from your IDE, or from the command line:

./gradlew compileJava
java -cp build/classes/java/main org.alxkm.antipatterns.racecondition.AccountExample

A few of the antipattern examples deliberately misbehave - DeadlockExample, for instance, is supposed to hang. That is the point; its resolution class next to it shows the way out.

Repository layout

src/main/java/org/alxkm/
├── patterns/        75 examples across 32 topics: atomics, locks, executors, queues,
│                    synchronizers, and the classic patterns (active object, balking,
│                    guarded suspension, monitor object, reactor, object pool, ...),
│                    plus Java 21 virtual threads and structured concurrency
└── antipatterns/    44 examples across 15 topics, each a broken version paired with its fix
                     (thread leakage, busy waiting, deadlock, lock contention, race conditions, ...)

src/test/java/org/alxkm/
├── patterns/        JUnit 5 tests for the pattern examples
├── antipatterns/    tests pinning down both the broken and the corrected behaviour
└── testsupport/     Await and Concurrently - helpers for writing tests that assert
                     concurrency properties deterministically, without Thread.sleep

Memory model

Every other section here shows a mechanism - a lock, a queue, an atomic. This one shows the rules those mechanisms exist to satisfy. Without them, "double-checked locking needs volatile" is a recipe to memorise rather than something you can reason about.

The model is defined in terms of happens-before: an ordering between actions in different threads. If a write happens-before a read, the read must see that write. If no such edge exists, the read may see the write, may see a stale value, or may see actions in a different order than the source lists them - and the compiler, JIT and CPU are all free to exploit that freedom.

Why volatile, concretely

Run the visibility example and the answer stops being abstract:

$ java -cp build/classes/java/main org.alxkm.memorymodel.VisibilityExample
plain field    -> reader observed the write: false
volatile field -> reader observed the write: true

The reader spins on a flag another thread sets. With a plain field it spins forever: nothing in the loop writes the flag, so the JIT may hoist the read out and turn while (!flag) into if (!flag) while (true). That is legal precisely because no happens-before edge exists between the writer's store and the reader's load. One volatile creates the edge and the loop exits.

Stress tests: what a unit test cannot show

Some of these races cannot be demonstrated by an ordinary test, and it is worth being precise about why. Lining two threads up requires synchronisation, and that synchronisation is itself a memory barrier that drains the store buffer producing the effect. Measured here, the textbook Dekker probe found zero reorderings in 20,000 thread-pair runs and zero in 500,000 barrier-synchronised iterations.

jcstress, the OpenJDK harness built for this, spins the actors without synchronisation and shuffles JIT decisions between forks. Given the same idiom:

RESULT       SAMPLES     FREQ       EXPECT  DESCRIPTION
  0, 0     9,914,377    3.74%  Interesting  Reordering: neither load saw the other store
  0, 1   126,435,416   47.65%   Acceptable  actor1 ran first
  1, 0   128,998,594   48.61%   Acceptable  actor2 ran first

That gap - zero by hand, millions under jcstress - is the lesson. These bugs do not fail loudly in testing; they fail in production, rarely, on someone else's hardware.

The same suite marks the safe variants FORBIDDEN, so a run fails if a guarantee is ever violated: volatile fields must never produce 0, 0, and a final field must never be observed at its default.

./gradlew jcstress                                          # full run, a few minutes
./gradlew jcstress -PjcstressArgs="-t PlainFields -m quick"  # one test, faster

./gradlew build compiles these tests but does not run them - a full pass takes minutes, which does not belong in every build. Reports land in build/reports/jcstress.

Diagnostics

Knowing the primitives is not the same as being able to work out what a stuck process is doing at 3am. This section covers the other direction: given a system that has stopped making progress, how to find out why.

Taking a dump

jcmd <pid> Thread.print        # preferred
jstack <pid>                   # older, same idea

Take three, twenty seconds apart. One dump shows where threads are; three show whether they are moving. A thread in the same frame across all three is stuck, whereas one that moves is just busy.

Reading it

State Means Usually
RUNNABLE running, or wants to be also covers blocking socket reads, so not always "busy"
BLOCKED waiting to enter a synchronized block contention; the dump names the monitor and its owner
WAITING parked until someone signals a handoff; if nobody signals, it never returns
TIMED_WAITING parked with a deadline normal for pool workers and sleep

Two traps worth knowing:

BLOCKED and WAITING are different problems. BLOCKED is contention and resolves when the owner releases. WAITING is a handoff that may never come. One costs throughput, the other is a hang.

ReentrantLock never shows as BLOCKED. It parks the thread, so it appears as WAITING on an ownable synchronizer. Grepping a dump for BLOCKED misses every lock in java.util.concurrent. Run ThreadDumpExample to see all three shapes side by side.

Deadlocks

The JVM finds these itself. A thread dump ends with a Found one Java-level deadlock section, and the same analysis is available programmatically:

Found a Java-level deadlock involving 2 threads:

"holder-a" id=21 BLOCKED
    waiting to lock java.lang.Object@4d405ef7 which is held by "holder-b" id=22
    holds java.lang.Object@76fb509a

It covers ReentrantLock as well as monitors, but it only finds cycles. A thread blocked forever on a lock nobody will release is not a cycle and will not be reported, and neither will a livelock, where threads keep running without progressing. For those, three dumps and your own eyes.

Note that the detector is JVM-wide. Anything asserting on the result should scope it with deadlockedAmong(threads), or an unrelated cycle elsewhere in the process will fail the assertion.

Virtual thread pinning

A virtual thread that blocks normally unmounts from its carrier, which is what lets a few carriers serve thousands of threads. Blocking inside synchronized is the exception: on Java 21 the monitor is tied to the carrier, so the thread holds it for the whole block. Measured by VirtualThreadPinningExample, 64 tasks blocking 500ms each on 12 cores:

synchronized  : 3060 ms     12 at a time, so 6 rounds
ReentrantLock :  503 ms     all 64 at once

Every task locks a monitor of its own, so nothing there contends. What runs out is carriers.

Pinning is invisible in a thread dump; the symptom is throughput that will not scale. To see it:

java -Djdk.tracePinnedThreads=short ...   # prints the frame holding the monitor
Thread[#97,ForkJoinPool-1-worker-12,5,CarrierThreads]
    org.alxkm.diagnostics.VirtualThreadPinningExample.lambda$runPinned$0(...) <== monitors:1

The jdk.VirtualThreadPinned JFR event records the same thing with far less overhead, which makes it the option for a production process.

The fix is a ReentrantLock, which a virtual thread can hold across an unmount. Everywhere else synchronized is fine. This advice has an expiry date: JEP 491 removed monitor pinning in Java 24, so on a recent JDK both versions run in the same time. It still matters on 21, the current LTS and what this repository builds against.

Benchmarks

A repository that makes performance claims should be able to back them. These are JMH benchmarks for the specific claims made above, and the numbers below come from running them, not from an article.

./gradlew jmh                                              # everything, several minutes
./gradlew jmh -PjmhArgs="CounterBenchmark -t 8"            # one benchmark, contended
./gradlew jmh -PjmhArgs="CounterBenchmark -f 1 -wi 3 -i 3" # quick and rough

./gradlew build compiles them but never runs them. Results land in build/reports/jmh.

All figures below are throughput in ops/us, higher is better, on a 12 core machine running JDK 21. Your numbers will differ; the point is that you can produce your own.

Counters

CounterBenchmark.java

1 thread 8 threads
synchronized 92.9 18.2
ReentrantLock 96.1 66.6
AtomicLong 204.2 115.5
LongAdder 210.3 1256.5

Two things worth noting. Uncontended, the atomics are already about twice as fast as either lock, which contradicts the old advice that an uncontended monitor is nearly free. That advice assumed biased locking, disabled in JDK 15 and removed in 18.

Under contention the spread is much wider, and LongAdder is in a different class: 69x the throughput of synchronized. It gets there by spreading its state across padded cells so threads stop fighting over one cache line, which is the effect FalseSharingExample measures directly. The catch is that sum() has to walk every cell, so a counter read as often as it is written is a different question from this one.

Queues

QueueBenchmark.java offers and polls from the same thread, 4 threads:

ops/us
ArrayBlockingQueue 23.6
LinkedBlockingQueue 11.1
ConcurrentLinkedQueue 4.4
ConcurrentLinkedDeque 3.2

ConcurrentLinkedDeque came out about 27% slower than ConcurrentLinkedQueue, which is the same direction as the 40% this README used to quote, but not the same number.

The LinkedBlockingQueue result contradicts what this README used to claim. Its two-lock design is supposed to beat ArrayBlockingQueue, but that argument is about producers and consumers running at once, which offer-then-poll on one thread cannot show either way. So QueueHandoffBenchmark.java runs 4 producers against 4 consumers:

total produce consume
ArrayBlockingQueue 49.5 ± 1.4 23.7 25.8
LinkedBlockingQueue 40.9 ± 18.2 15.2 25.7

Closer, as expected, but still not in favour of the two-lock design here. ArrayBlockingQueue writes into a ring buffer it allocated once; LinkedBlockingQueue allocates a node per element. Note the error bars: the linked queue's throughput is also far less predictable.

Lists

ListBenchmark.java, 1000 elements, throughput of the whole group:

7 readers, 1 writer 4 readers, 4 writers
CopyOnWriteArrayList 314.7 300.3
Collections.synchronizedList 19.3 12.4

The advice that CopyOnWrite suits infrequent writes is right, but it understates the case: the crossover is much further out than "infrequent" suggests. Even at an even read/write split it was 24x ahead here, because its reads take no lock at all (296 against 6.8 ops/us) and that dominates the total.

Read the write column separately before concluding too much. CopyOnWrite writes were 4.1 ops/us against 5.8 for the synchronized list at 1000 elements, and the gap widens with size, since every write copies the whole array. At 100,000 elements CopyOnWrite writes become both slower and very erratic. Total throughput still favoured CopyOnWrite at every size tested, which is a statement about this workload rather than a general rule.

Patterns

Atomics

Concurrent collections

Queue

Executors

Fork join pool

Future

Locks

Synchronizers

Thread local

Reentrant lock

Mutex

Semaphore

Double check locking singleton

Active object

Balking pattern

Guarded suspension pattern

Immutable pattern

Monitor object pattern

Multithreaded context

Reactor pattern

Scheduler

Singleton pattern

Thread-safe lazy initialization

Two-phase termination

Thread-Safe Builder Pattern

Concurrent Object Pool Pattern

Leader-Follower Pattern

Modern Java features (Java 21+)

Odd-Even Printer

Philosopher problem

Producer-Consumer with BlockingQueue Variations

Antipatterns

Thread Leakage

  • Description: Threads are created but never terminated, leading to resource exhaustion.
  • Solution: Use thread pools (e.g., ThreadPoolExecutor) to manage threads.

Examples

Busy Waiting

  • Description: A thread repeatedly checks a condition in a loop, wasting CPU cycles.
  • Solution: Use wait/notify mechanisms or higher-level concurrency constructs like CountDownLatch, CyclicBarrier, or Condition.

Examples

Nested Monitor Lockout (Deadlock)

  • Description: Two or more threads block each other by holding resources the other needs.
  • Solution: Always acquire multiple locks in a consistent global order, use tryLock with timeouts, or avoid acquiring multiple locks if possible.

Examples

Forgotten Synchronization

  • Description: Access to shared resources is not properly synchronized, leading to race conditions.
  • Solution: Use synchronized blocks or higher-level concurrency utilities (e.g., ReentrantLock, Atomic* classes).

Examples

Excessive Synchronization

  • Description: Overuse of synchronization, leading to contention and reduced parallelism.
  • Solution: Minimize the scope of synchronized blocks, use lock-free algorithms, or utilize concurrent collections (e.g., ConcurrentHashMap, CopyOnWriteArrayList).

Examples

Using Thread-Safe Collections Incorrectly

  • Description: Assuming individual thread-safe operations guarantee overall thread-safe logic.
  • Solution: Combine operations using explicit locks or use higher-level synchronization constructs to maintain logical thread safety.

Examples

Ignoring InterruptedException

  • Description: Swallowing or ignoring the InterruptedException, leading to threads that cannot be properly managed or interrupted.
  • Solution: Handle interruptions properly, typically by cleaning up and propagating the interruption status.

Examples

Starting a Thread in a Constructor

  • Description: Starting a thread from within a constructor, possibly before the object is fully constructed.
  • Solution: Start threads from a dedicated method called after construction, or use factory methods.

Examples

Double-Checked Locking

  • Description: A broken idiom for lazy initialization that was incorrectly implemented before Java 5.
  • Solution: Use the volatile keyword correctly or the Initialization-on-demand holder idiom.

Examples

Lock Contention

  • Description: Multiple threads trying to acquire the same lock, leading to reduced performance.
  • Solution: Reduce the granularity of locks, use read-write locks, or employ lock-free data structures.

Examples

Improper Use of ThreadLocal

  • Description: Using ThreadLocal incorrectly, leading to memory leaks or unexpected behavior.
  • Solution: Ensure proper management and cleanup of ThreadLocal variables.

Examples

Non-Atomic Compound Actions

  • Description: Performing compound actions (e.g., check-then-act, read-modify-write) without proper synchronization.
  • Solution: Use atomic variables or synchronized blocks to ensure compound actions are atomic.

Examples

Race Conditions

  • Description: The system's behavior depends on the sequence or timing of uncontrollable events.
  • Solution: Properly synchronize access to shared resources and use thread-safe collections.

Examples

Lack of Thread Safety in Singletons

  • Description: Singleton instances not properly synchronized, leading to multiple instances.
  • Solution: Use the enum singleton pattern or the Initialization-on-demand holder idiom.

Examples

Using Threads Instead of Tasks

  • Description: Directly creating and managing threads instead of using the Executor framework.
  • Solution: Use ExecutorService and related classes to manage thread pools and tasks efficiently.

Examples

java.util.concurrent.*

image

Concurrent Collections are a set of collections designed to operate more efficiently in multithreaded environments compared to the standard universal collections from the java.util package. Instead of using the basic Collections.synchronizedList wrapper, which blocks access to the entire collection, these collections utilize locks on data segments or employ wait-free algorithms to optimize parallel data reading and processing.

Queues - non-blocking and blocking queues with multithreading support. Non-blocking queues are designed for speed and work without blocking threads. Blocking queues are used when it is necessary to "slow down" the "Producer" or "Consumer" threads if some conditions are not met, for example, the queue is empty or full, or there is no free "Consumer".

Synchronizers are auxiliary utilities for synchronizing threads. They are a powerful weapon in "parallel" computing.

Executors - contains excellent frameworks for creating thread pools, scheduling asynchronous tasks and obtaining results.

Locks are alternative and more flexible thread synchronization mechanisms compared to the basic synchronized, wait, notify, notifyAll.

Atomics - classes with support for atomic operations on primitives and references.

Concurrent Collections

CopyOnWrite collections

image

The name is self-explanatory. All modification operations on the collection (add, set, remove) result in the creation of a new copy of the internal array. This ensures that when an iterator traverses the collection, a ConcurrentModificationException will not be thrown. It is important to note that only references to objects are copied during the array copy (shallow copy), meaning that access to the fields of elements is not thread-safe. CopyOnWrite collections are particularly useful when write operations are infrequent, such as when implementing a listener subscription mechanism and iterating through the listeners.

CopyOnWriteArrayList - A thread-safe analogue of ArrayList, implemented with the CopyOnWrite algorithm.

CopyOnWriteArraySet - Implementation of the Set interface, using CopyOnWriteArrayList as a basis. Unlike CopyOnWriteArrayList, there are no additional methods.

Examples

ConcurrentSkipListSet
CopyOnWriteArrayList

Scalable Maps

image

Improved implementations of HashMap, TreeMap with better support for multithreading and scalability.

ConcurrentMap<K, V> - An interface that extends Map with several additional atomic operations.

ConcurrentHashMap<K, V> - Unlike Hashtable and synchronized blocks on HashMap, writes lock only the bin they touch rather than the whole map, so unrelated keys never contend. Up to Java 7 this was done with a fixed set of segments; since Java 8 the map locks the individual bin head and uses CAS for the common uncontended case, which is why concurrencyLevel is now only a sizing hint. Iterators are weakly consistent: they reflect the map at some point during traversal and never throw ConcurrentModificationException. See the ConcurrentHashMap javadoc for details.

Additional constructor

ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) - The third parameter is the anticipated number of concurrently updating threads, defaulting to 16. Since Java 8 it no longer selects a segment count; it is used only as a sizing hint for the initial table.

ConcurrentNavigableMap<K,V> - This interface extends the NavigableMap interface and mandates that objects implementing ConcurrentNavigableMap are used as return values. All iterators provided by this interface are designated as safe for use and are programmed not to throw ConcurrentModificationException.

ConcurrentSkipListMap<K, V> - This class serves as a thread-safe equivalent of TreeMap. It organizes data based on keys and ensures an average performance of log(N) for operations like containsKey, get, put, remove, and similar operations.

ConcurrentSkipListSet - This class implements the Set interface and is built upon ConcurrentSkipListMap for thread-safe set operations.

Examples

ConcurrentHashMap
ConcurrentSkipListMap

Queues

Non-Blocking Queues

image

Thread-safe and non-blocking queue implementations based on linked nodes.

ConcurrentLinkedQueue - This implementation utilizes the wait-free algorithm devised by Michael & Scott, optimized to work efficiently with the garbage collector. Built on CAS, this algorithm ensures high-speed operations. However, it's worth noting that the size() method may incur significant overhead if called frequently, so it's advisable to minimize its usage.

ConcurrentLinkedDeque - Deque, pronounced as “Deck”, stands for Double-ended queue, indicating that data can be added to and removed from both ends. Consequently, this class supports both FIFO (First In First Out) and LIFO (Last In First Out) modes of operation. In practical scenarios, ConcurrentLinkedDeque should be employed only if LIFO functionality is indispensable, as its bidirectional nature costs throughput compared to ConcurrentLinkedQueue. Measured here it was about 27% slower on offer/poll; see Benchmarks.

Blocking Queues

image

BlockingQueue - When managing large data streams with queues, ConcurrentLinkedQueue alone may not suffice. If threads clearing the queue fail to keep up with the data influx, it could lead to memory exhaustion or significant IO/Net overload, causing a performance drop until system failure due to timeouts or lack of free descriptors. To address such scenarios, a queue with customizable size or conditional locking is necessary. This is where the BlockingQueue interface comes in, providing access to a range of useful classes. Besides setting the queue size, new methods have been introduced to handle underfilling or overflowing queues differently. For instance, when adding an element to a full queue, one method throws an IllegalStateException, another returns false, another blocks the thread until space is available, and yet another blocks the thread with a timeout, returning false if space is still unavailable. It's important to note that blocking queues don't support null values since null is used in the poll method as a timeout indicator.

ArrayBlockingQueue - A blocking queue implemented using a traditional ring buffer. In addition to the queue size, it allows control over lock fairness. If fair=false (default), thread order is not guaranteed. See the Locks section for more on "fairness".

DelayQueue - A specialized class that retrieves elements from the queue only after a delay specified in each element via the getDelay method of the Delayed interface.

LinkedBlockingQueue - A blocking queue implemented with linked nodes, using the "two lock queue" algorithm: one lock for adding, another for removing elements. The two locks let a put and a take proceed at once, which is often quoted as making it faster than ArrayBlockingQueue. Measured here that did not hold: ArrayBlockingQueue was ahead in both the single-threaded and the producer/consumer case, because it writes into a preallocated ring buffer while LinkedBlockingQueue allocates a node per element. It does consume more memory. See Benchmarks. The queue size is set via the constructor and defaults to Integer.MAX_VALUE.

PriorityBlockingQueue - A thread-safe wrapper over PriorityQueue. When inserting an element, its position in the queue is determined by the Comparator logic or the Comparable interface implemented in the elements. The smallest element is dequeued first.

SynchronousQueue - Operates on a "one in, one out" principle. Each insert operation blocks the producer thread until the consumer thread retrieves an element, and vice versa; the consumer waits until the producer inserts an element.

BlockingDeque - An interface providing additional methods for a bidirectional blocking queue, allowing data insertion and retrieval from both ends of the queue.

LinkedBlockingDeque - A bidirectional blocking queue implemented with linked nodes, essentially a doubly linked list with a single lock. The queue size is specified via the constructor and defaults to Integer.MAX_VALUE.

TransferQueue - This interface is interesting because it allows blocking the producer thread when adding an element until a consumer thread retrieves an element from the queue. The blocking can include a timeout or a check for waiting consumers, enabling synchronous and asynchronous message transfer mechanisms.

LinkedTransferQueue - An implementation of TransferQueue based on the Dual Queues with Slack algorithm, utilizing CAS and thread parking extensively when idle.

Examples

ArrayBlockingQueue example
ConcurrentLinkedDeque example
ConcurrentLinkedQueue example
BlockingQueue Producer-Consumer example
CustomBlockingQueue example

Synchronizers

image

This section introduces classes for active thread management:

Semaphore - Typically used to limit the number of threads accessing hardware resources or a file system. A counter controls access to a shared resource. If the counter is greater than zero, access is granted, and the counter is decremented. If the counter is zero, the current thread is blocked until another thread releases the resource. The number of permits and the "fairness" of thread release are specified via the constructor. The challenge with semaphores is setting the number of permits, often depending on hardware capabilities.

CountDownLatch - Allows one or more threads to wait until a specific number of operations in other threads are completed. For example, threads calling the latch's await method (with or without a timeout) will block until another thread completes initialization and calls the countDown method. This method decrements the count. When the counter reaches zero, all waiting threads proceed, and subsequent await calls pass without waiting. The count is one-time and cannot be reset.

CyclicBarrier - Used to synchronize a set number of threads at a common point. The barrier is reached when N threads call the await method and block. The counter then resets, and waiting threads are released. Optionally, a Runnable task can be executed before threads are unblocked and the counter is reset.

Exchanger - Facilitates the exchange of objects between two threads, supporting null values for single object transfers or as a simple synchronizer. The first thread calling the exchange method blocks until the second thread calls the same method. The threads then exchange values and proceed.

Phaser - An advanced barrier for thread synchronization, combining features of CyclicBarrier and CountDownLatch. The number of threads is dynamic and can change. The class can be reused and allows threads to report readiness without blocking.

Examples

Barrier wrapper
Barrier
CountDownLatch
Exchanger
Phaser
SemaphorePrintQueue

Executors

Here, we reach the most extensive section of the package. This part covers interfaces for executing asynchronous tasks with the capability of receiving results via the Future and Callable interfaces. Additionally, it includes services and factories for creating thread pools such as ThreadPoolExecutor, ScheduledThreadPoolExecutor, and ForkJoinPool. To enhance comprehension, we will break down the interfaces and classes into smaller, more manageable parts.

Future and Callable

image

Future - This is a useful interface for obtaining the results of an asynchronous operation. The key method is get, which blocks the current thread (with or without a timeout) until the asynchronous operation completes in another thread. Additional methods are available for canceling the operation and checking its current status. The FutureTask class often implements this interface.

RunnableFuture - While Future serves as a Client API interface, the RunnableFuture interface is used to start the asynchronous operation. The successful completion of the run() method marks the asynchronous operation as complete, allowing the results to be retrieved via the get method.

Callable - This is an extended version of the Runnable interface for asynchronous operations. It allows returning a typed value and throwing a checked exception. Although it lacks a run() method, many java.util.concurrent classes support it along with Runnable.

FutureTask - This class implements the Future and RunnableFuture interfaces. It accepts an asynchronous operation as input in the form of Runnable or Callable objects. The FutureTask class is designed to be launched in a worker thread, for example, via new Thread(task).start(), or through a ThreadPoolExecutor. The results of the asynchronous operation are retrieved using the get(...) method.

Delayed - This interface is used for asynchronous tasks that should start in the future, as well as in DelayQueue. It allows setting the time before the start of an asynchronous operation.

ScheduledFuture - A marker interface that combines the functionalities of Future and Delayed.

RunnableScheduledFuture - An interface that combines RunnableFuture and ScheduledFuture. It also allows specifying whether the task is one-time or should be launched at a specified frequency.

Executor Services

image

Executor - This is the fundamental interface for classes that execute Runnable tasks. It decouples the task submission process from the execution mechanism.

ExecutorService - An interface that defines a service for executing Runnable or Callable tasks. The submit methods take a task as a Callable or Runnable and return a Future through which the result can be obtained. The invokeAll methods handle lists of tasks, blocking the thread until all tasks in the provided list are completed or the specified timeout expires. The invokeAny methods block the calling thread until any one of the passed tasks completes. The interface also includes methods for graceful shutdown. Once the shutdown method is called, the service will no longer accept new tasks and will throw a RejectedExecutionException if an attempt is made to submit a task.

ScheduledExecutorService - This interface extends ExecutorService by adding capabilities for scheduling tasks to be executed after a delay or periodically.

AbstractExecutorService - An abstract class that serves as a base for building an ExecutorService. It provides the basic implementation of the submit, invokeAll, and invokeAny methods. Classes such as ThreadPoolExecutor, ScheduledThreadPoolExecutor, and ForkJoinPool inherit from this class.

Examples

Custom ExecutorService implementation
ExecutorCompletionService
Executors example
ExecutorServiceExample
ScheduledThreadPoolExecutors
ThreadPoolExecutors

ThreadPoolExecutor & Factory

image

ThreadPoolExecutor - A highly versatile and essential class used to execute asynchronous tasks within a thread pool. This approach minimizes the overhead associated with creating and terminating threads. By maintaining a fixed maximum number of threads in the pool, it ensures predictable application performance. It is generally recommended to create this pool using one of the factory methods provided by the Executors class. However, if the standard configurations are insufficient, all key parameters of the pool can be set via constructors or setters. For more details, refer to the relevant documentation.

ScheduledThreadPoolExecutor - In addition to the methods of ThreadPoolExecutor, this class allows tasks to be scheduled for execution after a specific delay or at a fixed rate, enabling the implementation of a timer service based on this class.

ThreadFactory - By default, ThreadPoolExecutor uses the standard thread factory obtained through Executors.defaultThreadFactory(). If additional customization is needed, such as setting thread priority or naming threads, you can implement this interface and pass it to ThreadPoolExecutor.

RejectedExecutionHandler - Defines a handler for tasks that cannot be executed by ThreadPoolExecutor for various reasons, such as a lack of available threads or the service being shut down. The ThreadPoolExecutor class includes several standard implementations: CallerRunsPolicy - runs the task in the calling thread; AbortPolicy - throws an exception; DiscardPolicy - silently discards the task; DiscardOldestPolicy - removes the oldest unexecuted task from the queue and retries adding the new task.

Fork Join

image

Java 1.7 introduces a new Fork Join framework for solving recursive problems using divide and conquer or Map Reduce algorithms.

Thus, by dividing into parts, it is possible to achieve their parallel processing in different threads. To solve this problem, you can use the usual ThreadPoolExecutor, but due to frequent context switching and tracking of execution control, all this does not work very effectively. Here, the Fork Join framework comes to our aid, which is based on the work-stealing algorithm. It reveals itself best in systems with a large number of processors. Doug Lea's design paper covers the algorithm and its performance characteristics in depth.

ForkJoinPool - The main entry point for initiating root (main) ForkJoinTask tasks. Subtasks are started using methods of the task being forked. By default, the thread pool is created with a number of threads equal to the number of processors (cores) available to the JVM.

ForkJoinTask - The base class for all Fork/Join tasks. Key methods include: fork() - adds a task to the queue of the current ForkJoinWorkerThread for asynchronous execution; invoke() - executes a task in the current thread; join() - waits for the subtask to complete and returns the result; invokeAll(…) - combines the previous three operations, executing two or more tasks at once; adapt(…) - creates a new ForkJoinTask from Runnable or Callable objects.

RecursiveTask - An abstract class derived from ForkJoinTask, requiring the implementation of the compute method, which performs the asynchronous operation.

RecursiveAction - Similar to RecursiveTask but does not return a result.

ForkJoinWorkerThread - Used as the default implementation in ForkJoinPool. Optionally, it can be extended to override worker thread initialization and completion methods.

Completion Service

image

CompletionService - An interface that separates the submission of asynchronous tasks from the retrieval of their results. The submit methods are used to add tasks, while the take method (blocking) and poll method (non-blocking) are used to obtain the results of completed tasks.

ExecutorCompletionService - A wrapper around any class that implements the Executor interface, such as ThreadPoolExecutor or ForkJoinPool. It is primarily used to abstract the task submission and execution monitoring process. If tasks are completed, their results can be retrieved; otherwise, the take method will wait for completion. The default service uses LinkedBlockingQueue, but any BlockingQueue implementation can be used.

Locks

image

Condition - An interface that provides alternative methods to the traditional wait/notify/notifyAll methods. A condition object is typically obtained from a lock using the lock.newCondition() method, allowing multiple wait/notify sets for a single object.

Lock - A fundamental interface in the lock framework that offers a more flexible approach to controlling access to resources or blocks compared to using synchronized. When using multiple locks, the release order can be arbitrary, and it provides an option to follow an alternative scenario if the lock is already held by another thread.

ReentrantLock - A reentrant lock that allows only one thread to enter a protected block at a time. This class supports both "fair" and "non-fair" thread locking. With "fair" locking, threads are released in the order they called lock(). With "unfair" locking, the release order is not guaranteed, but it operates faster. By default, "unfair" locking is used.

ReadWriteLock - An interface for creating read/write locks. These locks are particularly useful when the system has many read operations and few write operations.

ReentrantReadWriteLock - Commonly used in multithreaded services and caches, providing a significant performance improvement over synchronized blocks. This class operates in two mutually exclusive modes: multiple readers can read data simultaneously, while only one writer can write data at a time.

ReentrantReadWriteLock.ReadLock - A read lock for readers, obtained via readWriteLock.readLock().

ReentrantReadWriteLock.WriteLock - A write lock for writers, obtained via readWriteLock.writeLock().

LockSupport - Designed for creating classes with locks. It includes methods for parking threads, serving as replacements for the deprecated Thread.suspend() and Thread.resume() methods.

Examples

ReadWriteLock
ReentrantReadWriteLockCounter
ReentrantReadWriteLockCounter
AbstractOwnableSynchronizer
AbstractQueuedLongSynchronizer
AbstractQueuedSynchronizer
LockSupport

image

AbstractOwnableSynchronizer - A base class designed for creating synchronization mechanisms. It includes a simple getter/setter pair for storing and accessing an exclusive thread that can interact with the data.

AbstractQueuedSynchronizer - This class serves as the foundation for synchronization mechanisms in FutureTask, CountDownLatch, Semaphore, ReentrantLock, and ReentrantReadWriteLock. It can also be used to develop new synchronization mechanisms that rely on a single atomic integer value.

AbstractQueuedLongSynchronizer - A variant of AbstractQueuedSynchronizer that supports operations on an atomic long value.

Atomics

image

AtomicBoolean, AtomicInteger, AtomicLong, AtomicIntegerArray, AtomicLongArray - When you need to synchronize access to a simple int variable in a class, you can use synchronized constructs, or volatile with atomic set/get operations. However, the new Atomic* classes offer an even better solution. These classes use CAS (Compare-And-Swap) operations, which are faster than a lock: measured here about 2x uncontended and about 6x with eight threads, and LongAdder far more than that. See Benchmarks. Additionally, they provide methods for atomic addition, increment, and decrement.

AtomicReference - This class allows for atomic operations on an object reference.

AtomicMarkableReference - This class supports atomic operations on a pair of fields: an object reference and a boolean flag (true/false).

AtomicStampedReference - This class supports atomic operations on a pair of fields: an object reference and an integer value.

AtomicReferenceArray - An array of object references that can be updated atomically.

AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater - These classes allow for atomic updates of fields by their names using reflection. The field offsets for CAS are determined in the constructor and cached, so the performance impact of reflection is minimal.

Examples

Atomics
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicMarkableReference
AtomicReferenceArray
AtomicReference
AtomicReferenceFieldUpdater
AtomicStampedReference

Testing

Run the suite with ./gradlew test; ./gradlew build runs it as part of the build. A JaCoCo coverage report is written to build/reports/jacoco/test/html/index.html.

Concurrency tests that lean on Thread.sleep pass on a fast machine and fail on a loaded CI runner, so these ones do not. They assert the actual condition - a latch reached, a counter settled, an ordering observed - through the helpers in src/test/java/org/alxkm/testsupport:

  • Await - polls a condition up to a timeout and fails with a clear message instead of hanging.
  • Concurrently - holds N threads behind a start gate and releases them together, so the operations actually overlap instead of running one after another; collect returns one result per thread.

Tests that pin down an antipattern assert both halves: that the broken version can actually lose updates, and that the corrected version never does.

Running tests

To run all tests:

./gradlew test

To run specific test categories:

# Run only pattern tests
./gradlew test --tests "org.alxkm.patterns.*"

# Run only anti-pattern tests  
./gradlew test --tests "org.alxkm.antipatterns.*"
Full test catalogue

Pattern Tests

Active Objects

Atomics

Balking Pattern

Collections

Double Checked Locking

Executors

Future

Guarded Suspension

Immutable Pattern

Monitor Object

Mutex

Odd-Even Printer

Fork/Join

Philosopher Problem

Reentrant Lock

Producer-Consumer Patterns

Queue

Read-Write Lock

Reactor Pattern

Scheduler

Semaphore

Singleton

Synchronizers

Thread Local

Thread-Safe Lazy Initialization

Thread-Safe Builder Pattern

Concurrent Object Pool Pattern

Leader-Follower Pattern

Modern Java Features (Java 21+)

Virtual Threads Pattern

Structured Concurrency Pattern

Two-Phase Termination

Anti-Pattern Tests

Busy Waiting

Deadlock

Forgotten Synchronization

Race Conditions

Thread Leakage

Double-Checked Locking

Excessive Synchronization

Lack of Thread Safety in Singletons

Lock Contention

Non-Atomic Compound Actions

Race Conditions (account balance)

Using Thread-Safe Collections Incorrectly

Contributing

Contributions are welcome - please open an issue or submit a pull request. When adding an example:

  • Put it under the topic package it belongs to, mirroring the existing layout.
  • Give the class a Javadoc comment explaining what it demonstrates, and, for an antipattern, why it breaks.
  • Add a test that asserts the behaviour rather than waiting for it.
  • Add a link to it in the matching README section.

License

This project is licensed under the MIT License - see the LICENSE file for details. Feel free to fork and modify these implementations for your own use cases.

Acknowledgments

This repository was inspired by multithreading technics and adapted for educational purposes. Some images source.

About

Java concurrency patterns for educational purposes

Topics

Resources

Stars

80 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages