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.
Jump to a section
A simple way to picture Concurrency and Threads
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.
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.
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.
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.
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.
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).
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.
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:
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.
Creates an ExecutorService using Executors.newFixedThreadPool(10) to reuse ten threads for incoming requests.
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.
Submits the Callable to the executor and stores the Future object in a map associated with the user session.
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.
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.
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.
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.
These come up on the exam all the time. Here's how to tell them apart.
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
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()
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
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
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
Mistake
Creating more threads always makes a program faster.
Correct
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.
Mistake
If I use a ConcurrentHashMap, I never need to think about synchronisation again.
Correct
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.
Mistake
Volatile makes all operations on a variable atomic.
Correct
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.
Mistake
Callable and Runnable are interchangeable.
Correct
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.
Mistake
Calling shutdown() on an ExecutorService stops all running tasks immediately.
Correct
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.
Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.
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.
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.
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.
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.
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.
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.
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.
You've finished Concurrency and Threads. Continue through the 1Z0-829 study guide to build a complete picture of the exam.
Done with this chapter?