Multithreading and Concurrency in Java
Multithreading lets a program do multiple things at the same time. Each thread is an independent path of execution inside the same program — while one thread downloads a file, another can keep the UI responsive or process data.
public class Main { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); // main }}Every Java program already has at least one thread — the
mainthread. Multithreading is about starting more of them.
Why Multithreading?
- Responsiveness — a long task (file download, DB query) doesn’t freeze the whole program.
- Better CPU usage — modern CPUs have multiple cores; threads let you use them all.
- Parallel work — process independent tasks (e.g., resize 100 images) at the same time.
Creating Threads
1. Extending Thread
class DownloadTask extends Thread { @Override public void run() { System.out.println("Downloading in: " + Thread.currentThread().getName()); }}
public class Main { public static void main(String[] args) { DownloadTask t = new DownloadTask(); t.start(); // starts a NEW thread and calls run() }}2. Implementing Runnable (preferred)
class DownloadTask implements Runnable { @Override public void run() { System.out.println("Downloading in: " + Thread.currentThread().getName()); }}
public class Main { public static void main(String[] args) { Thread t = new Thread(new DownloadTask()); t.start(); }}With a lambda (since Runnable is a functional interface):
Thread t = new Thread(() -> System.out.println("Running: " + Thread.currentThread().getName()));t.start();Prefer
Runnableover extendingThread— Java has single inheritance, so extendingThreadwastes your only superclass slot, andRunnableseparates the task from the thread that runs it.
start() vs run()
t.run(); // ❌ runs on the CURRENT thread — just a normal method callt.start(); // ✅ creates a new thread, which then calls run()Calling start() twice on the same thread throws IllegalThreadStateException.
Thread Lifecycle
A thread moves through these states (Thread.State):
| State | Meaning |
|---|---|
NEW | Created but start() not called yet |
RUNNABLE | Running or ready to run |
BLOCKED | Waiting to enter a synchronized block |
WAITING | Waiting indefinitely (join(), wait()) |
TIMED_WAITING | Waiting with a timeout (sleep(1000)) |
TERMINATED | run() finished |
Thread t = new Thread(() -> {});System.out.println(t.getState()); // NEWt.start();System.out.println(t.getState()); // RUNNABLEsleep() and join()
Thread.sleep(millis) — pause the current thread
System.out.println("Order placed...");Thread.sleep(2000); // pause 2 seconds (throws InterruptedException)System.out.println("Order confirmed!");join() — wait for another thread to finish
Thread worker = new Thread(() -> { for (int i = 1; i <= 3; i++) { System.out.println("Processing step " + i); }});
worker.start();worker.join(); // main waits here until worker is doneSystem.out.println("All steps finished");Without join(), "All steps finished" could print before the worker completes.
The Problem: Race Conditions
When two threads modify shared data at the same time, results become unpredictable:
class Counter { int count = 0;
void increment() { count++; // NOT atomic: read → add → write }}
public class Main { public static void main(String[] args) throws InterruptedException { Counter counter = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter.increment(); }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) counter.increment(); });
t1.start(); t2.start(); t1.join(); t2.join();
System.out.println(counter.count); // expected 20000 — often prints LESS! }}Both threads read the same value, increment it, and overwrite each other’s update. This is a race condition.
Synchronization
The synchronized keyword ensures only one thread at a time can run a block/method on the same object:
class Counter { private int count = 0;
synchronized void increment() { // one thread at a time count++; }
synchronized int getCount() { return count; }}Now the output is reliably 20000.
Synchronized block (finer control)
void increment() { // ...non-critical work... synchronized (this) { count++; // only this part is locked }}Atomic classes — lock-free alternative
For simple counters, java.util.concurrent.atomic is faster than locking:
import java.util.concurrent.atomic.AtomicInteger;
AtomicInteger count = new AtomicInteger(0);count.incrementAndGet(); // atomic, thread-safe, no synchronized neededDeadlock warning: if thread A holds lock 1 and waits for lock 2, while thread B holds lock 2 and waits for lock 1 — both wait forever. Always acquire multiple locks in the same order.
ExecutorService — Thread Pools
Creating a Thread per task is expensive. A thread pool reuses a fixed set of threads:
import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;
public class Main { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) { int taskId = i; executor.submit(() -> System.out.println("Task " + taskId + " on " + Thread.currentThread().getName())); }
executor.shutdown(); // stop accepting new tasks, finish running ones }}Task 1 on pool-1-thread-1Task 2 on pool-1-thread-2Task 3 on pool-1-thread-3Task 4 on pool-1-thread-1Task 5 on pool-1-thread-2Common factory methods:
| Method | Use case |
|---|---|
newFixedThreadPool(n) | Fixed number of threads — most common |
newCachedThreadPool() | Grows/shrinks as needed — many short tasks |
newSingleThreadExecutor() | Tasks run one-by-one, in order |
newScheduledThreadPool(n) | Run tasks after a delay or periodically |
Always call
shutdown()— otherwise the pool’s threads keep the JVM alive.
Callable and Future — Getting a Result Back
Runnable returns nothing. Callable<V> returns a value (and can throw checked exceptions). Submitting one gives you a Future<V> — a handle to the result:
import java.util.concurrent.*;
public class Main { public static void main(String[] args) throws Exception { ExecutorService executor = Executors.newFixedThreadPool(2);
Callable<Integer> priceTask = () -> { Thread.sleep(1000); // simulate API call return 4999; };
Future<Integer> future = executor.submit(priceTask);
System.out.println("Doing other work..."); Integer price = future.get(); // blocks until result is ready System.out.println("Price: " + price);
executor.shutdown(); }}Useful Future methods: get() (wait for result), get(2, TimeUnit.SECONDS) (wait with timeout), isDone(), cancel(true).
Runnable vs Callable
Runnable | Callable<V> | |
|---|---|---|
| Method | void run() | V call() |
| Returns a value | ❌ No | ✅ Yes |
| Throws checked exceptions | ❌ No | ✅ Yes |
| Used with | Thread, executors | Executors only |
Quick Reference
| Concept | Tool |
|---|---|
| Run a task on a new thread | new Thread(runnable).start() |
| Pause current thread | Thread.sleep(ms) |
| Wait for a thread | thread.join() |
| Protect shared data | synchronized, AtomicInteger |
| Reuse threads | ExecutorService / thread pools |
| Get a result from a task | Callable + Future |
Common Mistakes
// ❌ Calling run() instead of start() — no new thread createdt.run();
// ❌ Forgetting join() — main finishes before workerst.start();System.out.println("done"); // may print too early
// ❌ Unsynchronized shared mutable state — race conditioncount++;
// ❌ Forgetting shutdown() — JVM never exitsexecutor.submit(task);
// ✅ Correct patternExecutorService executor = Executors.newFixedThreadPool(2);Future<Integer> f = executor.submit(() -> compute());Integer result = f.get();executor.shutdown();Summary
- A thread is an independent path of execution; every program starts with the
mainthread. - Create threads with
Runnable(preferred) and always usestart(), neverrun(). sleep()pauses the current thread;join()waits for another to finish.- Shared mutable data needs
synchronizedor atomic classes to avoid race conditions. - Use
ExecutorServicethread pools instead of raw threads, andCallable+Futurewhen you need a result back.