Modern programs rarely do one thing at a time. A web server handles thousands of connections simultaneously; a desktop application keeps the interface responsive while it saves a file in the background. In Java, the unit of concurrent work is the thread — an independent path of execution inside the same program.
Creating a thread in Java means giving the JVM a new stack, a new program counter, and a new set of local variables that run alongside the main thread. The operating system scheduler decides when each thread runs and for how long, which means threads are genuinely concurrent on multi-core machines and interleaved on single-core ones. That scheduling decision is outside your control — and that lack of control is the first thing every concurrent programmer must accept.
flowchart LR S["Single thread<br/>A then B then C"] -->|"sequential"| SEQ["predictable order"] M["Multiple threads"] --> T1["Thread 1<br/>A C B"] M --> T2["Thread 2<br/>B A C"] T1 -->|"scheduler decides"| UN["order not guaranteed"] T2 -->|"scheduler decides"| UN style M fill:#c2410c,color:#fff style UN fill:#b45309,color:#fff
Thread and Runnable
There are two ways to create a thread. The first is to extend java.lang.Thread and override its run() method. The second — preferred — is to implement java.lang.Runnable and hand it to a Thread constructor. Runnable is a functional interface with a single void run() method, so you can also supply a lambda.
Implementing Runnable is preferred over extending Thread because Java does not support multiple inheritance. If your class extends Thread, it cannot extend anything else. A Runnable is just a task description; it separates what to run from how to run it, which is why executors accept Runnable directly.
The crucial method is start(), not run(). Calling run() directly is just a normal method call on the current thread — nothing becomes concurrent. Calling start() asks the JVM to schedule the new thread independently, and its run() method will execute later, in parallel or interleaved with whatever the caller does next.
A thread moves through states: NEW after creation, RUNNABLE after start(), RUNNING when the scheduler picks it, TERMINATED when run() finishes. You cannot restart a terminated thread; doing so throws IllegalThreadStateException.
public class Main {
public static void main(String[] args) {
Runnable task = () -> {
System.out.println("Running in: " + Thread.currentThread().getName());
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
}
}
The problem with raw threads
Creating a thread is expensive. The JVM must ask the operating system for resources, allocate a stack (typically one megabyte), and bookkeep the thread in the scheduler. If your server creates a fresh thread for every incoming request, it will spend more time managing threads than serving requests, and it will eventually run out of memory.
Worse, there is no limit. A thousand simultaneous requests create a thousand threads, and the OS grinds to a halt swapping stacks in and out. You also lose control over prioritisation, cancellation, and gathering results. Raw threads are a low-level tool; production code needs a thread pool — a fixed group of worker threads that recycle themselves.
flowchart TD R["Raw threads<br/>1 task = 1 thread"] -->|"expensive"| C["create destroy<br/>create destroy"] P["Thread pool<br/>fixed workers"] -->|"reuse"| Q["Task 1 to Worker A"] P -->|"reuse"| W["Task 2 to Worker B"] P -->|"queue"| QU["Task 3 waits<br/>then reuses A"] style R fill:#b45309,color:#fff style P fill:#3776ab,color:#fff
Executors and thread pools
java.util.concurrent.Executors is a factory class that builds thread pools for common situations. Executors.newFixedThreadPool(4) gives you a pool with exactly four threads; Executors.newCachedThreadPool() creates threads on demand but reuses idle ones; Executors.newSingleThreadExecutor() guarantees tasks run sequentially on one thread.
The returned ExecutorService accepts tasks via submit(Runnable) or execute(Runnable). submit returns a Future that lets you check completion or wait for a result; execute fires and forgets. The pool stores excess tasks in an internal queue and hands them to workers as threads become free. If all four workers are busy, task five waits — memory and CPU stay bounded.
Choosing the right pool matters. A fixed pool is ideal for steady, predictable workloads because it caps resource usage. A cached pool suits burst workloads where tasks are short-lived and you want responsiveness over bounds. A single-thread executor is useful when you need guaranteed sequential execution without synchronizing manually — every task waits for the previous one to finish.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 4; i++) {
final int taskNum = i;
pool.submit(() -> {
System.out.println("Task " + taskNum + " on " + Thread.currentThread().getName());
});
}
pool.shutdown();
}
}
flowchart LR T1["Task 1"] --> W1["Worker 1"] T2["Task 2"] --> W2["Worker 2"] T3["Task 3"] --> Q["Queue"] T4["Task 4"] --> Q Q -->|"when free"| W1 Q -->|"when free"| W2 style Q fill:#c2410c,color:#fff style W1 fill:#3776ab,color:#fff style W2 fill:#3776ab,color:#fff
Shutting down gracefully
An ExecutorService does not stop when your tasks finish — its threads wait for more work. If you forget to shut it down, your program may never exit. Call shutdown() to stop accepting new tasks and let running ones finish. Call shutdownNow() to attempt immediate cancellation, though tasks that ignore interruption may keep running.
Always pair pool creation with shutdown, usually in a try-finally block or using try-with-resources if you wrap the pool in an ExecutorService that implements AutoCloseable. A shutdown pool cannot be restarted; you must build a new one.
A common pattern is to submit all tasks, call shutdown(), then await termination with a timeout: pool.awaitTermination(60, TimeUnit.SECONDS). This gives tasks a fair chance to finish but prevents the program from hanging indefinitely if a task is stuck.
What is the difference between calling t.run() and t.start() on a Thread t?
run() is just a normal method call — it executes synchronously on the current thread. start() tells the JVM to allocate and schedule a new thread of execution, then call run() on that new thread. start() can only be called once per Thread instance.
Why is creating a new Thread for every incoming request generally a bad idea in production?
Each thread allocates a stack and consumes OS resources. Creating unlimited threads for high traffic exhausts memory and slows the system. Thread pools bound the number of concurrent workers and reuse them.
Two threads both call System.out.println. What can you say about the order of the printed lines?
The OS scheduler decides when each thread runs and for how long. The interleaving of output is non-deterministic and can differ between executions, machines, and JVM versions.
What does ExecutorService.shutdown() do?
shutdown() initiates an orderly shutdown: no new tasks are accepted, but already-submitted tasks are allowed to complete. For immediate termination, use shutdownNow().
Which interface should your task implement when you want to hand it to a Thread or ExecutorService?
Runnable is the standard functional interface for tasks that do not return a result. It has a single abstract method, run(), making it ideal for threads and executor pools.
Recap
- A thread is an independent path of execution;
start()schedules it, whilerun()is just a normal method call. - Runnable is the preferred way to define work; pass it to a Thread or an ExecutorService.
- Thread pools reuse a fixed set of workers, bounding memory and avoiding the cost of per-task thread creation.
ExecutorServiceaccepts tasks viasubmit()andexecute(); always callshutdown()when done.- Thread scheduling is non-deterministic: never depend on the order in which threads produce output or finish work.
Next you will learn how to protect shared data when multiple threads access it simultaneously — and the hazards that await when you do not.