Create and start threads — Thread class, Runnable, and the lifecycle of a thread.
Published September 21, 2026
A thread is an independent path of execution inside a program. Every Java program starts with one, the main thread, and can create more. All threads in a process share the same heap memory (objects, static fields), but each has its own stack (local variables and method calls).
Why use more than one?
The price is that shared memory, accessed by several threads at once, introduces bugs that don't exist in single-threaded code. Most of the Java concurrency course is about managing that price.
// 1. A Runnable (preferred: the task is separate from the thread)
Runnable task = () -> System.out.println("Running in " + Thread.currentThread().getName());
Thread t1 = new Thread(task, "worker-1");
t1.start();
// 2. Subclassing Thread (works, but mixes "what to do" with "the thread itself")
class Downloader extends Thread {
@Override public void run() { download(); }
}
new Downloader().start();
// 3. Java 21 builders — platform or virtual
Thread t2 = Thread.ofPlatform().name("indexer").start(task);
Thread t3 = Thread.ofVirtual().start(task);
start() vs run(): start() creates a new thread, which then calls run(). Calling run() directly just executes the method on the current thread, with no concurrency. This is a common beginner mistake. Calling start() twice on the same thread throws IllegalThreadStateException.
In real applications, you rarely create threads by hand. You submit tasks to an ExecutorService (see ExecutorService & Thread Pools), or use virtual threads for I/O-heavy work (see Virtual Threads).
NEW ──start()──▶ RUNNABLE ◀────────────────────────────┐
│ │
waiting for a monitor lock ──▶ BLOCKED ───────────┤
wait() / join() / park() ──▶ WAITING ───────────┤
sleep(ms) / wait(ms) / join(ms) ▶ TIMED_WAITING ──┘
│
run() returns / throws ──▶ TERMINATED
RUNNABLE means "eligible to run". Whether it's actually on a CPU right now is up to the OS scheduler. You can inspect a thread's state with getState(), and thread dumps (jstack, or jcmd <pid> Thread.print) show every thread's state, which is essential for diagnosing hangs.
Thread worker = new Thread(() -> processBatch());
worker.start();
worker.join(); // wait for it to finish (join(ms) waits with a timeout)
Thread.sleep(500); // pause the current thread; throws InterruptedException
Java has no safe way to forcibly kill a thread (Thread.stop() was deprecated because it could leave objects half-updated). Instead, you ask a thread to stop with interrupt(), and the thread checks for it:
Thread poller = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
pollQueue();
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore the flag, then exit the loop
}
}
});
poller.start();
// later
poller.interrupt();
Blocking methods (sleep, wait, join, BlockingQueue.take) respond to interruption by throwing InterruptedException, which clears the interrupt flag. Never swallow it silently. Either propagate it, or restore the flag with Thread.currentThread().interrupt(), so the code above you can see it.
Thread t = new Thread(task);
t.setDaemon(true); // must be set before start()
The JVM exits when only daemon threads remain. Use daemon threads for background housekeeping that shouldn't keep the application alive. They can be stopped abruptly at exit, so don't use them for work that must complete, such as writing files.
class Counter {
private int count = 0;
void increment() { count++; } // NOT atomic: read, add, write
int get() { return count; }
}
Counter c = new Counter();
Thread a = new Thread(() -> { for (int i = 0; i < 100_000; i++) c.increment(); });
Thread b = new Thread(() -> { for (int i = 0; i < 100_000; i++) c.increment(); });
a.start(); b.start();
a.join(); b.join();
System.out.println(c.get()); // usually LESS than 200000
count++ is three steps. Both threads can read 41, both write 42, and one increment is lost. This is a race condition. Two related problems:
The fixes, covered in the following lessons:
synchronized or Lock for mutual exclusion (see synchronized and Locks).AtomicInteger.incrementAndGet()).ConcurrentHashMap).Thread-37 are hard to debug.Thread.setDefaultUncaughtExceptionHandler) to log them properly.setPriority) are just hints that the OS may ignore. Don't rely on them for correctness.Thread.yield() is a scheduler hint, and almost never needed.Q: What's the difference between a process and a thread? A: A process has its own memory space, isolated from other processes. Threads live inside a process and share its heap, which makes communication cheap but requires synchronization. Each thread has its own stack and program counter.
Q: What happens if you call run() instead of start()?
A: The method executes synchronously on the calling thread, like any normal method call. No new thread is created, so there's no concurrency.
Q: How do you stop a thread safely?
A: Call interrupt(), and write the thread's code to check isInterrupted() and to handle InterruptedException by cleaning up and exiting. For executor tasks, cancel the Future or shut down the executor, which interrupts running tasks.
Q: Why is count++ not thread-safe?
A: It's a read, an increment and a write. Two threads can interleave those steps, and both write the same value, losing an update. Use AtomicInteger, LongAdder, or synchronization.
Q: What's the difference between BLOCKED and WAITING?
A: BLOCKED means the thread is waiting to acquire a monitor lock (entering a synchronized block). WAITING means it's waiting indefinitely for another thread to act: wait(), join(), or LockSupport.park(), which also covers waits on java.util.concurrent locks.