AlgoInsight

Free lesson · Concurrency Interview Mastery

Creating Threads in Java

Medium6 min readconcurrencymultithreadingjava

This is the reference for getting code running on a thread in Java. We'll move fast and stay concrete - the concepts behind all this live in Module 1; here we're just nailing down the Java mechanics and the choices that matter. Quick orientation from me before the code: in modern Java you almost never create raw Thread objects by hand. You hand work to an executor and let it manage threads. But you need to know the raw mechanics first, because interviewers ask about them and because the higher-level tools are built on top.

There are essentially four ways to get work onto a thread, in rough order from low-level to high-level: extending Thread, implementing Runnable, implementing Callable (for results), and submitting to an ExecutorService.

Way 1: Extend the Thread class

Subclass Thread, override run(), call start().

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Running on: " + Thread.currentThread().getName());
    }
}

MyThread t = new MyThread();
t.start();   // starts a NEW thread, calls run() on it

This works but is the least preferred approach. Java has single inheritance, so extending Thread burns your one superclass slot on a threading detail, and it conflates what to run (your logic) with how to run it (the thread). Prefer Runnable.

Way 2: Implement Runnable (preferred for simple tasks)

Runnable is a functional interface with a single run() method. You pass it to a Thread.

Runnable task = () -> {
    System.out.println("Running on: " + Thread.currentThread().getName());
};

Thread t = new Thread(task);
t.start();

This separates the task from the thread. Your class is free to extend something else, the task is reusable, and the lambda form is clean. Whenever you'd reach for extends Thread, use Runnable instead.

Warning: The single most common beginner bug: calling t.run() instead of t.start(). Calling run() directly just executes the method on the current thread, synchronously, like any normal method call - no new thread is created at all. Only start() spins up a new thread and schedules run() on it. If your "threads" seem to run one after another in order, you almost certainly called run(). This is a classic interview gotcha - be ready to explain the difference.

flowchart LR
    A["t.start()"] --> B[New thread created]
    B --> C["run() executes on the NEW thread"]
    D["t.run()"] --> E["run() executes on the CURRENT thread<br/>no new thread, fully synchronous"]
This flowchart contrasts start and run. Calling t dot start creates a new thread, and the run method executes on that new thread. Calling t dot run directly does not create any thread - the run method simply executes on the current thread, synchronously, like a normal method call. The takeaway is that only start launches a new thread; run by itself does nothing concurrent.

Way 3: Callable and Future (when you need a result)

Runnable.run() returns nothing and can't throw checked exceptions. When your task needs to return a value or throw, use Callable<V>, which returns a V. You don't run a Callable on a raw Thread - you submit it to an executor, which hands you a Future<V> to retrieve the result later.

Callable<Integer> task = () -> {
    Thread.sleep(100);
    return 42;
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(task);

Integer result = future.get();   // blocks until the result is ready
executor.shutdown();

future.get() blocks the calling thread until the result is available. That blocking is important to remember - it's where naive code accidentally serializes itself. There's also future.get(timeout, unit) to bound the wait, future.isDone() to poll, and future.cancel() to attempt cancellation.

Interface Returns a value? Can throw checked exceptions? How you run it
Runnable No (void run()) No Thread or executor
Callable<V> Yes (V call()) Yes Executor → Future<V>

Way 4: ExecutorService - the way you should actually do it

Creating a raw Thread per task is expensive and unbounded - spin up thousands and you exhaust memory and thrash on context switches (see Module 1 on why we pool). The modern, idiomatic approach is a thread pool via ExecutorService: create a managed pool once, submit tasks to it, reuse threads.

ExecutorService pool = Executors.newFixedThreadPool(4);  // 4 reused threads

for (int i = 0; i < 100; i++) {
    final int id = i;
    pool.submit(() -> System.out.println("Task " + id));
}

pool.shutdown();   // no new tasks accepted; existing ones finish

Common factory methods on Executors:

Tip: Rule of thumb: don't new Thread() in application code. Submit tasks to an ExecutorService and let it own the threads. You get pooling, bounded resource use, lifecycle management, and a clean path to Future results. The raw Thread API is for understanding the model and for the rare low-level case; executors are for real work. In an interview, "I'd use an ExecutorService rather than creating threads directly" is the answer that signals you've worked at scale.

Running many tasks and shutting down cleanly

Two practical things you'll do constantly with executors: run a batch of tasks and collect results, and shut the pool down properly.

invokeAll submits a collection of Callables and blocks until all finish, returning their Futures in order:

List<Callable<Integer>> tasks = List.of(
    () -> compute(1), () -> compute(2), () -> compute(3));

List<Future<Integer>> results = pool.invokeAll(tasks);  // waits for all
for (Future<Integer> f : results) {
    System.out.println(f.get());   // each is already done
}

Shutdown has two flavors, and confusing them is a common bug:

pool.shutdown();                              // stop accepting new work
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
    pool.shutdownNow();                       // force if it didn't finish in time
}

Warning: A pool you never shut down keeps its threads alive and can prevent the JVM from exiting (non-daemon pool threads are user threads). Forgetting shutdown() is a classic resource leak - the program "hangs" at the end for no obvious reason. Always shut your executors down, ideally in a finally or a lifecycle hook.

Thread essentials you'll be asked about

A few Thread mechanics worth keeping at your fingertips:

Interview: Expect these three: (1) "Difference between start() and run()?" - start() creates a new thread and runs run() on it; calling run() directly executes synchronously on the current thread. (2) "Runnable vs Callable?" - Callable returns a value and can throw checked exceptions; Runnable does neither. (3) "Why use a thread pool instead of creating threads?" - bounded resources, thread reuse avoids creation cost, and managed lifecycle; unbounded thread creation exhausts memory and thrashes. Answer all three crisply and you've cleared the Java-threading basics bar.

Quick reference

// Runnable (no result)
new Thread(() -> doWork()).start();

// Callable (result) via executor
Future<T> f = executor.submit(() -> computeResult());
T result = f.get();

// Preferred: pool + submit
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> doWork());
pool.shutdown();

That's the toolkit for getting code onto threads in Java. Next we go under it - the Java Memory Model, the rules that decide when one thread actually sees another thread's writes, which is what makes all of the above safe or unsafe.

Back to Concurrency Interview Mastery