Java Thread Class: Creating and Managing Threads
java thread class: Learn how to create and manage threads with the Java Thread class, including lifecycle, synchronization, and common pitfalls.
When you need to run code concurrently in Java, the java thread class is the core API for creating and managing threads. A thread is a lightweight unit of execution that runs independently of the main program flow. The Thread class provides constructors, lifecycle methods, and static utilities that let you start, pause, interrupt, and coordinate threads.
Creating a Thread by Extending Thread
The simplest way to create a thread is to subclass Thread and override its run() method. The run() method contains the code that executes when the thread starts.
class Worker extends Thread { @Override public void run() { System.out.println("Running in thread: " + getName()); } } Worker worker = new Worker(); worker.start();
Calling start() schedules the thread for execution and returns immediately. The run() method executes asynchronously on the new thread. Do not call run() directly; that would execute the code on the current thread and defeat the purpose of concurrency.
Extending Thread is straightforward, but it couples your class to a specific superclass. If your worker also needs to inherit from another class, you cannot use this approach. That is why the Runnable interface is often preferred.
Creating a Thread with Runnable
The Runnable interface separates the task from the thread that runs it. You implement Runnable and pass it to a Thread constructor.
class Task implements Runnable { @Override public void run() { System.out.println("Task executed by " + Thread.currentThread().getName()); } } Thread thread = new Thread(new Task()); thread.start();
This approach keeps your task class free from thread-specific code. It also allows the task to be reused with thread pools or other executors. The Thread class itself implements Runnable, so the two patterns are functionally similar, but Runnable is more flexible.
Thread Lifecycle and State Transitions
A thread moves through several states during its lifetime. The Thread.State enum defines these states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED.
When you create a Thread object, it is in the NEW state. Calling start() moves it to RUNNABLE, meaning it is eligible for execution. The thread may enter BLOCKED when it tries to acquire a lock that another thread holds. It enters WAITING when it calls Object.wait() or Thread.join() without a timeout. TIMED_WAITING occurs with methods like sleep() or join(long). Finally, when run() returns, the thread enters TERMINATED.
You can inspect the current state with getState(), which is useful for debugging but not for coordinating threads. State transitions are controlled by the JVM and the operating system scheduler, so you cannot directly force a thread into a particular state.
Controlling Thread Execution with join and interrupt
The join() method lets one thread wait for another to finish. This is useful when the main thread needs the result of a worker thread.
Thread worker = new Thread(() -> { // simulate work try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); worker.start(); worker.join(); // main thread waits until worker finishes
join() throws InterruptedException, which you must handle. If the calling thread is interrupted while waiting, the method throws and the interrupted status is cleared. Re-interrupting the thread is a common pattern to preserve the interruption signal.
The interrupt() method sets the thread's interrupted status. It does not forcibly stop the thread. Instead, it cooperatively signals the thread to stop. Methods like sleep(), wait(), and join() respond to interruption by throwing InterruptedException. Your code should check the interrupted status periodically if the thread performs long-running non-blocking work.
Synchronizing Access to Shared State
When multiple threads access shared mutable data, you must coordinate access to avoid race conditions. The synchronized keyword is the simplest tool for this. It ensures that only one thread can execute a block or method at a time.
class Counter { private int count = 0; public synchronized void increment() { count++; } public synchronized int getCount() { return count; } }
Synchronizing on the object's monitor prevents two threads from executing increment() concurrently. This guarantees visibility and atomicity for the operation. However, excessive synchronization can reduce concurrency. For simple counters, you might prefer AtomicInteger, which provides thread-safe operations without explicit locks.
Common Pitfalls and How to Avoid Them
One common mistake is calling run() instead of start(). This executes the task on the calling thread and does not create a new thread. Another is ignoring InterruptedException by swallowing it. This can leave the thread in an inconsistent state and prevent proper shutdown.
Deadlock is another risk. When two threads each hold a lock and wait for the other's lock, neither can proceed. To avoid deadlock, acquire locks in a consistent order and use timeouts where possible.
Thread leaks occur when threads are created without bound. Each thread consumes memory and system resources. In long-running applications, prefer a thread pool from ExecutorService to reuse threads and control the maximum number of concurrent tasks.
Choosing Between Thread and ExecutorService
The Thread class gives you direct control over individual threads. It is appropriate for simple cases where you need one or a few threads and you manage their lifecycle manually. For most production applications, ExecutorService is a better choice. It decouples task submission from thread management, provides a pool of reusable threads, and offers methods for shutdown and task tracking.
ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> System.out.println("Task in pool")); executor.shutdown();
Using ExecutorService reduces the risk of thread leaks and makes it easier to scale the number of threads. It also supports Future objects for retrieving results and handling exceptions. The Thread class remains useful for low-level control, such as setting thread priorities or names, but the executor abstraction is generally safer and more maintainable.