Creating Threads
Creating Threads
In Java there are two classic ways to define the unit of work that a thread should execute: extend Thread or implement Runnable. A third option — Callable with a Future — is covered in a later lesson on thread pools. For now we focus on the fundamentals, and on the single most important rule beginners break every day: calling start(), not run().
Option 1: Extending Thread
Because java.lang.Thread implements Runnable itself, you can subclass it and override run() directly:
The two threads run concurrently: the output lines from Thread-A and Thread-B will interleave in a non-deterministic order.
run() directly. Calling t1.run() instead of t1.start() does not create a new thread — it is a plain method call on the current thread, the same as calling any other method. No concurrency happens at all. This is one of the most common mistakes in beginner concurrent code.
Option 2: Implementing Runnable
Runnable is a functional interface (one abstract method: void run()). You pass an instance of it to a Thread constructor:
Because Runnable is a functional interface you can also write the task inline with a lambda — no separate class needed:
start() vs run() — the mechanics
Understanding what start() actually does prevents a whole class of bugs:
start()asks the JVM to allocate a native OS thread, schedule it, and eventually invokerun()on that new thread. The calling thread returns immediately fromstart()and continues its own work.run()is just a normal Java method. Calling it directly runs the logic on the caller's thread, sequentially, with zero concurrency.- Calling
start()on a thread that has already been started throwsIllegalThreadStateException. AThreadobject is single-use.
Extending Thread vs Implementing Runnable — the trade-offs
Both approaches create threads, but they differ in design:
- Extending
Thread— easy for small demos, but Java only allows single inheritance. If your class already extends something else, this door is closed. It also tightly couples the task logic to the threading mechanism. - Implementing
Runnable— separates what to do from how it is scheduled. The sameRunnablecan be submitted to a thread pool (ExecutorService), run inline in tests, or wrapped in a virtual thread (Java 21). This flexibility is whyRunnableis almost always the right choice in production code.
Runnable (or a lambda) for tasks; use the Thread class only when you specifically need to configure thread properties (setDaemon, setName, setPriority) or when you extend it to build a specialised thread type such as a pooled worker. In modern Java you will almost never extend Thread in application code.
Naming threads
Always give threads meaningful names. The default names (Thread-0, Thread-1, …) are useless when reading a thread dump during a production incident:
With a named thread, a stack trace immediately tells you which logical component is stuck or consuming CPU.
Summary
There are two classical ways to create threads: extend Thread (override run()) or implement Runnable (pass it to a Thread constructor). Always call start() to get a new thread — run() is just a method call. Prefer Runnable or a lambda in production code because it decouples the task from the execution mechanism, leaves your inheritance hierarchy open, and works seamlessly with ExecutorService and virtual threads. Next we will look at the full lifecycle a thread passes through from creation to termination.