Java • Java-Specific Topics

Runnable & Thread — Use Runnable/Thread at an introductory level (conceptual)

Learn how Java programs do multiple things at once using threads and the Runnable interface.

Java programming emblem with a coffee cup, code braces, and terminal
Inside this lesson 10 sections 5 questions 5 figures

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

Early mainframes introduced the concept of sharing processor time among multiple users and tasks. This planted the seed for concurrent programming.

1988 • POSIX Threads (pthreads)

The POSIX standard formalized how threads should work across Unix systems, making multithreaded C programs portable between different machines.

1995 • Java 1.0 Ships with Built-In Threads

Java was one of the first mainstream languages to include threading as a core feature. The Thread class and Runnable interface were part of the language from day one.

2004 • Java 5 — java.util.concurrent

Java added higher-level concurrency utilities like thread pools and executors, but Runnable and Thread remained the foundational building blocks.

2014 • Java 8 — Lambdas Simplify Runnable

Lambda expressions made it possible to write a Runnable in a single line of code, making thread creation easier than ever.

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.

1

Thread

A thread is an independent path of execution within a program. Think of it as a separate worker that can run code at the same time as other workers. In Java, the Thread class represents and controls one such worker.
2

Runnable Interface

The Runnable interface defines a single method, run(). Any class that implements Runnable is essentially saying, 'I have a job to do.' The job's instructions go inside the run() method.
3

Main Thread

Every Java program starts with a main thread — the thread that runs your main() method. Without creating additional threads, everything happens sequentially on this single thread.
4

start() vs. run()

Calling 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.
5

Concurrency vs. Parallelism

Concurrency means managing multiple tasks that can overlap in time. Parallelism means tasks physically execute at the same instant on separate CPU cores. Threads enable both, depending on the hardware.
✦

KEY TAKEAWAY

Think of a restaurant kitchen. A Runnable is like a recipe card — it describes what needs to be done. A Thread is like a chef — the worker who reads the recipe and actually does the cooking. You can hand the same recipe to multiple chefs, and they can all cook at the same time. Separating the 'what' (recipe) from the 'who' (chef) is the key design idea behind Runnable and Thread.

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.

Java Thread LifecycleNEWRUNNABLERUNNINGBLOCKEDTERMINATEDstart()CPU scheduledwaiting / I/Oresource availablerun() completesyield🔵 new Thread(myRunnable)🟢 thread.start() begins life🟡 sleep(), wait(), I/O blocks🔴 run() returns → dead
A thread begins in the NEW state when you create it. Calling 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.

Approach 1: Extend ThreadApproach 2: Implement Runnablejava.lang.Thread+ run() + start()MyTask extends Thread@Override run()extends❌ Cannot extend another class«interface» Runnable+ run()MyTask implements Runnable@Override run()implementsThreadnew Thread(myTask)passed to✅ Can still extend another classBest Practice: Prefer Runnable — it gives you more design flexibility
On the left, extending Thread uses up your one allowed superclass. On the right, implementing Runnable only uses an interface, so your class remains free to extend another class. The Runnable approach also separates the task logic from the threading mechanism.
FeatureExtends ThreadImplements Runnable
InheritanceCannot extend another classFree to extend any class
Separation of concernsTask and thread are mixed togetherTask is cleanly separated from thread
ReusabilityHarder to reuse the task elsewhereSame Runnable can be passed to multiple Threads or a thread pool
Code simplicitySlightly less code (no separate Thread constructor)One extra line to create the Thread object
Industry preferenceRarely used in production codePreferred 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. 1

    Step 1 — Define the Task with Runnable

    Create a class that implements 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); } } }
    The Countdown class now holds the task logic, completely independent of any specific thread.
  2. 2

    Step 2 — Create Thread Objects

    In your 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");
    Both threads reference the same Runnable object. At this point, both threads are in the NEW state.
  3. 3

    Step 3 — Start the Threads

    Call start() (not run()!) 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. 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: 1
    The interleaving proves that both threads are executing concurrently. The exact order depends on how the operating system schedules each thread.
⚠

Common Mistake

If you had written 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.

StrengthsLimitations
Built into Java — no external libraries neededCreating too many threads can slow your program down instead of speeding it up
Fine-grained control over each thread's behaviorYou must manually manage thread creation, starting, and cleanup
Makes programs responsive — one thread can handle user input while another does heavy computationShared 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 designThe run() method cannot return a value or throw checked exceptions
Foundation for all higher-level concurrency tools in JavaDebugging multithreaded code is significantly harder than single-threaded code

Strengths and limitations of using Runnable and Thread

✦

KEY TAKEAWAY

Think of threads like lanes on a highway. Adding a second lane (thread) can double throughput, but adding 500 lanes to a two-mile stretch creates chaos — merging, collisions, and overhead. Similarly, creating thousands of threads wastes memory and CPU time on scheduling. In real-world Java, thread pools (covered in later lessons) help manage this, but Runnable and Thread are the building blocks underneath.

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 ConceptAdvanced EquivalentWhat It Adds
RunnableCallable<V>Can return a value and throw checked exceptions
new Thread(r).start()ExecutorServiceManages a pool of reusable threads automatically
Manual synchronizationjava.util.concurrent locksMore flexible and efficient locking strategies
Checking thread completion manuallyFuture<V>Represents a result that will be available later
Shared mutable dataAtomicInteger, ConcurrentHashMapThread-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

Explain in your own words the difference between calling myThread.start() and calling myThread.run(). Why does this distinction matter?
Show answer
Calling 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

A Java program creates 4 Thread objects, each given the same Runnable that prints one line. The main method calls 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
At most, there are 5 threads active: the main thread plus the 4 newly created threads. The total number of lines printed is 4, because each thread's 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

You have a class 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
Java does not support multiple inheritance for classes — a class can only extend one superclass. If you extend 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

You are building a simple chat application. The program needs to simultaneously listen for incoming messages from the network and read user input from the keyboard. Describe how you would use Runnable and Thread to structure this. How many threads would you create, and what would each one do?
Show answer
You would create at least two Runnables: one for listening to the network (let's call it 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

Two threads both execute the following Runnable, which increments a shared variable 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
No, you would not always get 2000. This is a classic race condition. The increment operation 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.

Varsity Tutors • Java • Runnable & Thread — Use Runnable/Thread at an introductory level (conceptual)