1. The Thread class: your first thread in Java
In Java, every thread of execution is represented by an instance of the Thread class. This class is like a conductor in an orchestra—it’s the one that controls starting, stopping, and the thread’s lifecycle.
To start a thread, you need to:
- Create an instance of the Thread class (or a subclass of it).
- Call the start() method on that instance.
Let’s see it in practice.
Example 1. Extending Thread
The most straightforward way to create a thread is to extend Thread and override its run() method. Everything you write inside run() will execute in a separate thread.
// Simple thread via inheritance
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("Hello from a thread! My name is: " + getName());
}
}
public class Main {
public static void main(String[] args) {
HelloThread thread = new HelloThread(); // create a thread object
thread.start(); // start the thread
System.out.println("The main thread is finishing.");
}
}
What actually happens?
When you call thread.start(), Java creates a new thread and then runs the run() method inside it. Meanwhile, the main thread (the one that started with main) doesn’t wait and continues its work in parallel.
And the key warning: don’t confuse start() with a direct call to run(). If you write thread.run(), no new thread will be created—it’s just a regular method that executes in the same thread where you call it. Real multithreading starts only with start().
What is getName()?
The getName() method returns the thread’s name. By default, Java gives threads names like "Thread-0", "Thread-1", and so on. This is convenient for debugging.
2. The Runnable interface: the best approach for most cases
Java is a language that values flexibility. The Thread class already extends Object, and Java doesn’t support multiple inheritance of classes. If you extend your class from Thread, you won’t be able to extend it from anything else (for example, if you have your own Car class and want to make it a thread—you’ll have to choose).
The solution: move the thread logic into a separate object that implements the Runnable interface. This interface has a single method, run(). Then you create a Thread instance, pass your Runnable to it, and start the thread.
Example 2. A class with Runnable
// A class implementing Runnable
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from Runnable! Thread: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Runnable runnable = new HelloRunnable();
Thread thread = new Thread(runnable); // pass the Runnable to Thread
thread.start();
System.out.println("The main thread is finishing.");
}
}
Here we create a HelloRunnable class that implements the Runnable interface. In the main method, an instance of this class is created and passed to the Thread constructor. After that, the thread is started by calling start().
It’s worth mentioning Thread.currentThread(). This static method lets you obtain the current thread object and thus understand exactly where your code is executing.
Why is this better?
- Your class can extend any other class (not just Thread).
- You can reuse the same Runnable to run in multiple threads.
- The code becomes more flexible and cleaner.
3. Syntax: anonymous classes and lambda expressions
Java hasn’t stood still! Since Java 8 we can write even more concisely and conveniently.
Example 3. Anonymous class
Sometimes you don’t want to create a separate file for a class you only need once. You can declare it right where you use it—this is called an anonymous class.
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Anonymous Runnable! Thread: " + Thread.currentThread().getName());
}
});
thread.start();
System.out.println("The main thread is finishing.");
}
}
Example 4. Lambda expression (Java 8+)
The Runnable interface is functional (only one abstract method). Therefore, you can use a lambda expression:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Lambda thread! Thread: " + Thread.currentThread().getName());
});
thread.start();
System.out.println("The main thread is finishing.");
}
}
In short:
You implement Runnable in place, and a lambda expression supplies the implementation of the run() method.
4. Starting multiple threads
You can start as many threads as you like (well, almost—it depends on your computer’s resources).
Example 5. Starting multiple threads
public class Main {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int threadNumber = i; // be sure to copy the variable for the lambda!
Thread thread = new Thread(() -> {
System.out.println("Thread #" + threadNumber + ": hello!");
});
thread.start();
}
System.out.println("The main thread is finishing.");
}
}
The output may vary!
Threads run in parallel—who prints first depends on the OS and the thread scheduler.
5. Thread lifecycle
Understanding how a thread lives is important for debugging and proper use of threads.
Main thread states
- NEW — the thread has been created but not started yet (new Thread(...)).
- RUNNABLE — the thread has been started (start()) and may run.
- TERMINATED — the thread has finished executing (the run() method has completed).
What happens when calling start() and run()
- start() — creates a new thread and invokes its run() method in that new thread.
- run() — a regular method; if called directly, it runs in the current thread (it does not create a new thread!).
Example:
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("run() from thread: " + Thread.currentThread().getName()));
thread.run(); // CALLED IN THE MAIN THREAD!
thread.start(); // CALLED IN A SEPARATE THREAD!
}
}
6. Practice: build a simple multithreaded application
Let’s continue building our sample application—suppose we’re writing a simple logging system where each thread writes its own message.
public class LoggerTask implements Runnable {
private final String message;
public LoggerTask(String message) {
this.message = message;
}
@Override
public void run() {
System.out.println("[" + Thread.currentThread().getName() + "] Log: " + message);
}
}
public class Main {
public static void main(String[] args) {
Thread logger1 = new Thread(new LoggerTask("System startup"));
Thread logger2 = new Thread(new LoggerTask("Loading data"));
Thread logger3 = new Thread(new LoggerTask("Processing request"));
logger1.start();
logger2.start();
logger3.start();
System.out.println("The main thread is finishing.");
}
}
What’s happening?
Three threads write their messages to the console in parallel. The order of messages is not guaranteed—that’s the nature of multithreading.
7. Useful details
Table: comparing ways to create threads
| Approach | Flexibility | Reusability | Recommended? |
|---|---|---|---|
| Extending Thread | Low | No | Only for simple/educational examples |
| Implementing Runnable | High | Yes | Yes |
| Anonymous class | Medium | No | For one-off tasks |
| Lambda expression | High | No | Yes (Java 8+) |
Diagram: how starting a thread works
+----------------------+
| main (main thread) |
+----------------------+
|
v
+----------------------+
| Thread thread = ... |
+----------------------+
|
v
+----------------------+
| thread.start() | ← The thread "wakes up"
+----------------------+
|
v
+----------------------+
| run() in a new thread|
+----------------------+
|
v
+----------------------+
| Thread terminated |
+----------------------+
8. Typical mistakes when starting threads
Error #1: Calling run() instead of start().
A very common beginner mistake is to call the run() method on a thread object directly. In that case, no new thread is created—the method simply executes in the current (main) thread. The correct approach: always use start() to start a thread.
Error #2: Starting a thread twice.
You cannot call start() on the same Thread instance more than once—this will throw IllegalThreadStateException. If you need to run a task again, create a new Thread instance.
Error #3: Mutating shared variables without synchronization.
If multiple threads access the same variables, the result can be unexpected (a race condition). We’ll talk about synchronization later, but for now—be careful.
Error #4: Not accounting for thread completion.
If it’s important to wait for a thread to finish, use the join() method. Otherwise, the main thread may finish before the child threads.
Error #5: Confusing the thread name with the class name.
The getName() method returns the thread’s name, not the class name. For debugging, it’s often helpful to set thread names explicitly via the constructor or setName().
GO TO FULL VERSION