Introduction to Threads in Java

In the last lesson you learned about Java BufferedWriter. So far your programs did one thing at a time. But real programs often need to do several things at once, and Java does that with threads.

Threads let you:

  • Download a file while keeping the screen responsive.
  • Handle many users at the same time.
  • Run a slow task without freezing everything.

Let’s learn what a thread is and how to start one.

🤔 Why do we need threads?

Picture a program that downloads a big file on one path. The whole program freezes during the download. The user clicks and nothing happens. The fix is to move slow work onto a separate path of execution. That separate path is a thread. It is like hiring an extra worker.

Why people reach for threads:

  • Keep an app responsive. The screen reacts while a slow task runs in the background.
  • Finish faster. Two independent jobs run in parallel, not one after the other.
  • Use the whole machine. Threads let your program use more than one CPU core at once.
  • Serve many requests. A web server handles many users at the same time, one thread per request.

A quick note on words:

  • Concurrency means several tasks make progress in overlapping time.
  • Parallelism means they run at the very same instant on different cores.

For now, treat both as “doing several things at once.”

🧩 What is a thread?

Think of a program as a kitchen. One cook (one thread) chops, then stirs, then plates. Nothing overlaps. Add more cooks (more threads) and the work happens side by side, so the meal is ready sooner. That is the whole idea behind threads.

The key facts:

  • A thread is a single path of execution within a program.
  • Every Java program already has one thread, the main thread, which runs your main method.
  • Multithreading means having more than one thread running at the same time.
  • All threads share the same memory, objects, and variables. That sharing is powerful but also tricky.

Two cooks reaching for the same knife can clash. We handle that clash in the synchronization lesson.

🛠️ Creating a thread by extending Thread

One way to make a thread is to extend the Thread class and override its run method. Whatever code you put inside run is the job the new thread carries out. The example below prints a few numbers.

class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println("Thread says: " + i);
}
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start(); // ✅ starts a NEW thread that runs run()
System.out.println("Main thread keeps going");
}
}

Reading it line by line:

  • class MyThread extends Thread makes our class a kind of Thread, so it can run on its own path.
  • public void run() holds the work. This is the only method the new thread executes.
  • new MyThread() creates the object, but nothing runs yet.
  • t.start() launches a new thread, which runs run in parallel while main moves on.
  • The println in main runs on the main thread, which did not wait.

Two paths now run side by side. Because they run at the same time, the output order can change from run to run.

Output (order may vary)

Main thread keeps going
Thread says: 1
Thread says: 2
Thread says: 3

The preferred way for most real code is implementing the Runnable interface, not extending Thread. That is the very next lesson. Here we focus on the Thread route.

⚠️ start() vs run(): a crucial difference

This trips up almost everyone. You call start() to run code on a new thread. Calling run() directly does not start a thread. It just runs the method on the current thread.

MyThread t = new MyThread();
t.run(); // ❌ WRONG for threading: runs on the main thread, no parallel work
t.start(); // ✅ RIGHT: starts a new thread and runs run() on it

The difference in plain terms:

  • start() asks the system to create a new thread, which then calls run for you. Your code runs in parallel.
  • run() is just a method. Calling it runs its code on the thread you are already on. No new path, no parallel work.

Think of start() as hiring a new cook. Calling run() yourself is doing the cook’s job with your own hands. The work gets done, but there is no extra worker and no speed-up.

Always use start(), never call run() directly

To run work on a new thread, call start(). Calling run() directly just executes the method on the current thread with no new thread at all. This is the single most common multithreading mistake.

🔄 The thread lifecycle

A thread moves through a small set of states during its life. You do not control these by hand. The system moves the thread between them.

  • New. The thread object exists but you have not called start() yet.
  • Runnable. You called start(). The thread is running now or waiting its turn for the CPU.
  • Running. The CPU is executing the thread’s run code right now.
  • Blocked or Waiting. The thread is paused, maybe by sleep, join, or a lock another thread holds.
  • Terminated. run finished. A terminated thread is done for good and cannot start again.

Java’s own enum has a single RUNNABLE value, but it helps to keep “ready” and “actually running” separate in your head.

🔧 Useful thread methods

A handful of methods let you control and inspect threads:

  • Thread.sleep(ms) pauses the current thread for a number of milliseconds.
  • join() makes one thread wait until another finishes.
  • isAlive() returns true if a thread has started and not yet terminated.
  • setName(...) and getName() set and read a thread’s name.

The example below uses sleep, getName, and currentThread, which hands you the thread you are running on.

public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("Start");
Thread.sleep(1000); // pause the main thread for 1 second
System.out.println("One second later");
System.out.println("Current thread: " + Thread.currentThread().getName());
}
}

Reading it through:

  • Thread.sleep(1000) pauses for 1000 milliseconds, which is one second. It is useful for delays or for pretending a task is slow while you test.
  • Thread.currentThread() returns the thread that is running this code right now.
  • getName() reads that thread’s name, which is "main" here because the main thread is running main.
  • sleep can throw InterruptedException, so the method declares throws InterruptedException. You must either declare it or wrap the call in try/catch.

Output

Start
One second later
Current thread: main

🧪 Two threads, interleaved output

Let’s make the parallel nature real. The program below starts two threads. Each prints its name with a number and pauses a little between prints, so the two take turns on the CPU.

class Worker extends Thread {
public Worker(String name) {
super(name); // give the thread a readable name
}
@Override
public void run() {
for (int i = 1; i <= 3; i++) {
System.out.println(getName() + " -> " + i);
try {
Thread.sleep(100); // small pause so threads take turns
} catch (InterruptedException e) {
System.out.println(getName() + " was interrupted");
}
}
}
}
public class Main {
public static void main(String[] args) {
Worker alex = new Worker("Alex");
Worker riya = new Worker("Riya");
alex.start(); // ✅ both run in parallel
riya.start();
System.out.println("Main started both workers");
}
}

What is happening here:

  • super(name) passes a name up to Thread, so getName() returns "Alex" or "Riya".
  • Both start() calls launch real new threads. Now three threads exist: main, Alex, and Riya.
  • The short sleep(100) makes each worker pause and give the other a turn. That is why the lines mix together.

Output (one possible order)

Main started both workers
Alex -> 1
Riya -> 1
Alex -> 2
Riya -> 2
Alex -> 3
Riya -> 3

Run it again and the order may differ. That is expected. The threads are independent, so their lines arrive in whatever order the system schedules them.

🎲 Why output order is not fixed

A part of the system called the scheduler decides which thread gets the CPU and for how long. Here is what follows:

  • It can pause one thread mid-way, let another run, then switch back.
  • You do not control these switches. They can land differently every run.
  • So never write code that depends on threads printing or finishing in a particular order.
  • If you need one thread’s result first, make the waiting explicit with join, shown next.

⏳ Waiting for a thread with join()

Sometimes the main thread needs a worker’s result before it can continue. The download must finish before you show the file. For that you use join(). Calling worker.join() makes the caller pause until that worker has terminated.

class Downloader extends Thread {
@Override
public void run() {
System.out.println("Downloading...");
try {
Thread.sleep(1000); // pretend the download takes a second
} catch (InterruptedException e) {
System.out.println("Download interrupted");
}
System.out.println("Download finished");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Downloader d = new Downloader();
d.start();
System.out.println("isAlive before join? " + d.isAlive());
d.join(); // main waits here until d finishes
System.out.println("isAlive after join? " + d.isAlive());
System.out.println("Now showing the file");
}
}

Reading the flow:

  • d.start() launches the downloader, which begins its pretend one-second download.
  • d.isAlive() is true right after starting, because the thread is running.
  • d.join() pauses the main thread until the downloader terminates. Without this line, “Now showing the file” could print before the download finishes.
  • After join returns, isAlive() is false because the thread is done, and the main thread safely continues.

Output

Downloading...
isAlive before join? true
Download finished
isAlive after join? false
Now showing the file

Notice how join removes the guesswork. The last line always comes after the download, because the main thread truly waited.

👻 Daemon threads, briefly

A daemon thread is a background helper that the program does not wait for. You mark one with t.setDaemon(true) before start().

  • Use a daemon thread for background chores like cleanup or polling.
  • The Java Virtual Machine shuts down once the last user thread ends, even if daemon threads still run.
  • Set the daemon flag before start(), or it throws an exception.

You will rarely need daemon threads early on, so just keep the idea handy.

⚠️ Common Mistakes

A few thread slip-ups to watch for.

  • Calling run() instead of start(). This runs the code on the current thread with no parallel work at all.
t.run(); // ❌ no new thread, everything stays on the current thread
t.start(); // ✅ launches a new thread, then calls run() on it
  • Expecting a fixed output order. Threads run independently, so their lines can interleave differently each run.
alex.start();
riya.start();
// ❌ do not assume "Alex" prints before "Riya"
// ✅ if order matters, use join() or proper coordination
  • Calling start() twice on one thread. A thread can only be started once. Starting it again throws IllegalThreadStateException.
Worker w = new Worker("Alex");
w.start();
w.start(); // ❌ throws IllegalThreadStateException
Worker w2 = new Worker("Alex");
w2.start(); // ✅ create a fresh thread instead

✅ Best Practices

Habits for working with threads.

  • Use start() to launch a thread. Keep run() for the work itself, never as the way to start.
  • Do not assume an order. Design so the result is correct no matter how the scheduler interleaves the threads.
  • Use join() to wait when needed. If the main thread needs a worker’s result, have it wait instead of guessing.
  • Give threads clear names. A name like “Alex” in the output makes threaded logs far easier to read.
  • Handle InterruptedException. Either declare throws or wrap sleep/join in try/catch so the code compiles and behaves.
  • Keep thread work independent. Threads that share data need extra care, which is the synchronization lesson.

🧩 What You’ve Learned

Nicely done. Let’s recap threads.

  • ✅ A thread is a single path of execution; multithreading runs several at once and uses multiple CPU cores.
  • ✅ Every program has a main thread; you can create more to do work in parallel.
  • ✅ One way to make a thread is to extend Thread and override run (implementing Runnable is the preferred way, coming next).
  • ✅ Call start() to launch a new thread; calling run() directly gives no parallel work.
  • ✅ A thread moves through New, Runnable, Running, Blocked/Waiting, and Terminated.
  • ✅ Helpful methods include Thread.sleep, join, isAlive, and getName, and output order across threads is not fixed.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is a thread?

    Why: A thread is one path of execution; multithreading runs several at the same time.

  2. 2

    Which method actually starts a new thread?

    Why: start() creates a new thread and then runs run() on it; calling run() directly does not.

  3. 3

    What happens if you call run() directly instead of start()?

    Why: Calling run() directly executes it like a normal method on the current thread, with no new thread.

  4. 4

    What does Thread.sleep(1000) do?

    Why: sleep pauses the current thread for the given number of milliseconds.

🚀 What’s Next?

Extending Thread works, but it helps to look closely at the Thread class itself, its constructors, and the methods it gives you. Let’s explore the Thread class next.

Java Thread Class

Share & Connect