Creating and Managing Threads in Java

In Java, a thread is a lightweight subprocess that can execute concurrently with other parts of a program. Threads are widely used for performing multiple tasks simultaneously, such as handling user input, network communication, and parallel processing. This article explores how to create and manage threads in Java, offering insights into thread creation, synchronization, and thread pooling.

Creating Threads in Java

There are two main approaches to create threads in Java:

1. Extending the Thread class

To create a thread by extending the Thread class, follow these steps:

  1. Create a new class that extends the Thread class.
  2. Override the run() method, which contains the code to be executed in the thread.
  3. Instantiate and start the thread by creating an instance of the class and calling its start() method.

Here's an example of creating a thread by extending the Thread class:

class MyThread extends Thread {
    public void run() {
        // Code to be executed in the thread
        System.out.println("Thread is running");
    }
}

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

2. Implementing the Runnable interface

To create a thread by implementing the Runnable interface, follow these steps:

  1. Create a new class that implements the Runnable interface.
  2. Implement the run() method, which contains the code to be executed in the thread.
  3. Create an instance of the class and pass it as an argument to a new Thread object.
  4. Start the thread by calling the start() method.

Here's an example of creating a thread by implementing the Runnable interface:

class MyRunnable implements Runnable {
    public void run() {
        // Code to be executed in the thread
        System.out.println("Thread is running");
    }
}

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

Synchronizing Threads

In multithreaded applications, synchronization is essential to ensure thread safety and avoid race conditions. Java provides synchronization mechanisms such as the synchronized keyword and wait()/notify() methods.

1. Synchronized keyword

The synchronized keyword is used to create synchronized blocks or methods. It ensures that only one thread can access the synchronized block or method at a time.

class MyThread extends Thread {
    private static int counter = 0;

    public synchronized void run() {
        // Synchronized block
        counter++;
        System.out.println("Counter: " + counter);
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        // Both threads will execute the synchronized run method separately
        thread1.start();
        thread2.start();
    }
}

2. wait() and notify() methods

The wait() and notify() methods are used to implement inter-thread communication, allowing threads to pause and resume execution based on certain conditions.

class MyThread extends Thread {
    public static final Object lock = new Object();

    public void run() {
        synchronized (lock) {
            System.out.println("Thread is waiting");
            try {
                lock.wait(); // Thread waits until notified
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread is notified and resumed");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        synchronized (MyThread.lock) {
            try {
                Thread.sleep(2000); // Delay to ensure thread starts waiting
                MyThread.lock.notify(); // Notify the waiting thread
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Thread Pooling

Creating a new thread for each task can be resource-intensive in situations with numerous short-lived tasks. Thread pooling addresses this issue by reusing existing threads from a pool, minimizing the overhead of creating new threads.

Using the Executor Framework

The Executor framework provides the ExecutorService interface and its implementations, such as ThreadPoolExecutor. Here's an example:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new MyRunnable();
            executor.execute(worker);
        }
        executor.shutdown();
        while (!executor.isTerminated()) {
            // Wait for all tasks to finish
        }
        System.out.println("All tasks completed");
    }
}

In this example, we create a thread pool with a fixed number of threads (5). We submit Runnable tasks to the executor, which handles thread allocation and task execution. Finally, we wait for all tasks to complete and shutdown the executor.

Conclusion

Threads play a crucial role in writing concurrent and efficient programs. By understanding how to create and manage threads in Java, utilizing synchronization mechanisms, and implementing thread pooling, developers can leverage the power of concurrent processing while maintaining thread safety and performance. Remember to use threads judiciously, as excessive or inefficient thread usage can lead to resource contention and other concurrency-related issues.


noob to master © copyleft