How to Create Threads in Java: Thread Class, Runnable and start() vs run()

0

Creating Threads in Java

Once you understand what a thread is, the next question is simple: how do we create one? Java provides several ways to create and execute concurrent tasks, but two classic approaches are especially important for understanding the foundation of multithreading: extending the Thread class and implementing the Runnable interface.

Modern Java applications often use higher-level concurrency tools such as executors, but these fundamental techniques remain important because they explain what is actually happening underneath those abstractions.

Important: Creating a Thread object does not start a new thread. The new execution path begins when you call start().

Why Create Multiple Threads?

Suppose an application needs to perform two independent tasks. A single thread would normally execute them one after another. If the first task takes a long time, the second task must wait.

public class SequentialTasks {

    public static void main(String[] args) {

        performTask("Download file");
        performTask("Generate report");
    }

    static void performTask(String task) {

        System.out.println(task + " started");

        for (int i = 1; i <= 3; i++) {
            System.out.println(task + " - step " + i);
        }

        System.out.println(task + " completed");
    }
}

Here, the report task cannot begin until the download task finishes. If these tasks are independent, we may be able to execute them concurrently using separate threads.

Approach 1: Extending the Thread Class

One way to create a thread is to create a class that extends Thread and override its run() method.

class MyThread extends Thread {

    @Override
    public void run() {

        System.out.println("Worker thread is running");

        for (int i = 1; i <= 3; i++) {
            System.out.println("Step: " + i);
        }
    }
}

public class ThreadCreation {

    public static void main(String[] args) {

        MyThread thread = new MyThread();

        thread.start();

        System.out.println("Main thread is running");
    }
}

The run() method contains the work that the new thread should perform. Calling start() causes the JVM to arrange for that run() method to execute on the newly started thread.

Remember: Think of run() as the task and start() as the operation that begins a new thread to execute that task.

Understanding start() vs run()

This is one of the most important details in Java multithreading.

Calling start() creates a new execution path. Calling run() directly does not.

class Worker extends Thread {

    @Override
    public void run() {

        System.out.println(
            "Running on: "
            + Thread.currentThread().getName()
        );
    }
}

public class StartVsRun {

    public static void main(String[] args) {

        Worker worker = new Worker();

        worker.start();
    }
}

When start() is used, the output will normally show a thread name different from main, because the run() method is executing on the newly created thread.

Now consider this:

Worker worker = new Worker();

worker.run();

This does not create a new thread. The run() method executes normally on the current thread, which in this example is the main thread.

Interview tip: If an interviewer asks whether calling run() creates a new thread, the answer is no. Only start() initiates a new thread of execution.

Approach 2: Implementing Runnable

The second classic approach is to implement the Runnable interface.

class MyTask implements Runnable {

    @Override
    public void run() {

        System.out.println("Task is running");

        for (int i = 1; i <= 3; i++) {
            System.out.println("Step: " + i);
        }
    }
}

public class RunnableExample {

    public static void main(String[] args) {

        MyTask task = new MyTask();

        Thread thread = new Thread(task);

        thread.start();
    }
}

Here, MyTask represents the work that needs to be performed, while the Thread object represents the thread that executes that work.

This separation is valuable because the task and the mechanism used to execute it are treated as separate responsibilities.

Thread and Runnable Have Different Responsibilities

Component Responsibility
Runnable Represents the task or work that should be performed.
Thread Represents the thread that can execute the task.
run() Contains the task's executable logic.
start() Starts a new thread and eventually causes run() to execute on it.

Creating a Thread with a Lambda Expression

Because Runnable is a functional interface, Java allows us to represent its implementation using a lambda expression.

public class LambdaThread {

    public static void main(String[] args) {

        Runnable task = () -> {

            System.out.println("Task is running");

            for (int i = 1; i <= 3; i++) {
                System.out.println("Step: " + i);
            }
        };

        Thread thread = new Thread(task);

        thread.start();
    }
}

This version is shorter because the lambda expression provides the implementation of the run() method automatically.

Passing a Lambda Directly to Thread

When the task is small, the code can be made even more concise.

public class SimpleThread {

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {

            System.out.println("Background task running");

        });

        thread.start();
    }
}

This style is common for small tasks where creating a separate class would add unnecessary ceremony.

Creating Multiple Threads

We can create multiple thread objects when several independent tasks need to run concurrently.

public class MultipleThreads {

    public static void main(String[] args) {

        Thread first = new Thread(() -> {

            for (int i = 1; i <= 3; i++) {
                System.out.println("First: " + i);
            }
        });

        Thread second = new Thread(() -> {

            for (int i = 1; i <= 3; i++) {
                System.out.println("Second: " + i);
            }
        });

        first.start();
        second.start();
    }
}

Both threads become eligible for execution after their respective start() calls. The exact order of their output should not be assumed.

Naming Threads

Meaningful thread names can make debugging and production troubleshooting much easier.

public class NamedThread {

    public static void main(String[] args) {

        Thread thread = new Thread(
            () -> {
                System.out.println(
                    "Executing background task"
                );
            },
            "File-Download-Thread"
        );

        thread.start();
    }
}

Instead of seeing a generic thread name in logs or debugging tools, developers can immediately understand what the thread is responsible for.

Industry practice: Descriptive thread names are surprisingly useful when diagnosing concurrency problems in production systems. A name such as Payment-Processor is much more informative than an automatically generated name.

Thread Creation Using Thread Subclass vs Runnable

Approach How It Works Major Consideration
Extends Thread Create a class that extends Thread and override run(). The class uses its single class inheritance relationship for Thread.
Implements Runnable Create a task that implements Runnable and pass it to Thread. Separates the task from the thread executing it.
Lambda Runnable Provide the Runnable task using a lambda expression. Convenient for short tasks.

Why Runnable Is Often Preferred

The Runnable approach provides better separation between what should be done and how it should be executed.

Java supports single class inheritance. If a class extends Thread, it cannot extend another class. With Runnable, the class can still extend another class while also defining a task that can be executed by a thread.

class EmployeeService extends SomeBaseService
        implements Runnable {

    @Override
    public void run() {

        System.out.println(
            "Employee task is running"
        );
    }
}

This design keeps inheritance available for the actual domain relationship while using Runnable for executable behavior.

A Critical Rule: A Thread Cannot Be Started Twice

A particular Thread object can be started only once. Attempting to invoke start() on the same Thread object again results in IllegalThreadStateException.

Thread thread = new Thread(() -> {
    System.out.println("Task running");
});

thread.start();

// Do not do this:
// thread.start();

If the same work needs to be executed again, create another Thread object or, in production applications, use an appropriate executor-based design.

Common Beginner Mistakes

  • Calling run() instead of start() when a new thread is required.
  • Assuming that creating a Thread object automatically starts it.
  • Starting the same Thread object more than once.
  • Assuming thread execution order from the order of start() calls.
  • Creating unnecessary threads for every small operation.
  • Ignoring meaningful thread names when building systems that require production debugging.

Best Practices

  • Use Runnable when you want to separate a task from the thread that executes it.
  • Use lambda expressions for short and simple Runnable tasks.
  • Give important application threads descriptive names.
  • Never rely on a particular scheduling order unless the program explicitly establishes that ordering.
  • Avoid manually creating large numbers of threads in production systems; prefer managed concurrency mechanisms such as executor services when appropriate.

Interview Insights

Question: What are the common ways to create a thread in Java?

A strong answer is: A thread can traditionally be created by extending the Thread class and overriding run(), or by implementing Runnable and passing the task to a Thread. Runnable is generally more flexible because it separates the task from the execution mechanism.

Question: What happens when run() is called directly?

The method executes normally on the current thread. No new thread is created merely by calling run().

Question: Can the same Thread object be started twice?

No. Once a Thread has been started, attempting to start the same Thread object again results in IllegalThreadStateException.

Quick Revision

Topic Key Point
Thread Represents an execution path that can execute a task.
run() Contains the work performed by the thread.
start() Starts a new thread of execution.
Thread inheritance Create a class by extending Thread and override run().
Runnable Represents a task separately from the Thread object.
Lambda Provides a concise way to implement Runnable.
Multiple starts The same Thread object cannot be started more than once.
Thread naming Descriptive names improve debugging and observability.

Creating threads is the first practical step into Java multithreading. The essential distinction is simple but extremely important: Runnable defines the work, while Thread provides a mechanism for executing that work concurrently. Once this distinction is clear, the next topic—Runnable Interface—becomes much easier to understand and apply in real applications.

Post a Comment

0Comments
Post a Comment (0)