Java tutorials > Multithreading and Concurrency > Threads and Synchronization > How to create threads (Thread vs Runnable)?

How to create threads (Thread vs Runnable)?

In Java, multithreading allows concurrent execution of multiple parts of a program. Creating threads is a fundamental aspect of utilizing multithreading capabilities. There are two primary ways to create threads in Java: by extending the Thread class or by implementing the Runnable interface. Each approach has its own advantages and disadvantages. This tutorial will walk you through both methods with clear examples.

Creating Threads by Extending the Thread Class

This method involves creating a new class that inherits from the Thread class. You then override the run() method to define the task that the thread will execute. The start() method is crucial; it initiates the thread and calls the run() method in a new thread of execution. Calling run() directly simply executes the code within the current thread, defeating the purpose of multithreading. Using Thread.currentThread().getName() provides insight into which thread is currently running.

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        thread1.start(); // Starts the thread and calls the run() method

        MyThread thread2 = new MyThread();
        thread2.start();
    }
}

Creating Threads by Implementing the Runnable Interface

This approach involves implementing the Runnable interface. You create a class that implements Runnable and provides a run() method defining the task for the thread. Then, you create a new Thread object, passing your Runnable implementation as an argument to the Thread constructor. Finally, you call start() on the Thread object to begin execution in a separate thread. This approach promotes better separation of concerns and allows your class to inherit from another class if needed.

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread1 = new Thread(myRunnable);
        thread1.start();

        Thread thread2 = new Thread(myRunnable);
        thread2.start();
    }
}

Concepts Behind the Snippets

Extending Thread: This approach directly couples the thread's behavior with the class definition. It's straightforward but less flexible as Java doesn't allow multiple inheritance. Your class becomes a specialized type of Thread.

Implementing Runnable: This is more flexible. You define the task to be executed in the run() method, and then create a Thread object to execute that task. This decoupling is beneficial because your class can extend another class, adhering to the principle of single responsibility and facilitating better code organization.

Real-Life Use Case

Imagine a server application handling multiple client requests concurrently. Each request can be processed in a separate thread. Using Runnable is preferred in such scenarios because the request handler class might need to extend another class related to networking or data processing. For example, a web server might use a thread pool where each thread executes a Runnable task representing a client request.

Best Practices

  • Prefer Runnable: In most cases, implementing Runnable is the preferred approach because it provides greater flexibility and promotes better code organization.
  • Avoid direct manipulation of threads: Whenever possible, use higher-level concurrency utilities like ExecutorService, which manages thread pools and simplifies thread management.
  • Handle exceptions carefully: Ensure that exceptions within the run() method are properly handled to prevent thread termination and potential data corruption.
  • Use appropriate synchronization mechanisms: When multiple threads access shared resources, use synchronization mechanisms like synchronized blocks or Lock objects to prevent race conditions and ensure data consistency.

Interview Tip

Be prepared to discuss the differences between extending Thread and implementing Runnable, including the benefits of each approach. Also, be ready to explain scenarios where one approach is more appropriate than the other. Understanding thread lifecycle and synchronization is crucial.

When to use them

  • Extend Thread: Use this when you are creating a very specialized thread type and inheritance from Thread does not conflict with other design requirements. It is more straightforward for simple cases.
  • Implement Runnable: Use this in most cases, especially when your class already needs to inherit from another class. It provides better separation of concerns and allows for more flexible design. It allows you to pass the runnable task to different types of thread executors (e.g., fixed thread pool, cached thread pool).

Memory footprint

The memory footprint of a thread depends on various factors like the stack size and the JVM implementation. There's no significant difference in memory footprint between a thread created by extending Thread and a thread created using Runnable. The memory used by the Thread object itself is relatively small.

Alternatives

Alternatives to creating threads directly include using ExecutorService for managing thread pools, using ForkJoinPool for divide-and-conquer algorithms, and using reactive programming libraries like RxJava or Project Reactor for asynchronous and event-driven programming.

Pros and Cons of Extending Thread

Pros:

  • Simple and straightforward for basic threading scenarios.
  • Less verbose code for simple tasks.

Cons:

  • Limits inheritance capabilities, as Java only allows single inheritance.
  • Tightly couples the task to the thread, reducing flexibility.

Pros and Cons of Implementing Runnable

Pros:

  • More flexible, as the class can inherit from another class.
  • Promotes separation of concerns between the task and the thread.
  • Allows for better code organization and reusability.

Cons:

  • Slightly more verbose compared to extending Thread.

FAQ

  • What happens if I call the run() method directly instead of start()?

    Calling the run() method directly will execute the code within the current thread, not in a new thread. It will behave like a regular method call, defeating the purpose of multithreading.
  • Can I reuse a Runnable object with multiple threads?

    Yes, you can create multiple Thread objects using the same Runnable instance. Each Thread will execute the run() method of the Runnable object concurrently.
  • What is the difference between Thread.sleep() and Thread.yield()?

    Thread.sleep() pauses the execution of the current thread for a specified amount of time. The thread goes into a waiting state. Thread.yield() hints to the scheduler that the current thread is willing to relinquish its use of the CPU. The scheduler is free to ignore this hint.