Creating Threads in Java: Thread Class vs Runnable Interface

Illustration for Creating Threads in Java: Thread Class vs Runnable Interface
By Last updated:

Multithreading allows Java programs to perform multiple operations concurrently, maximizing CPU utilization and improving responsiveness. Two foundational ways to create threads in Java are by extending the Thread class or implementing the Runnable interface.

Understanding the differences between these approaches is crucial for designing maintainable, scalable concurrent applications.


Thread Class vs Runnable Interface: The Basics

Extending Thread

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread using Thread class");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

Implementing Runnable

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread using Runnable interface");
    }
}

public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

Core Concept: What Is Multithreading?

Multithreading is the ability of a CPU to execute multiple threads concurrently. It enhances:

  • Responsiveness in GUIs
  • Throughput in server applications
  • Resource utilization in compute-intensive apps

Thread Lifecycle in Java

NEW → RUNNABLE → BLOCKED/WAITING/TIMED_WAITING → TERMINATED

Lifecycle Methods

  • start() – begins execution of the thread
  • run() – contains the logic
  • join() – wait for thread to finish
  • sleep() – pause current thread
  • interrupt() – signal interruption

Java Memory Model & Thread Visibility

  • volatile ensures visibility of updates to variables
  • synchronized ensures atomicity and visibility
  • Avoid stale data by understanding how threads interact through the heap

Coordination and Communication

  • wait(), notify(), notifyAll() – used for condition-based coordination
  • join() – synchronize execution
  • sleep() – used for delays, not communication

Synchronization Tools

  • synchronized blocks/methods
  • ReentrantLock – flexible and interruptible
  • ReadWriteLock – improves concurrency for reads
  • StampedLock – supports optimistic reads

High-Level Concurrency Tools

  • ExecutorService, ThreadPoolExecutor
  • ForkJoinPool, RecursiveTask
  • Future, CompletableFuture
  • BlockingQueue, ConcurrentHashMap

Real-World Examples

Producer-Consumer

BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);

Thread Pool

ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> System.out.println("Task"));
executor.shutdown();

Parallel File Processing

Use thread pools to assign parts of file processing to multiple threads.


Pros and Cons

Feature Thread Class Runnable Interface
Inheritance Restricts single inheritance Allows flexibility
Code Sharing Less reusable More reusable and flexible
Separation Tightly couples thread and logic Clean separation
Preferred Use For simple use cases For most real-world scenarios

Java Version Tracker

📌 What's New in Java Versions?

Java 8

  • Lambda support for Runnable
  • CompletableFuture
  • Parallel Streams

Java 9

  • Flow API for reactive streams

Java 11+

  • Small enhancements in CompletableFuture

Java 21

  • Virtual Threads
  • Structured Concurrency
  • Scoped Values

Best Practices

  • Prefer Runnable over extending Thread
  • Use thread pools via Executors
  • Avoid shared mutable state
  • Use higher-level APIs for complex tasks

Common Pitfalls

  • Calling run() instead of start()
  • Race conditions
  • Deadlocks from nested locks
  • Resource leaks from unclosed executors

Multithreading Design Patterns

  • Worker Thread Pattern
  • Future Task Pattern
  • Thread-per-Message
  • Producer-Consumer Pattern

Conclusion and Key Takeaways

  • Use Runnable for flexibility and reusability
  • Extend Thread only for quick prototypes or tiny apps
  • Always prefer high-level concurrency APIs for thread safety and scalability
  • Understand the underlying lifecycle and memory implications

FAQs

Q1: Why is Runnable preferred over Thread?
A: Runnable decouples task logic from threading mechanism, allowing reuse and better design.

Q2: What happens if I call run() instead of start()?
A: The thread runs in the current thread and doesn’t spawn a new one.

Q3: Can I pass parameters to threads?
A: Yes, via constructors or shared variables (with care).

Q4: Is synchronized enough for all scenarios?
A: Not always. Use locks for finer control and features like timeout, fairness.

Q5: What's the benefit of ExecutorService?
A: Manages a pool of threads efficiently, avoids overhead of thread creation.

Q6: Can I stop a thread externally?
A: Not directly. Use interruption via interrupt() and check isInterrupted().

Q7: What's the difference between Callable and Runnable?
A: Callable can return results and throw exceptions.

Q8: How are virtual threads different?
A: Virtual threads (Java 21) are lightweight, scalable threads managed by JVM.

Q9: What is structured concurrency?
A: A model to manage thread lifecycles hierarchically for better error handling.

Q10: Can I combine Runnable with Thread subclassing?
A: Technically yes, but it's discouraged due to poor separation of concerns.