Skip to content

Threads in Java

Threads are the smallest unit of a CPU’s execution, representing an independent path of execution within a program. In Java, a thread is a lightweight process that shares the same memory space as other threads in the same program. Multithreading in Java allows multiple threads to run concurrently, enabling efficient CPU utilization and creating responsive applications.


Key Concepts of Threads in Java

  1. What is a Thread?
    1. A thread is a single sequence of execution in a program. It has its own program counter, stack, and local variables, but it shares memory space with other threads.
    1. Threads allow a Java program to perform two or more tasks simultaneously, making it possible to build applications that are more responsive and efficient.
  2. Thread Class and Runnable Interface
    1. Java provides two main ways to create and manage threads:
      1. Extending the Thread Class: You can create a thread by creating a class that extends Thread and overriding its run() method.
      1. Implementing the Runnable Interface: You can implement the Runnable interface and pass an instance of it to a Thread object.

Creating Threads

1. Extending the Thread Class

  • Create a class that extends the Thread class and override the run() method to define the code that should be executed by the thread.
  • Call start() to begin the thread’s execution.

Example:

class MyThread extends Thread {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(“Thread running: ” + i);

        }

    }

}

public class ThreadExample {

    public static void main(String[] args) {

        MyThread thread = new MyThread();

        thread.start(); // Start the thread, which will invoke the run method in a new thread

    }

}

2. Implementing the Runnable Interface

  • Create a class that implements the Runnable interface and override its run() method. Pass the Runnable instance to a Thread object.

Example:

class MyRunnable implements Runnable {

    public void run() {

        for (int i = 0; i < 5; i++) {

            System.out.println(“Runnable thread running: ” + i);

        }

    }

}

public class RunnableExample {

    public static void main(String[] args) {

        MyRunnable myRunnable = new MyRunnable();

        Thread thread = new Thread(myRunnable);

        thread.start(); // Start the thread, which will invoke the run method in a new thread

    }

}

Difference Between Extending Thread and Implementing Runnable:

  • Extending Thread limits your class to inherit from Thread, preventing it from inheriting from other classes (Java supports single inheritance).
  • Implementing Runnable allows a class to extend another class and still create a thread.

Thread Lifecycle

A thread in Java goes through several states during its lifecycle:

  1. New: A thread is in the “new” state when it is created but not yet started.
  2. Runnable: The thread is ready to run and waiting for CPU scheduling.
  3. Blocked: The thread is waiting for a resource that is currently unavailable.
  4. Waiting: The thread is waiting indefinitely for another thread to perform a particular action.
  5. Timed Waiting: The thread is waiting for a specified period (e.g., sleep(), join(), wait()).
  6. Terminated: The thread has completed its execution or has been terminated.

Example of Thread States:

public class ThreadLifecycleExample {

    public static void main(String[] args) {

        Thread thread = new Thread(() -> {

            System.out.println(“Thread started…”);

            try {

                Thread.sleep(2000); // Thread will sleep for 2 seconds

            } catch (InterruptedException e) {

                System.out.println(“Thread interrupted”);

            }

            System.out.println(“Thread completed.”);

        });

        System.out.println(“Thread state before start: ” + thread.getState());

        thread.start(); // Transition from New to Runnable

        System.out.println(“Thread state after start: ” + thread.getState());

        try {

            thread.join(); // Wait for the thread to complete

        } catch (InterruptedException e) {

            System.out.println(“Main thread interrupted”);

        }

        System.out.println(“Thread state after completion: ” + thread.getState()); // Terminated

    }

}


Thread Methods

  • start(): Starts a new thread and calls the run() method.
  • run(): Defines the code that the thread will execute. This method should not be called directly; it is invoked by the start() method.
  • sleep(milliseconds): Causes the current thread to pause for the specified number of milliseconds.
  • join(): Waits for the thread to finish execution before continuing with the rest of the code.
  • interrupt(): Interrupts a thread that is in a blocked or waiting state.
  • isAlive(): Checks if the thread is still running.
  • getState(): Returns the current state of the thread.

Example:

Thread thread = new Thread(() -> {

    for (int i = 0; i < 5; i++) {

        System.out.println(“Thread running: ” + i);

    }

});

thread.start();

System.out.println(“Thread state after start: ” + thread.getState());

try {

    thread.join(); // Waits for thread to complete

} catch (InterruptedException e) {

    System.out.println(“Main thread interrupted”);

}

System.out.println(“Thread state after completion: ” + thread.getState());


Thread Synchronization

When multiple threads access shared resources, there can be data inconsistency due to concurrent modifications. Synchronization ensures that only one thread can access a critical section at a time.

  1. Synchronized Methods:
    1. Use the synchronized keyword to prevent more than one thread from executing the method simultaneously.

Example:

public synchronized void synchronizedMethod() {

    // Critical section code

}

  • Synchronized Blocks:
    • Synchronize a specific part of code to reduce the scope of synchronization and increase performance.

Example:

public void someMethod() {

    synchronized (this) {

        // Critical section code

    }

}

  • Reentrant Locks:
    • Use classes from java.util.concurrent.locks package, such as ReentrantLock, for more control over synchronization.

Example:

Lock lock = new ReentrantLock();

lock.lock();

try {

    // Critical section code

} finally {

    lock.unlock();

}


Thread Safety and Best Practices

  1. Immutable Objects: Make objects immutable to avoid concurrent modifications.
  2. ThreadLocal Variables: Use ThreadLocal to create variables that are local to the thread, preventing shared access.
  3. Use High-Level Concurrency Utilities: Use classes from the java.util.concurrent package like ExecutorService, Semaphore, and ConcurrentHashMap for better concurrency control.
  4. Avoid Deadlocks: Ensure that multiple threads do not block each other indefinitely by managing lock orders and using timeout mechanisms.

Conclusion Threads in Java provide a powerful way to achieve concurrency and parallelism, which helps create responsive and high-performance applications. Understanding how to create, manage, and synchronize threads, as well as using the appropriate synchronization techniques, is essential for building safe and efficient multithreaded applications.