-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Computers can do many things at the same time. Operating systems run multiple programs simultaneously, so we can listen to music while browsing the web. A text editing program might search through one file while we edit another. Computers can also do the same thing on many processors. Image manipulation software often uses multiple cores when applying costly operations to large images.
Parallelism and concurrency are related but separate concepts. Parallelism is about how a program is executed, concurrency is about how a program is written. In traditional programming languages like Java, that support statements mutating memory explicitly, how a program is written is often close to how it is executed. It is still useful to distinguish between these concepts.
Using parallelism means to execute a program using multiple processors or cores (or, with hyper-threading, using multiple logical threads.) Programs can be executed in parallel even if they are written without primitives for concurrent programming. Different sub-expressions of an expression could be evaluated on different cores if evaluation is costly. In Java, stream pipelines can be processed sequentially or in parallel with minimal changes to the underlying code. Some programs do not require doing multiple things at the same time but still benefit from parallel execution on many cores.
Other programs do require an explicit notion of doing multiple things at the same time. Such programs must be written with special support from programming languages for managing multiple so called threads of execution. Different threads of a program can be regarded as separate programs accessing the same memory and files. They can be (and usually are) coordinated, for example to wait for and consume results computed by other threads. Defining and coordinating multiple threads of execution in a program is called concurrent programming.
Concurrent programs can be executed using parallelism if the executing computer has multiple cores. They can also be executed sequentially where a scheduler switches between different threads usually in quick succession. The order in which statements in different threads are executed is generally unspecified. Executing a concurrent program can have non-deterministic effects, meaning that different runs may behave differently, regardless of whether they are executed using parallelism or not.
This tutorial introduces Java's API for programming concurrently. We will re-implement parts of the standard library and discuss core concepts on the way. The underlying source code is available online, and this tutorial includes tasks to extend it. If you want to follow along, you can use your own Java development environment supporting Java 21 or start a code space in you own fork of this repository.
This section introduces the foundational concepts of concurrency in Java
by exploring how to create and manage threads using the Thread
class and the Runnable
interface.
Introductory examples demonstrate starting threads, synchronizing their execution with start()
and join()
, and handling thread interruptions.
The section covers the basics of executing multiple threads concurrently and understanding their non-deterministic execution order.
Delving into the mechanisms that ensure thread safety,
this part covers intrinsic locking using the synchronized
keyword to protect shared resources from concurrent access issues like race conditions.
It explains how every Java object has an intrinsic lock and demonstrates synchronized blocks through practical examples.
The section also explores thread interruption handling and extends custom executor implementations to manage active threads safely.
We will gain insights into coordinating thread actions using wait
and notifyAll
,
implementing termination signaling.
Expanding on the executor framework, this section introduces the ExecutorService
interface,
which provides advanced capabilities for managing individual tasks.
It differentiates between Callable
and Runnable
, highlighting how Callable
allows tasks to return results and throw exceptions.
We learn to submit tasks and handle asynchronous computations using Future
and FutureTask
.
The section also covers the implementation of completable futures (CompletableFuture
),
enabling more sophisticated task coordination and transformation methods such as thenApply
, thenCompose
, and whenComplete
,
thereby facilitating the creation of complex asynchronous workflows.
This part presents a more flexible approach to synchronization
by introducing the Lock
and Condition
interfaces as alternatives to intrinsic locks and synchronized blocks.
Using ReentrantLock
and multiple Condition
objects,
we learn to implement custom executor services that manage task queues and thread termination without prolonged lock contention.
The section demonstrates how explicit locks provide finer control over lock acquisition and release,
enabling the creation of multiple condition variables for different synchronization scenarios.
Practical examples illustrate building a SingleThreadExecutorService
that leverages these mechanisms
to enhance thread coordination and executor shutdown processes.
Focusing on thread-safe data structures, this section explores Java's synchronized wrappers and lock-free collections provided by the standard library.
We learn how to utilize Collections.synchronizedList
for synchronized access and ConcurrentLinkedDeque
for lock-free operations,
enhancing performance in concurrent environments.
The section also introduces blocking collections like LinkedBlockingQueue
,
which support blocking operations essential for producer-consumer patterns.
A practical task involves implementing an AlternativeSingleThreadExecutorService
using a BlockingQueue
,
demonstrating how to manage task submissions and executor termination without explicit locking.
Concluding the workshop, this section highlights the power of Java Streams for parallel computations with minimal code changes.
We discover how invoking the parallel()
method on streams enables multi-threaded processing,
leveraging the ForkJoinPool
for improved performance on CPU-intensive tasks.
The section includes examples that compare the performance of sequential and parallel streams,
illustrating scenarios where parallelism yields significant benefits and cases where it offers marginal improvements.
© Sebastian Fischer 2024 CC BY-SA 4.0