Skip to content

Multithreading in Java

Multithreading in Java allows multiple threads to execute concurrently, enabling better CPU utilization, faster execution, and more efficient handling of tasks. Multithreading is essential for performing complex tasks such as parallel processing, managing I/O operations, or handling multiple user requests simultaneously.

In Java, threads are the smallest unit of execution. Each thread can run concurrently with other threads, and they share the same memory space, allowing communication between them.

Key Concepts of Multithreading

  1. Thread: A lightweight process that can run concurrently with other threads.
  2. Process: A heavyweight entity that consists of one or more threads.
  3. Concurrency: The ability to run multiple threads at the same time.
  4. Parallelism: The simultaneous execution of tasks on multiple processors or cores.

Creating Threads in Java

In Java, there are two primary ways to create threads:

  1. Extending the Thread Class
  2. Implementing the Runnable Interface

1. Extending the Thread Class

You can create a thread by extending the Thread class and overriding the run() method.

Example:

java
class MyThread extends Thread {
    @Override
    public void run() {
        // Code to be executed in the thread
        System.out.println("Thread is running: " + Thread.currentThread().getName());
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();  // Start the thread
    }
}

Explanation:

  • Thread class: Java provides a built-in Thread class that can be extended to create a custom thread.
  • run() method: This is the entry point for the thread's execution. Code inside run() will be executed when the thread starts.
  • start() method: The start() method is used to begin the execution of the thread. It internally calls the run() method.

2. Implementing the Runnable Interface

Alternatively, you can create a thread by implementing the Runnable interface and overriding its run() method. This approach is preferred when your class already extends another class (since Java supports single inheritance).

Example:

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

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();  // Start the thread
    }
}

Explanation:

  • Runnable interface: The Runnable interface defines a single method run(), which is executed when the thread is started.
  • Thread Creation: You create an instance of the Thread class, passing the Runnable object to its constructor.

Thread Lifecycle in Java

A thread goes through several states in its lifecycle:

  1. New: A thread is created but not started.
  2. Runnable: A thread is ready to run, but the CPU may not be executing it yet.
  3. Blocked: A thread is blocked and cannot run (waiting for a resource, like I/O or a lock).
  4. Waiting: A thread is waiting indefinitely for another thread to perform a specific action.
  5. Timed Waiting: A thread is waiting for a specific amount of time (e.g., sleep).
  6. Terminated: A thread has finished execution.

Thread Methods

Java's Thread class provides several useful methods to control thread behavior:

  • start(): Begins the execution of the thread. It calls the run() method.
  • sleep(long millis): Causes the current thread to sleep for the specified number of milliseconds.
  • join(): Allows one thread to wait for another thread to finish before it continues.
  • interrupt(): Interrupts a thread.
  • getName(): Returns the name of the current thread.
  • setPriority(int priority): Sets the priority of the thread.

Example of sleep() and join():

java
class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(2000);  // Sleep for 2 seconds
            System.out.println(Thread.currentThread().getName() + " finished sleeping.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadMethodsExample {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread1 = new MyThread();
        thread1.start();
        thread1.join();  // Main thread waits for thread1 to finish
        
        System.out.println("Main thread resumes after thread1 finishes.");
    }
}

Thread Synchronization

Thread synchronization is a technique to ensure that only one thread can access a resource at a time, preventing race conditions.

In Java, you can synchronize a method or block of code using the synchronized keyword.

Example: Synchronizing a Method

java
class Counter {
    private int count = 0;

    // Synchronized method to ensure thread-safe increment
    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final count: " + counter.getCount());  // Output: 2000
    }
}

Explanation:

  • synchronized keyword: The increment() method is synchronized, meaning only one thread can execute it at a time, ensuring thread safety.

Thread Pool (Executor Framework)

Creating and managing threads manually can be cumbersome and inefficient, especially in applications with many threads. Java provides the Executor Framework for managing a pool of threads, which can help reuse threads and reduce overhead.

Example of Using ExecutorService:

java
import java.util.concurrent.*;

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);  // Create a thread pool with 2 threads

        Runnable task1 = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing task1");
        };

        Runnable task2 = () -> {
            System.out.println(Thread.currentThread().getName() + " is executing task2");
        };

        executor.submit(task1);
        executor.submit(task2);

        executor.shutdown();  // Initiates an orderly shutdown
    }
}

Explanation:

  • ExecutorService: Manages a pool of threads to execute tasks.
  • submit(): Submits tasks for execution.
  • shutdown(): Initiates an orderly shutdown of the executor.

Concurrency Utilities

Java provides several classes in the java.util.concurrent package to help manage concurrency, such as:

  1. CountDownLatch: Allows one or more threads to wait until a set of operations is completed.
  2. CyclicBarrier: A synchronization barrier that enables multiple threads to wait for each other to reach a common point.
  3. Semaphore: Controls access to a particular resource by multiple threads.
  4. ReentrantLock: An implementation of a lock that allows threads to lock and unlock resources.
  5. Atomic Variables: Classes like AtomicInteger, AtomicLong, and AtomicReference provide thread-safe operations on variables.

Example of Using CountDownLatch:

java
import java.util.concurrent.*;

public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);

        Runnable task = () -> {
            try {
                System.out.println(Thread.currentThread().getName() + " is working");
                Thread.sleep(1000);
                latch.countDown();  // Decrements the latch count
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }

        latch.await();  // Main thread will wait until the latch count reaches 0
        System.out.println("All threads finished");
    }
}

Conclusion

Multithreading in Java is a powerful feature that enables efficient execution of concurrent tasks. Key concepts and features include:

  • Creating threads: Using Thread class or Runnable interface.
  • Thread lifecycle: The various states a thread goes through.
  • Synchronization: Ensuring thread safety when accessing shared resources.
  • Executor framework: Efficient thread management with a thread pool.
  • Concurrency utilities: Tools like CountDownLatch, Semaphore, and ReentrantLock for advanced concurrency control.

By understanding and leveraging multithreading, you can significantly improve the performance and responsiveness of Java applications.

J2J Institute private limited