Historical Context & Motivation
In the early days of computing, programs ran one instruction at a time from start to finish. If a program needed to download a file and also update the screen, it had to finish the download completely before it could refresh the display. This made programs feel slow and unresponsive, even though the computer itself was fast. The solution was multithreading β the ability for a single program to do multiple things at the same time by dividing work into separate paths of execution called threads.
Thread class and Runnable interface were part of the language from day one.Runnable and Thread remained the foundational building blocks.Today, almost every application β from web servers to mobile apps to video games β uses threads to stay responsive. Java's Runnable interface and Thread class give you the tools to split a program into multiple concurrent paths of execution. The key question this lesson addresses: how do you tell Java what to do (Runnable) and where to do it (Thread)?
Core Principles & Definitions
Before writing any code, you need to understand five foundational ideas that govern how Java handles concurrency. These concepts form the mental model you will use every time you work with threads.
Thread
Thread class represents and controls one such worker.Runnable Interface
run(). Any class that implements Runnable is essentially saying, 'I have a job to do.' The job's instructions go inside the run() method.Main Thread
main() method. Without creating additional threads, everything happens sequentially on this single thread.start() vs. run()
start() on a Thread object launches a new thread of execution. Calling run() directly just runs the code on the current thread β no new thread is created. This is the most common beginner mistake.Concurrency vs. Parallelism
Visual Explanation β Thread Lifecycle
A thread in Java goes through several states during its life. Understanding this lifecycle helps you predict what your code will do when threads interact. The diagram below shows the five major states a thread can be in, and the transitions between them.
start() moves it to RUNNABLE. The operating system schedules it to RUNNING. It may become BLOCKED if it waits for a resource. Once run() completes, the thread is TERMINATED and cannot be restarted.Notice that once a thread reaches the TERMINATED state, you cannot call start() on it again. If you need the same task to repeat, you must create a new Thread object. This is another reason why separating the task (Runnable) from the worker (Thread) is so useful β you can reuse the same Runnable with a fresh Thread.
How It Works β Two Ways to Create Threads
Java gives you two approaches to create a new thread. Both accomplish the same goal, but they differ in flexibility and design. Understanding both methods β and when to prefer one over the other β is essential.
Approach 1 β Extending the Thread Class
You can create a subclass that extends Thread and override its run() method. When you call start() on an instance of your subclass, Java creates a new thread and executes your run() method on it. Here is the pattern:
class MyTask extends Thread {
public void run() {
System.out.println("Running in: " + getName());
}
}
MyTask t = new MyTask();
t.start(); // launches a NEW thread
Because MyTask already extends Thread, it cannot extend any other class. Java only allows single inheritance.Approach 2 β Implementing the Runnable Interface
The preferred approach is to implement the Runnable interface. You write a class that implements Runnable, define your task in run(), and then pass an instance to a Thread constructor. This separates the task from the thread machinery.
class MyTask implements Runnable {
public void run() {
System.out.println("Running in: "
+ Thread.currentThread().getName());
}
}
Thread t = new Thread(new MyTask());
t.start(); // launches a NEW thread
Because MyTask only implements an interface, it is free to extend another class if needed. This makes Runnable much more flexible.The Critical Difference: start() vs. run()
If you accidentally call t.run() instead of t.start(), Java will execute the run() method on the current thread β no new thread is created. Your code will still work, but it will run sequentially. Always use start() to actually launch a new thread.
Extends Thread vs. Implements Runnable
Now that you have seen both approaches, let's compare them side by side. The diagram below illustrates how the class relationships differ, and the table that follows summarizes the practical trade-offs.
| Feature | Extends Thread | Implements Runnable |
|---|---|---|
| Inheritance | Cannot extend another class | Free to extend any class |
| Separation of concerns | Task and thread are mixed together | Task is cleanly separated from thread |
| Reusability | Harder to reuse the task elsewhere | Same Runnable can be passed to multiple Threads or a thread pool |
| Code simplicity | Slightly less code (no separate Thread constructor) | One extra line to create the Thread object |
| Industry preference | Rarely used in production code | Preferred in professional Java |
Worked Example β Countdown with Two Threads
Let's trace through a complete example where two threads count down simultaneously. We will use the Runnable approach since it is the recommended practice.
Runnable. Inside run(), write a loop that counts from 3 down to 1 and prints the thread's name along with the number.
class Countdown implements Runnable {
public void run() {
for (int i = 3; i >= 1; i--) {
System.out.println(
Thread.currentThread().getName()
+ ": " + i);
}
}
}Countdown class now holds the task logic, completely independent of any specific thread.main() method, create a single Runnable instance and pass it to two separate Thread constructors. Give each thread a descriptive name.
Runnable task = new Countdown();
Thread t1 = new Thread(task, "Thread-A");
Thread t2 = new Thread(task, "Thread-B");start() (not run()!) on each thread to move them into the RUNNABLE state.
t1.start();
t2.start();Thread-A: 3
Thread-B: 3
Thread-A: 2
Thread-B: 2
Thread-B: 1
Thread-A: 1t1.run(); t2.run(); instead of t1.start(); t2.start();, the output would always be Thread-A: 3, 2, 1 followed by Thread-B: 3, 2, 1 β completely sequential, with no concurrency at all.Strengths & Limitations of Basic Threading
Using Runnable and Thread directly gives you full control, but it also comes with responsibilities. The table below summarizes the key strengths and limitations you should be aware of as you begin working with threads.
| Strengths | Limitations |
|---|---|
| Built into Java β no external libraries needed | Creating too many threads can slow your program down instead of speeding it up |
| Fine-grained control over each thread's behavior | You must manually manage thread creation, starting, and cleanup |
| Makes programs responsive β one thread can handle user input while another does heavy computation | Shared data between threads can cause race conditions (bugs where two threads modify data at the same time) |
| Runnable separates the task from threading, promoting clean design | The run() method cannot return a value or throw checked exceptions |
| Foundation for all higher-level concurrency tools in Java | Debugging multithreaded code is significantly harder than single-threaded code |
Connection to Advanced Concurrency
Once you are comfortable with Runnable and Thread, Java provides a whole ecosystem of more powerful tools built on top of these basics. The table below previews how these foundational concepts connect to what you will encounter next.
| Basic Concept | Advanced Equivalent | What It Adds |
|---|---|---|
Runnable | Callable<V> | Can return a value and throw checked exceptions |
new Thread(r).start() | ExecutorService | Manages a pool of reusable threads automatically |
| Manual synchronization | java.util.concurrent locks | More flexible and efficient locking strategies |
| Checking thread completion manually | Future<V> | Represents a result that will be available later |
| Shared mutable data | AtomicInteger, ConcurrentHashMap | Thread-safe data structures that avoid race conditions |
Every one of these advanced tools depends on the same core ideas you have learned in this lesson: a task defines what to execute, and a thread (or thread pool) determines where and when to execute it. Mastering Runnable and Thread gives you a solid foundation for everything that follows. As you move into AP Computer Science or college-level courses, you will see these patterns everywhere β from Android app development to building web servers.
Practice Problems
myThread.start() and calling myThread.run(). Why does this distinction matter?start() on all four threads. How many threads are active in the program at most (including the main thread)? How many total lines will be printed?Animal that you want to extend, and you also need to run some code in a new thread. Why can't you simply extend both Animal and Thread? Describe how using Runnable solves this problem, and write the class signature (just the first line) that makes this work.Runnable and Thread to structure this. How many threads would you create, and what would each one do?count from 0 to 1000 in a loop. After both threads finish, would you always expect count to equal 2000? Why or why not? (Hint: think about what happens when two threads read and write the same variable at the exact same moment.)Lesson Summary
Java provides two fundamental approaches to multithreading. A Runnable is an interface with a single run() method that defines the task β the work to be done. A Thread is the worker that executes that task on a separate path of execution. You can create threads by extending Thread or by implementing Runnable and passing it to a Thread constructor. The Runnable approach is preferred because it preserves inheritance flexibility and cleanly separates the task from the threading mechanism.
Always call start() to launch a new thread β calling run() directly runs code on the current thread with no concurrency. A thread moves through the states NEW β RUNNABLE β RUNNING β TERMINATED, and once terminated, it cannot be restarted. While basic threading gives you fine-grained control, be aware of challenges like race conditions when threads share data. These foundational concepts β Runnable and Thread β are the building blocks for all of Java's advanced concurrency tools, including Callable, ExecutorService, and Future.