Java Thread Class
Table of Contents + −
In the last lesson you learned an introduction to threads. Now let’s look closely at the tool that makes a thread: the Thread class. We will create a thread, settle the famous start vs run confusion, and walk through Thread’s handy methods.
🤔 Why look at the Thread class?
Every thread in your program is a Thread object, even the main thread. Understand this one class and threads become normal objects you can name, inspect, pause, and wait on.
This lesson fixes real pain:
- People call
run()thinking it starts a thread, and nothing runs in parallel. - People start the same thread twice and get a crash.
- People assume output comes out in a tidy order and write buggy code.
All of these come from not knowing how the Thread class works. Let’s learn it.
🛠️ Creating a thread by extending Thread
The first way to make a thread is to write a class that extends Thread and override run. Whatever code you put inside run becomes the new thread’s job. Here is a thread that prints three numbers.
class CounterThread extends Thread { @Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println("Counting: " + i); } }}
public class Main { public static void main(String[] args) { CounterThread t = new CounterThread(); t.start(); // ✅ launches a NEW thread that runs run()
System.out.println("Main is done setting up"); }}Reading it step by step:
class CounterThread extends Threadmakes our class a kind ofThread. It inheritsstart,getName, and the rest.public void run()holds the work. This is the only method the new thread executes.new CounterThread()builds the object, but nothing runs yet.t.start()creates a real new thread, which callsrunfor you.- The
printlninmainruns on the main thread, which did not wait.
Output (order may vary)
Main is done setting upCounting: 1Counting: 2Counting: 3The main line and the counting lines can appear in different orders on different runs. That is normal, explained later.
⚠️ start() vs run(): the difference that trips everyone
This is the most important idea in the lesson. You call start() to run code on a brand new thread. Calling run() yourself runs the method right here on the thread you are already on, like any normal method call.
Let’s prove it. The thread below prints the name of whatever thread runs its run method. We launch it both ways and compare.
class NameThread extends Thread { @Override public void run() { System.out.println("run() is executing on: " + Thread.currentThread().getName()); }}
public class Main { public static void main(String[] args) { NameThread t1 = new NameThread(); t1.start(); // ✅ runs run() on a new thread
NameThread t2 = new NameThread(); t2.run(); // ❌ runs run() on the main thread, no new thread }}Here Thread.currentThread().getName() reports the actual thread running the code, so the output tells us where each call ran.
Output (order may vary)
run() is executing on: Thread-0run() is executing on: mainThe output proves it. start() ran the work on a separate thread named Thread-0. run() ran it on main, the same thread we were already on.
The difference in plain words:
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 current thread. No new path, no parallel work.
Always use start()
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 most common multithreading mistake.
🏷️ Naming threads: getName, setName, and getId
Every thread has a name and a numeric id. Names make output easier to read, because you can tell which thread printed which line. With no name set, Java picks a default like Thread-0.
The example below sets a name with the constructor and with setName, then reads the name and id back.
class TaskThread extends Thread { public TaskThread(String name) { super(name); // pass a name up to Thread }
@Override public void run() { System.out.println("Name: " + getName() + ", id: " + getId()); }}
public class Main { public static void main(String[] args) { TaskThread a = new TaskThread("Loader"); a.start();
TaskThread b = new TaskThread("temp"); b.setName("Saver"); // rename before or while it runs b.start(); }}Reading it through:
super(name)passes a name up to theThreadconstructor, sogetName()returns it.setName("Saver")changes the name after the object is built. Both ways work.getId()returns a unique number Java assigns to each thread. You cannot set it; Java controls it.
Output (order may vary, ids will differ)
Name: Loader, id: 21Name: Saver, id: 22The id numbers will differ on your machine, and the two lines can swap order. Each thread has its own readable name and id.
😴 Pausing with sleep() and finding the current thread
Two helpers come up constantly. Thread.sleep(ms) pauses the current thread for a number of milliseconds. Thread.currentThread() hands you the Thread object you are running on, so you can read its name or id.
This example pauses the main thread for one second, then prints which thread it is on.
public class Main { public static void main(String[] args) throws InterruptedException { System.out.println("Start on: " + Thread.currentThread().getName()); Thread.sleep(1000); // pause this thread for 1 second System.out.println("One second later"); }}A few things to note.
Thread.sleep(1000)pauses for 1000 milliseconds, which is one second. It always pauses the thread that calls it, not some other thread.Thread.currentThread().getName()is"main"here, because the main thread runsmain.sleepcan throwInterruptedException, so the method declaresthrows InterruptedException. You must either declare it or wrap the call in try/catch.
Output
Start on: mainOne second later⏳ Waiting with join() and checking isAlive()
Sometimes the main thread needs a worker to finish first. For that you use join(). Calling worker.join() makes the caller pause until the worker finishes. The isAlive() method tells you whether a thread has started and not yet finished.
The thread below does slow work, and the main thread waits for it.
class SlowJob extends Thread { @Override public void run() { System.out.println("Working..."); try { Thread.sleep(1000); // pretend the job is slow } catch (InterruptedException e) { System.out.println("Job interrupted"); } System.out.println("Done working"); }}
public class Main { public static void main(String[] args) throws InterruptedException { SlowJob job = new SlowJob(); job.start();
System.out.println("Alive before join? " + job.isAlive()); job.join(); // main waits here until job finishes System.out.println("Alive after join? " + job.isAlive());
System.out.println("Main continues safely"); }}Reading the flow:
job.start()launches the worker, which begins its pretend one-second job.job.isAlive()istrueright after starting, because the thread is running.job.join()pauses the main thread until the worker finishes. Without this line, “Main continues safely” could print before the work was done.- After
joinreturns,isAlive()isfalsebecause the thread has ended.
Output
Working...Alive before join? trueDone workingAlive after join? falseMain continues safelySee how join removes the guesswork. The last main line always comes after the job, because the main thread truly waited.
🎚️ Priority and daemon threads
The Thread class gives you two more knobs:
- setPriority(int) hints which threads matter more, from
Thread.MIN_PRIORITY(1) toThread.MAX_PRIORITY(10), defaultThread.NORM_PRIORITY(5). It is only a hint. The system can ignore it, so never rely on it for order. setDaemon(true)marks a thread as a background helper. The program does not wait for daemon threads before it exits. User threads do keep the program alive until done.
The example shows both. setDaemon must be called before start().
class Background extends Thread { @Override public void run() { System.out.println(getName() + " priority: " + getPriority() + ", daemon: " + isDaemon()); }}
public class Main { public static void main(String[] args) { Background t = new Background(); t.setName("Helper"); t.setPriority(Thread.MAX_PRIORITY); // hint: higher importance t.setDaemon(true); // must be set before start() t.start(); }}What the methods do.
setPriority(Thread.MAX_PRIORITY)asks the scheduler to favour this thread. It is a hint, not a promise.getPriority()reads the priority back, which is10here.setDaemon(true)makes the thread a background helper that does not hold the program open.isDaemon()confirms the daemon flag is on.
Output
Helper priority: 10, daemon: trueYou will rarely need priority or daemon threads early on, so just keep them handy.
🎲 Why output order is not fixed
A part of the system called the scheduler decides which thread gets the CPU and for how long. It can pause one thread, let another run, then switch back. You do not control these switches, and they can land differently every run.
The example below starts two threads that each print a few lines. Run it twice and the lines mix in different orders.
class Printer extends Thread { public Printer(String name) { super(name); }
@Override public void run() { for (int i = 1; i <= 3; i++) { System.out.println(getName() + " -> " + i); } }}
public class Main { public static void main(String[] args) { new Printer("Alpha").start(); new Printer("Beta").start(); }}Output (one possible order)
Alpha -> 1Beta -> 1Alpha -> 2Alpha -> 3Beta -> 2Beta -> 3Run it again and the mix differs. The rule: never write code that depends on thread order. If you need one thread done first, make the waiting explicit with join.
🔀 extends Thread vs implements Runnable
There are two ways to give a thread its work, and the preferred way for real code is Runnable:
- Extend Thread uses up your one inheritance slot, so the class cannot extend anything else.
- Implement Runnable is an interface, so the class stays free to extend something else.
Runnablealso separates the “what to do” from the “how to run it.”
Extending Thread is the simple route. Runnable, covered next, is the one you reach for in real projects.
⚠️ 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; runs on the current threadt.start(); // ✅ launches a new thread, then calls run() on it- Starting the same thread twice. A thread can only be started once. Starting it again throws
IllegalThreadStateException.
Printer p = new Printer("Alpha");p.start();p.start(); // ❌ throws IllegalThreadStateException
Printer p2 = new Printer("Alpha");p2.start(); // ✅ create a fresh thread insteadThat second start() produces an error like this.
Error
Exception in thread "main" java.lang.IllegalThreadStateException at java.base/java.lang.Thread.start(Thread.java:794) at Main.main(Main.java:13)- Relying on output order. Threads run independently, so their lines can interleave differently each run.
new Printer("Alpha").start();new Printer("Beta").start();// ❌ do not assume "Alpha" prints before "Beta"// ✅ if order matters, use join() or proper coordination✅ Best Practices
Habits for working with the Thread class.
- Use
start()to launch a thread. Keeprun()for the work itself, never as the way to start. - Start a thread only once. If you need to run the work again, create a fresh
Threadobject. - Give threads clear names. A name like “Loader” in the output makes threaded logs far easier to read.
- 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 one thread needs another’s result, have it wait instead of guessing. - Treat priority as a hint. Never depend on
setPriorityto control which thread runs first. - Handle
InterruptedException. Either declarethrowsor wrapsleepandjoinin try/catch so the code compiles and behaves.
🧩 What You’ve Learned
Nicely done. Let’s recap the Thread class.
- ✅ A thread is a
Threadobject; you can make one by extending Thread and overridingrun. - ✅ Call
start()to runrunon a new thread; callingrun()directly runs it on the current thread with no parallelism. - ✅
getName,setName, andgetIdlabel and identify threads;Thread.currentThread()gives the running thread. - ✅
sleep(ms)pauses the current thread,join()waits for another to finish, andisAlive()checks if it is still running. - ✅
setPriorityis only a hint, andsetDaemon(true)(set beforestart()) makes a background helper thread. - ✅ Output order across threads is not fixed, starting a thread twice throws
IllegalThreadStateException, andRunnableis usually preferred over extendingThread.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
Which method runs run() on a brand new thread?
Why: start() creates a new thread and then calls run() on it. Calling run() directly runs on the current thread.
- 2
What happens if you call start() twice on the same Thread object?
Why: A thread can only be started once. Calling start() again throws IllegalThreadStateException; create a new object instead.
- 3
What does Thread.currentThread() return?
Why: Thread.currentThread() hands you the Thread object that is executing the current code.
- 4
What is true about setPriority?
Why: Priority is just a hint. The scheduler is free to ignore it, so never rely on it to control order.
🚀 What’s Next?
Extending Thread is the simple way to make a thread, but it locks up your single chance to extend another class. The cleaner and more common approach is to implement the Runnable interface and pass it to a Thread. Let’s learn that next.