You are currently viewing How to Create a Thread in Java

How to Create a Thread in Java

This entry is part 1 of 1 in the series Concurrency (Threads)
  • How to Create a Thread in Java

Introduction

If you have been writing Java for a while, you have almost certainly used threads — even if you did not create them directly. Threads are how Java runs tasks concurrently, and knowing how to create them is one of the first things you need to understand when working with concurrent code.

Java has supported threads since version 1.0, and the platform has added better ways to work with them ever since. This article walks through the three core approaches: extending the Thread class, implementing Runnable, and using lambda expressions (Java 8+). It also covers the most important best practices to keep in mind as soon as you start writing concurrent code.

Higher-level tools — such as ExecutorService, Callable, Future, and the thread lifecycle — are each covered in dedicated follow-up articles in this series. This one focuses on the fundamentals, which are a foundation you will rely on no matter which path you take next.

1. Understanding Threads and Concurrency in Java

Before examining specific thread-creation techniques, it is essential to establish a clear conceptual foundation. A thread is the smallest unit of execution within a process. In Java, the JVM enables an application to run multiple threads concurrently within the same process, with each thread executing its own sequence of instructions while sharing the process’s memory space.

Concurrency refers to the ability to manage multiple tasks that overlap in time. Importantly, concurrency does not necessarily imply true parallelism. On a single-core CPU, the JVM time-slices threads cooperatively, giving each thread the illusion of simultaneous execution. On a multi-core CPU, however, multiple threads genuinely execute at the same time, each occupying its own physical core.

This reality makes understanding thread creation a critical engineering competency. Java provides two primary low-level abstractions for concurrency:

  • java.lang.Thread — the class that represents a thread of execution
  • java.lang.Runnable — the functional interface whose run() method contains the thread’s task

2. Approach 1: Extending the Thread Class

The simplest and most historically familiar technique for creating a thread in Java is to extend java.lang.Thread and override its run() method. The run() method defines the work that the new thread will perform. To launch the thread, you must call start() on the instance — never run() directly.

“Calling run() directly does not start a new thread of execution; it simply executes the method in the calling thread, just as any ordinary method call would.”

This distinction is one of the most common sources of confusion for developers new to Java concurrency. Therefore, always remember that start() instructs the JVM to create a new thread and schedule it for execution; the JVM subsequently invokes run() within that new thread.

The following code snippet demonstrates how to create a thread by extending Thread. Notice that the custom class overrides run() to define its task, and that the caller invokes start() to launch the thread, followed by join() to wait for its completion:

// Approach 1: Extend the Thread class and override run()
class MyThread extends Thread {

    @Override
    public void run() {
        // This code runs inside the new thread, not the caller's thread
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }
}

// --- Usage ---
MyThread thread = new MyThread();
thread.setName("extend-thread-demo");
thread.start(); // start() spawns a new thread and eventually calls run()
thread.join();  // Block the main thread until the new thread finishes

While this approach works correctly, it carries a significant architectural limitation. Because Java does not support multiple class inheritance, a class that extends Thread cannot extend any other superclass. As a result, this technique restricts code flexibility and tightly couples the task definition with the execution mechanism, thereby violating the single-responsibility principle. For these reasons, extending Thread is generally discouraged in modern Java development.

3. Approach 2: Implementing the Runnable Interface

A more flexible and widely preferred alternative is to implement the java.lang.Runnable functional interface. Runnable declares a single abstract method, run(). You pass an instance of your Runnable implementation to a Thread constructor, thereby decoupling the task from the execution mechanism.

This separation of concerns produces several important benefits. First, your class remains free to extend another superclass. Second, the same Runnable object can be submitted to different threads without modification. Third, this design aligns with the composition-over-inheritance principle that underpins modern object-oriented design. Consequently, virtually every authoritative Java resource — including the Oracle documentation and Java Concurrency in Practice — recommends Runnable over Thread inheritance.

The following snippet shows a named Runnable implementation and its usage:

// Approach 2: Implement the Runnable interface
class MyRunnable implements Runnable {

    @Override
    public void run() {
        // The task is completely independent of thread-management concerns
        System.out.println("Runnable running: " + Thread.currentThread().getName());
    }
}

// --- Usage ---
// Wrap the Runnable in a Thread; the task and the carrier are separate objects
Thread thread = new Thread(new MyRunnable());
thread.setName("runnable-thread-demo");
thread.start();
thread.join();

Furthermore, this approach facilitates unit testing: you can invoke myRunnable.run() directly in a test without spinning up a real thread. Additionally, the Runnable instance is reusable — you can pass the same object to multiple Thread constructors or to any higher-level executor without modification.

4. Approach 3: Using Anonymous Classes and Lambda Expressions

For scenarios where a separate named class is unnecessary, Java offers two additional syntactic forms: anonymous inner classes and — since Java 8 — lambda expressions. Both forms define the Runnable implementation inline, thereby reducing boilerplate code considerably and keeping related logic close to its point of use.

4.1 Anonymous Inner Class

Prior to Java 8, developers frequently used anonymous inner classes to create short-lived threads without the ceremony of a separate named class. Although this technique is now largely superseded by lambda expressions, you will still encounter it in older codebases. The following snippet illustrates the pattern:

// Approach 3a: Anonymous inner class (common in pre-Java-8 code)
Thread anonymousThread = new Thread(new Runnable() {
    @Override
    public void run() {
        // Inline task defined without a named class
        System.out.println("Anonymous class thread: " + Thread.currentThread().getName());
    }
});
anonymousThread.setName("anonymous-thread-demo");
anonymousThread.start();
anonymousThread.join();

4.2 Lambda Expression (Java 8+)

With the introduction of lambda expressions in Java 8, the same thread creation becomes dramatically more concise. Because Runnable is a functional interface, any expression matching the () -> void signature serves as a valid Runnable implementation. Consider the equivalent code using a lambda:

// Approach 3b: Lambda expression — concise, simple, preferred form (Java 8+)
Thread lambdaThread = new Thread(
    () -> System.out.println("Lambda thread: " + Thread.currentThread().getName())
);
lambdaThread.setName("lambda-thread-demo");
lambdaThread.start();
lambdaThread.join();

Your take-away advice:

“Prefer lambdas to anonymous classes.”

This advice applies directly to thread creation. Lambda expressions not only reduce visual noise; they also encourage a functional style that sharpens the distinction between the what (the task) and the how (the threading mechanism). Moreover, modern IDEs provide better refactoring support for lambdas than for anonymous classes, making lambda-based thread creation the standard choice in modern Java.

5. Best Practices for Creating Threads in Java

Having explored the three core thread-creation approaches, it is valuable to consolidate the most important guidelines for professional Java development. Applying these practices consistently will result in code that is safer, more debuggable, and considerably more maintainable.

5.1 Prefer Runnable (or Lambda) over Extending Thread

As demonstrated in Sections 3 and 4, extending Thread unnecessarily restricts your class hierarchy and couples task logic to execution mechanics. Implementing Runnable — or, better still, using a lambda expression — keeps your design composable and testable. This single change eliminates the most common structural mistake developers make when first learning Java concurrency.

5.2 Handle InterruptedException Correctly

Never silently discard InterruptedException. Either re-throw it as a checked exception, or restore the interrupt status by calling Thread.currentThread().interrupt(). Swallowing the exception prevents cooperative cancellation signals from propagating to the thread’s callers — a subtle bug that is notoriously difficult to diagnose in production.

5.3 Name Your Threads Meaningfully

Assigning descriptive names to threads is a simple yet impactful practice. Thread names appear in stack traces, thread dumps, and profiler reports. Without them, diagnosing a problem across dozens of anonymous threads — named Thread-0, Thread-1, and so on — becomes unnecessarily difficult. Therefore, always pass a name to the Thread constructor or to thread.setName() before calling start().

5.4 Minimize Shared Mutable State

The root cause of most threading bugs — race conditions, data corruption, and visibility failures — is concurrent access to shared mutable state. Therefore, prefer immutable objects and thread-safe data structures wherever possible. When shared mutable state is unavoidable, apply proper synchronization.

The following snippet brings together best practices 5.2 and 5.3 in a single, idiomatic example. Notice how InterruptedException is handled without silently discarding it, and how the thread carries a descriptive name that would surface immediately in any stack trace or profiler:

// Best practices: descriptive thread name + correct InterruptedException handling
Thread namedThread = new Thread(() -> {
    try {
        Thread.sleep(50); // Simulate work that may be interrupted
        System.out.println("Named thread running: " + Thread.currentThread().getName());
    } catch (InterruptedException e) {
        // Re-establish the interrupt status so callers can observe it
        Thread.currentThread().interrupt();
        System.out.println("Thread was interrupted: " + Thread.currentThread().getName());
    }
}, "worker-thread-1"); // Descriptive name — visible in stack traces and profilers

namedThread.start();
namedThread.join();

Conclusion

Java offers a clear, progressively expressive set of mechanisms for creating threads. Starting from the foundational technique of extending Thread, through the more flexible Runnable interface, and culminating in the concise lambda-expression syntax introduced in Java 8, each approach reflects a step forward in the evolution of Java’s concurrency model. Furthermore, the four best practices examined in Section 5 — preferring Runnable over inheritance, handling InterruptedException responsibly, naming threads meaningfully, and minimizing shared mutable state — apply universally, regardless of which higher-level framework you ultimately adopt.

The techniques presented here form the essential groundwork upon which all advanced Java concurrency is built. Consequently, understanding them thoroughly enables you to reason clearly about any concurrent code you read or write.

You can find the complete code of this article on GitHub

Noel Kamphoa

Senior Software Engineer and Tech Lead with 14+ years of professional experience in backend development, system design, and enterprise software. Passionate about clean architecture, scalable Java applications, and helping developers grow through structured learning and real-world practice.