SECTION 1
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.
1960s • Time-Sharing Operating Systems
1988 • POSIX Threads (pthreads)
1995 • Java 1.0 Ships with Built-In Threads
Thread class and Runnable interface were part of the language from day one.2004 • Java 5 — java.util.concurrent
Runnable and Thread remained the foundational building blocks.2014 • Java 8 — Lambdas Simplify Runnable
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)?
SECTION 2
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
KEY TAKEAWAY
SECTION 3
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.
SECTION 4
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:
CODE PATTERN — Extending Thread
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.
CODE PATTERN — Implementing Runnable
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.
SECTION 5
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 |
Comparison of the two threading approaches in Java
SECTION 6
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.
Two Threads Counting Down at the Same Time
- 1
Step 1 — Define the Task with Runnable
Create a class that implementsRunnable. Insiderun(), 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); } } }TheCountdownclass now holds the task logic, completely independent of any specific thread. - 2
Step 2 — Create Thread Objects
In yourmain()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");Both threads reference the same Runnable object. At this point, both threads are in the NEW state. - 3
Step 3 — Start the Threads
Callstart()(notrun()!) on each thread to move them into the RUNNABLE state.t1.start(); t2.start();The JVM now has three threads running: the main thread, Thread-A, and Thread-B. - 4
Step 4 — Observe Interleaved Output
Because both threads run concurrently, their output may interleave in unpredictable order. One possible output is shown below. Running the program again might produce a different order.Thread-A: 3 Thread-B: 3 Thread-A: 2 Thread-B: 2 Thread-B: 1 Thread-A: 1The interleaving proves that both threads are executing concurrently. The exact order depends on how the operating system schedules each thread.
Common Mistake
t1.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.SECTION 7
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 |
Strengths and limitations of using Runnable and Thread
KEY TAKEAWAY
SECTION 8
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 |
How Runnable and Thread connect to advanced Java concurrency
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
myThread.start() and calling myThread.run(). Why does this distinction matter?Show answer
start() tells the JVM to create a brand-new thread of execution and then invoke the run() method on that new thread. Calling run() directly just executes the method on the current thread, exactly like calling any other ordinary method — no new thread is created. This distinction matters because the entire purpose of using threads is to achieve concurrency. If you call run() directly, your code runs sequentially and you gain no concurrency benefit at all. It is one of the most common beginner mistakes in Java threading.Question 2: BASIC CALCULATION
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?Show answer
run() method prints exactly one line. The main thread itself does not print in this scenario. Note that 'at most 5' is the answer because some of the child threads may have already finished before the last one starts, depending on scheduling.Question 3: INTERMEDIATE
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.Show answer
Animal, you cannot also extend Thread. However, Java allows a class to extend one class and implement as many interfaces as it needs. Since Runnable is an interface, you can write: class Dog extends Animal implements Runnable. This lets Dog inherit from Animal and still define a run() method that can be executed on a new thread by passing a Dog instance to a Thread constructor.Question 4: APPLIED
Runnable and Thread to structure this. How many threads would you create, and what would each one do?Show answer
NetworkListener) and one for reading keyboard input (let's call it InputReader). Each Runnable would be passed to its own Thread, giving you two new threads plus the main thread — three total. The NetworkListener thread would run in a loop, waiting for incoming messages and printing them to the screen. The InputReader thread would run in a loop, reading lines from System.in and sending them over the network. Without threads, the program would freeze while waiting for network input and would be unable to accept keyboard input, or vice versa.Question 5: CRITICAL THINKING
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.)Show answer
count++ is actually three steps at the CPU level: (1) read the current value of count, (2) add 1, (3) write the new value back. If Thread-A reads count as 500 and Thread-B also reads count as 500 before Thread-A writes 501, both threads will write 501 — losing one increment entirely. The final value will be somewhere between 1000 and 2000, and it will likely be different each time you run the program. Solving this requires synchronization, which is covered in future lessons.SUMMARY
Lesson Summary
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.