# Concurrency and Threads

> Chapter 14 of the Courseiva ORACLE-OCP-JAVA17 curriculum — https://courseiva.com/learn/oracle-ocp-java17/concurrency-and-threads

**Official objective:** 9.1 — Create and manage threads using Runnable, Callable, ExecutorService, and use synchronization and concurrent collections.

## Introduction

Concurrency and threads. These are the tools that let your Java programs do multiple things at once instead of waiting around. For the 1Z0-829 exam, you must understand how to create and manage threads, how to use the ExecutorService to run tasks efficiently, and how to protect shared data from chaos when multiple threads access it at the same time.

## The Restaurant Kitchen Analogy

Have you ever been to a busy restaurant and wondered how all the food gets cooked at the same time without complete chaos?

In a single-threaded kitchen, there is only one chef. That chef handles one order completely from start to finish before moving to the next. If a steak needs to grill for ten minutes, the chef just stands there waiting. Every other table's order gets delayed. That is sequential execution — it works but it is slow and wasteful.

Now think of a concurrent kitchen. There is still one chef, but they start the steak on the grill, then immediately begin chopping vegetables for another order while the steak cooks. They switch between tasks rapidly, making progress on everything. This is like a single-core CPU running multiple threads by time-slicing.

For the real power, imagine a kitchen with multiple chefs. Each chef works independently on different orders. Chef A handles table 3s pasta while Chef B plates table 5s dessert. This is parallel execution on multiple CPU cores. But they must share the same cutting boards, ovens, and fridge. If Chef A reaches for the last tomato while Chef B grabs it too, you get a race condition — and a ruined dish. Synchronisation, like a sign-out sheet for ingredients, prevents these conflicts. That is exactly what locks and concurrent collections do in Java: they let threads cooperate without stepping on each other.

## Core explanation

Let us start with the absolute basics. A program is a set of instructions. Normally, a program runs one instruction after another, in order. That is called single-threaded execution. It is simple but slow, especially when the program has to wait for something like a file to load or a network response.

A thread is the smallest unit of work inside a program. You can think of a thread as a single line of execution. A multi-threaded program has multiple threads running at the same time, allowing the program to do several things concurrently. Concurrency means making progress on multiple tasks in overlapping time periods, not necessarily simultaneously. It is about dealing with lots of things at once. Parallelism, on the other hand, means doing lots of things at literally the same time, which requires multiple CPU cores.

To create a thread in Java, you have two main options. The older way is to extend the Thread class and override its run method. The newer and better way is to implement the Runnable interface and pass it to a Thread. Runnable is a functional interface with a single method called run that takes no arguments and returns no value. It is ideal for tasks that do not produce a result.

But what if your task does produce a result? That is where Callable comes in. Callable is also a functional interface, but its call method can return a value and can throw a checked exception. You cannot just give a Callable to a Thread directly. Instead, you submit it to an ExecutorService.

The ExecutorService is a framework for managing threads. Instead of manually creating new Thread objects, you create a thread pool — a collection of pre-made threads that wait for work. You submit tasks to the ExecutorService, and it assigns them to available threads from the pool. This is far more efficient because creating and destroying threads is expensive. The ExecutorService reuses threads.

You can create an ExecutorService using the Executors utility class. Common factory methods include:

- newSingleThreadExecutor() — creates an executor that uses a single worker thread. Tasks run sequentially.
- newFixedThreadPool(int n) — creates a pool with a fixed number of threads. If all threads are busy, tasks wait in a queue.
- newCachedThreadPool() — creates a pool that creates new threads as needed but reuses previously constructed ones when they become available.
- newScheduledThreadPool(int n) — creates a pool that can schedule tasks to run after a delay or at regular intervals.

Once you submit a task to an ExecutorService, you get back a Future object. The Future represents a pending result. You can call get() on it to wait for the result. If the task is a Callable, get() returns the result value. If it is a Runnable, get() returns null. The get() method blocks — it makes the calling thread wait until the result is available. You can also use isDone() to check without blocking.

Now for the tricky part: synchronisation. When multiple threads access shared data, you can run into race conditions. A race condition occurs when the outcome depends on the unpredictable timing of thread execution. For example, two threads might both read a variable at the same time, then both increment it, then both write it back. One increment gets lost.

To prevent this, Java provides the synchronized keyword. You can synchronise a block of code or an entire method. Only one thread at a time can execute a synchronised block on the same object. Think of it as locking the door while you update the shared variable. Other threads must wait outside until you are done.

Java also provides atomic classes like AtomicInteger which use low-level hardware support to perform operations like increment without requiring synchronisation. They are faster for simple operations.

Finally, concurrent collections are thread-safe data structures that handle synchronisation internally. Instead of synchronising manually, you can use classes like ConcurrentHashMap, CopyOnWriteArrayList, or BlockingQueue. They handle the complexity for you. For example, ConcurrentHashMap allows multiple threads to read and write without blocking each other as much as a fully synchronised HashMap would.

Knowing when to use synchronisation, atomic classes, or concurrent collections is a key skill for the exam and for real development. The wrong choice leads to bugs or poor performance.

## Real-world context

Imagine you are writing the backend for a ticketing website. Thousands of users try to buy tickets for a concert at the same time. Each request is processed by a thread in your ExecutorService thread pool. Without proper synchronisation, two threads could both read the same remaining ticket count, both decrement it, and both write the same wrong value back. You would sell the same seat twice. That is a classic race condition.

What a real IT professional does step by step:

1. Starts by assessing the workload. Is the task CPU-intensive (like image processing) or I/O-bound (like waiting for database queries)? This decides the thread pool size. For CPU-heavy tasks, use a pool size close to the number of cores. For I/O-heavy tasks, a larger pool helps.
2. Creates an ExecutorService using Executors.newFixedThreadPool(10) to reuse ten threads for incoming requests.
3. Writes a Callable that queries the database for the ticket count, checks if tickets remain, decrements the count atomically, and returns a booking confirmation. They use an AtomicLong for the ticket count so that decrementing is thread-safe without needing synchronisation.
4. Submits the Callable to the executor and stores the Future object in a map associated with the user session.
5. Handles the result by calling future.get() with a timeout. If the ticket count was zero, the Callable returns a failure message. If successful, it writes the booking to the database and sends a confirmation email.
6. Gracefully shuts down the ExecutorService when the server stops, using shutdown() to stop accepting new tasks and awaitTermination() to wait for ongoing tasks to finish. They also define a timeout so the server does not hang forever if a task is stuck.

Real tools and libraries that professionals use:

- The java.util.concurrent package is the standard. Every Java developer uses it.
- ConcurrentHashMap is the go-to map when dealing with concurrent reads and writes. It avoids full synchronisation.
- BlockingQueue implementations like ArrayBlockingQueue serve as thread-safe work queues. One thread produces data and puts it in the queue, another thread consumes it. This is the producer-consumer pattern.
- CompletableFuture (not directly in the exam objective but used daily) allows chaining async operations without blocking.

The professional also writes unit tests using CountDownLatch or CyclicBarrier to simulate multiple threads hitting the same code, verifying that no race conditions exist. They use tools like Java Flight Recorder to monitor lock contention and thread starvation in production.

Without these skills, the ticketing site would crash under load or oversell tickets. With them, it handles thousands of concurrent users smoothly and correctly.

## Exam focus

The 1Z0-829 exam tests objective 9.1 thoroughly. Expect between 3 and 6 questions on concurrency. The questions are often tricky, with subtle wording designed to catch candidates who memorise without understanding.

Here is exactly what you need to know:

- How to create threads using Runnable vs Callable. The exam loves to give you code that passes a Runnable to a Thread constructor but forgets to call start(). Or passes a Callable to a Thread — which does not compile because Thread's constructor does not accept Callable.
- The ExecutorService methods: submit() returns a Future; execute() returns void (only works with Runnable). The method shutdown() initiates a graceful shutdown; shutdownNow() attempts to stop all actively executing tasks. The method awaitTermination(long timeout, TimeUnit unit) blocks until all tasks complete or the timeout expires.
- The different executor pool types: single, fixed, cached, scheduled. Know which one is appropriate for which scenario. A common trap: a cached thread pool may create too many threads under heavy load, causing OutOfMemoryError.
- The behaviour of Future.get(). It blocks indefinitely unless you call the overloaded get(long timeout, TimeUnit unit). An uncaught exception in the Callable is wrapped in an ExecutionException.
- The synchronized keyword: the difference between synchronising on an instance method vs a static method. Instance methods synchronise on the current object (this). Static methods synchronise on the Class object. The exam shows code where two threads call different synchronised methods and asks whether they block each other.
- Atomic variables: AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference. Know that incrementAndGet() is atomic. The exam might contrast this with the non-atomic i++ operation.
- Concurrent collections: ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedQueue, and BlockingQueue (especially ArrayBlockingQueue). Know that ConcurrentHashMap does not allow null keys or values, unlike HashMap.

Common trap patterns to watch for:

- Code that starts a thread but never joins it, so the main thread finishes before the worker thread even starts. The output order becomes unpredictable.
- Calling run() directly instead of start(). The run() method executes synchronously on the current thread, not on a new thread.
- Forgetting that Runnable is a functional interface. A lambda like () -> System.out.println("hi") works as a Runnable.
- Assuming that placing a shared variable in a synchronised block guarantees visibility across threads. Actually, synchronisation also guarantees happens-before — without it, one thread may never see another thread's changes.
- Mixing up the behaviour of scheduled tasks. schedule() runs once after a delay. scheduleAtFixedRate() runs repeatedly with a fixed period between the start of executions. scheduleWithFixedDelay() runs repeatedly with a fixed delay between the end of one execution and the start of the next.

Key definitions to memorise:

- Thread: a lightweight process.
- Runnable: a task that returns no result and cannot throw checked exceptions.
- Callable: a task that returns a result and can throw checked exceptions.
- ExecutorService: an interface for executing submitted tasks.
- Future: a handle for a pending result of an asynchronous task.
- Synchronisation: coordinating thread access to shared data to prevent race conditions.
- Race condition: a bug where the output depends on the timing of thread execution.
- Deadlock: a situation where two threads are each waiting for the other to release a lock.
- Atomic: an operation that appears to happen all at once, indivisible.

The exam does not test details of the Java Memory Model or advanced topics like lock striping. Keep your focus on the basics listed above.

## Step by step

1. **Define the task** — Decide whether your concurrent work is a Runnable (no result) or a Callable (with a result). Write a class or a lambda that implements the corresponding interface. This step is crucial because the wrong choice will cause a compilation error or force you to handle results awkwardly.
2. **Create an ExecutorService** — Use the Executors utility class to create a thread pool. Choose the pool type based on your workload: single-thread for sequential tasks, fixed pool for predictable load, cached pool for many short-lived tasks. The executor manages thread lifecycle for you.
3. **Submit the task to the executor** — Call executor.submit() if it is a Callable or if you want a Future for a Runnable. Call executor.execute() only if it is a Runnable and you do not need a Future. The submit method returns a Future object that lets you check completion and retrieve the result.
4. **Handle the result (if applicable)** — Call future.get() to retrieve the result of a Callable. This blocks the calling thread until the task completes. Use the overloaded get(long timeout, TimeUnit unit) to avoid indefinite blocking. Catch InterruptedException (if the current thread is interrupted while waiting) and ExecutionException (if the task threw an exception).
5. **Shut down the executor** — Always call executor.shutdown() when you are done submitting tasks. Then call executor.awaitTermination(timeout, unit) to wait for existing tasks to finish. This prevents the JVM from hanging because active threads keep the program alive. Without this step, your application will never terminate.

## Comparisons

### Runnable vs Callable

**Runnable:**
- run method returns void
- Cannot throw checked exceptions
- Can be passed to Thread constructor or ExecutorService.execute()

**Callable:**
- call method returns a value of generic type
- Can throw checked exceptions
- Must be submitted to ExecutorService.submit() to get a Future

### synchronized block vs AtomicInteger

**synchronized block:**
- Uses locks, can cause thread contention
- Slower for simple operations like increments
- Works with any shared object or code block

**AtomicInteger:**
- Uses low-level hardware instructions, no locks
- Faster for single-variable updates like increment
- Only works with specific atomic wrapper types

### shutdown() vs shutdownNow()

**shutdown():**
- Stops accepting new tasks
- Lets already submitted tasks complete normally
- Returns void

**shutdownNow():**
- Stops accepting new tasks
- Attempts to stop actively executing tasks via interrupts
- Returns a list of tasks that never started

### ConcurrentHashMap vs Collections.synchronizedMap(new HashMap())

**ConcurrentHashMap:**
- Allows multiple threads to read and write concurrently
- Does not lock the entire map for writes
- Does not allow null keys or values

**Collections.synchronizedMap(new HashMap()):**
- Locks the entire map for every operation
- Slower under high concurrency due to full locking
- Allows null keys and values (if underlying map does)

### scheduleAtFixedRate vs scheduleWithFixedDelay

**scheduleAtFixedRate:**
- Starts the next execution at a fixed interval from the start of the previous one
- If a task takes longer than the period, the next execution starts immediately after
- Used when you want consistent execution rate regardless of task duration

**scheduleWithFixedDelay:**
- Starts the next execution after a fixed delay from the end of the previous one
- Always waits the full delay between executions
- Used when you want to ensure a minimum gap between task completions

## Diagram

_Flowchart showing how the main thread creates an ExecutorService, which manages a thread pool that executes tasks and returns results via Future objects._

```mermaid
flowchart TD
    A[Main Thread] -->|Creates| B[ExecutorService]
    B -->|Manages| C[Thread Pool]
    C --> D[Thread 1]
    C --> E[Thread 2]
    C --> F[Thread 3]
    D -->|Executes| G[Runnable/Callable Task]
    G -->|Returns| H[Future]
    H -->|get()| I[Result]
    B -->|Shutdown| J[Graceful Shutdown]
```

## Common misconceptions

- **Misconception:** Creating more threads always makes a program faster. **Reality:** Adding threads introduces overhead for context switching and synchronisation. Beyond a certain point, more threads make the program slower. The optimal number depends on CPU cores and the type of task. (This misconception comes from the intuitive idea that doing more things at once must be faster, ignoring the real costs of coordination and resource sharing.)
- **Misconception:** If I use a ConcurrentHashMap, I never need to think about synchronisation again. **Reality:** ConcurrentHashMap ensures individual operations are thread-safe, but compound actions (like check-then-act) still need external synchronisation. For example, checking if a key exists and then inserting it requires synchronisation to prevent a race. (Beginners think thread-safe collections make all code thread-safe, but they only guarantee atomicity for single operations, not sequences of operations.)
- **Misconception:** Volatile makes all operations on a variable atomic. **Reality:** Volatile only guarantees visibility — changes by one thread are immediately visible to others. It does not make compound operations like i++ atomic. You still need AtomicInteger for that. (The word 'volatile' sounds like it prevents all threading problems. Its actual guarantee is narrow and easily misunderstood.)
- **Misconception:** Callable and Runnable are interchangeable. **Reality:** They serve different purposes. Runnable cannot return a value or throw a checked exception. Callable can do both. You submit a Runnable with execute() or submit() and get a Future that returns null. A Callable must be submitted via submit() and returns a specialised Future. (Both are functional interfaces for tasks, so it is easy to assume they are the same. The exam tests this distinction directly.)
- **Misconception:** Calling shutdown() on an ExecutorService stops all running tasks immediately. **Reality:** shutdown() initiates a graceful shutdown. It stops accepting new tasks but lets already submitted tasks complete. To stop running tasks immediately, you call shutdownNow(), which tries to interrupt them. (The word 'shutdown' sounds final and immediate. The distinction between graceful and abrupt shutdown is subtle but frequently tested.)

## Key takeaways

- A Runnable does not return a result and cannot throw checked exceptions; a Callable does both.
- Always call start() on a Thread, not run() — run() executes the task on the current thread synchronously.
- An ExecutorService manages a pool of threads for you; always shut it down with shutdown() and awaitTermination().
- Synchronisation using the synchronized keyword prevents race conditions by ensuring only one thread accesses a critical section at a time.
- Atomic classes like AtomicInteger provide thread-safe operations without explicit locks, and they are faster than synchronisation for simple counters.
- Concurrent collections like ConcurrentHashMap are thread-safe for individual operations but not for compound actions like check-then-act.
- Deadlock occurs when two or more threads are blocked forever, each waiting on the other to release a lock.
- The Future.get() method blocks indefinitely unless you use its timeout overload.

## FAQ

**What is the difference between Runnable and Callable in Java?**

Runnable has a run method that returns void and cannot throw checked exceptions. Callable has a call method that returns a value and can throw checked exceptions. Use Runnable when you do not need a result, use Callable when you do.

**What happens if I call run() instead of start() on a Thread?**

The run() method executes synchronously on the calling thread. No new thread is created. The output may appear correct, but you lose all concurrency benefits.

**What is the difference between shutdown() and shutdownNow() on ExecutorService?**

shutdown() initiates a graceful shutdown: it stops accepting new tasks but lets already submitted tasks run to completion. shutdownNow() attempts to stop all actively executing tasks and returns a list of tasks that never started.

**What is a race condition?**

A race condition occurs when two or more threads access shared data at the same time, and the final outcome depends on the unpredictable timing of their execution. This can cause data corruption or incorrect results.

**What does the synchronized keyword do?**

It ensures that only one thread at a time can execute a synchronised block or method on a given object. It also guarantees memory visibility — changes made by one thread inside the synchronised block are visible to other threads when they enter the same block.

**What does Future.get() do?**

It blocks the calling thread until the task associated with the Future completes, then returns the result. If the task was a Runnable, it returns null. If the task threw an exception, get() throws an ExecutionException.

**What is a deadlock and how can I avoid it?**

A deadlock is when two threads are blocked forever, each waiting for a lock that the other holds. To avoid it, always acquire locks in a consistent order across threads and use timeouts when trying to acquire locks.

## Check your understanding

1. **Which interface should you implement if your task needs to return a result and may throw an exception?**

   Answer: Callable. Its call method returns a value and can throw checked exceptions.

2. **What is the difference between calling thread.start() and thread.run()?**

   Answer: start() creates a new thread that executes the run method. run() executes the run method on the current thread synchronously.

3. **Which ExecutorService method do you call to prevent new tasks from being accepted but allow existing tasks to complete?**

   Answer: shutdown(). It initiates a graceful shutdown.

4. **What type of exception does Future.get() throw if the Callable threw an exception during execution?**

   Answer: ExecutionException.

5. **True or false: A ConcurrentHashMap makes all compound operations like check-then-act thread-safe automatically.**

   Answer: False. ConcurrentHashMap guarantees thread safety for individual operations but not for sequences of operations.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/oracle-ocp-java17/concurrency-and-threads
