Master

Concurrency and Multithreading

Build responsive apps with threads, executors, and synchronized data.

What you will see

2

About 2 min of reading

Try It

Run work concurrently. Launch a thread with a lambda, then use an AtomicInteger that stays safe without explicit locks, watch how each thread keeps the shared value consistent.

1

Start a thread from a lambda that prints its own name. The thread runs independently and start() hands it to the scheduler; join waits for it to finish before main ends.

class Main { public static void main(String[] args) throws Exception { Thread t = new Thread(() -> { System.out.println("Running in " + Thread.currentThread().getName()); }); t.start(); t.join(); } }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Running in Thread-0
The thread name is assigned by the JVM, so the exact label is not something you hard-code. t.join() blocks main until the task is done.
2

Guard shared state without synchronized using an AtomicInteger. Two back-to-back increments show the value counting up safely even though each increment is its own call.

import java.util.concurrent.atomic.AtomicInteger; class Main { public static void main(String[] args) { AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); System.out.println(count.incrementAndGet()); } }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
2
AtomicInteger uses hardware-level operations, so no lock is needed. Prefer it over a synchronized counter when you only need to increment shared state.

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

Work is submitted to a thread pool; each task runs concurrently and returns a future the caller awaits.
1 Submit task to executor
↓
2 A worker thread picks it up
↓
3 Task runs concurrently
↓
4 Future holds the result for the caller
Why this works
Thread lifecycle

A thread moves through states: NEW after creation, RUNNABLE when executing, BLOCKED waiting for a lock, WAITING for a signal, and TERMINATED when done. The scheduler decides when runnable threads actually run on a CPU.

Avoiding deadlocks

A deadlock happens when threads wait on each other forever. Avoid by always acquiring locks in the same order, using timeouts with tryLock, or preferring lock-free constructs:

import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();   // thread-safe without synchronized

AtomicInteger and similar atomic classes use hardware-level operations to stay safe without explicit locks.

Examples

Creating a thread

Running work in the background

Thread t = new Thread(() -> {
    System.out.println("Running in " + Thread.currentThread().getName());
});
t.start();

A lambda passed to Runnable defines the work. start() runs it on a separate thread. Use Thread or implement Runnable.

Using an ExecutorService

Managing thread pools

try (ExecutorService pool = Executors.newFixedThreadPool(4)) {
    Future<Integer> f = pool.submit(() -> compute());
    int result = f.get();   // block until done
}

ExecutorService manages a reusable pool of threads. submit returns a Future you can await. Prefer pools over spawning raw threads.

Synchronising shared state

Safe concurrent access

class Counter {
    private int value;

    public synchronized int increment() {
        return ++value;
    }
}

synchronized ensures only one thread enters the method at a time, preventing race conditions on shared mutable state. Prefer higher-level utilities like AtomicInteger when possible.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Design Patterns
Previous Lesson
Design Patterns
Next Lesson
Advanced Streams and Generics
Advanced Streams and Generics →