Introduction to Threads in Java
Table of Contents + −
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
mainmethod. - 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 Threadmakes our class a kind ofThread, 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 runsrunin parallel while main moves on.- The
printlninmainruns 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 goingThread says: 1Thread says: 2Thread says: 3The 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 workt.start(); // ✅ RIGHT: starts a new thread and runs run() on itThe difference in plain terms:
start()asks the system to create a new thread, which then callsrunfor 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
runcode right now. - Blocked or Waiting. The thread is paused, maybe by
sleep,join, or a lock another thread holds. - Terminated.
runfinished. 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()returnstrueif a thread has started and not yet terminated.setName(...)andgetName()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 runningmain.sleepcan throwInterruptedException, so the method declaresthrows InterruptedException. You must either declare it or wrap the call in try/catch.
Output
StartOne second laterCurrent 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 toThread, sogetName()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 workersAlex -> 1Riya -> 1Alex -> 2Riya -> 2Alex -> 3Riya -> 3Run 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()istrueright 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
joinreturns,isAlive()isfalsebecause the thread is done, and the main thread safely continues.
Output
Downloading...isAlive before join? trueDownload finishedisAlive after join? falseNow showing the fileNotice 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 ofstart(). This runs the code on the current thread with no parallel work at all.
t.run(); // ❌ no new thread, everything stays on the current threadt.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 throwsIllegalThreadStateException.
Worker w = new Worker("Alex");w.start();w.start(); // ❌ throws IllegalThreadStateExceptionWorker w2 = new Worker("Alex");w2.start(); // ✅ create a fresh thread instead✅ Best Practices
Habits for working with threads.
- Use
start()to launch a thread. Keeprun()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 declarethrowsor wrapsleep/joinin 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
Threadand overriderun(implementingRunnableis the preferred way, coming next). - ✅ Call
start()to launch a new thread; callingrun()directly gives no parallel work. - ✅ A thread moves through New, Runnable, Running, Blocked/Waiting, and Terminated.
- ✅ Helpful methods include
Thread.sleep,join,isAlive, andgetName, 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
What is a thread?
Why: A thread is one path of execution; multithreading runs several at the same time.
- 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
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
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.