Multithreading and Multiprocessing in Java

Java supports concurrent programming through multithreading and multiprocessing. These mechanisms enable a program to perform multiple tasks simultaneously, improving performance and resource utilization.

What is Multiprocessing?

Multiprocessing is the capability of a system to run multiple processes simultaneously. Each process runs independently, having its own memory space and resources.

Example: Running multiple Java applications on the same system.

What is Multithreading?

Multithreading is a programming technique where a single process can execute multiple threads concurrently. Threads share the same memory space and resources.


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

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

Differences Between Multiprocessing and Multithreading

Feature Multiprocessing Multithreading
Memory Each process has its own memory space. Threads share the same memory space.
Communication Processes communicate using IPC (Inter-Process Communication). Threads communicate more easily by sharing memory.
Overhead Higher due to context switching between processes. Lower due to lightweight threads.
Execution Independent processes run in parallel. Threads run within the same process.

Creating Threads in Java

Threads can be created in Java using two main approaches:

1. Extending the Thread Class


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

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

2. Implementing the Runnable Interface


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

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

Getting and Setting the Name of a Thread

In Java, every thread has a name, which can be set by the programmer or automatically generated by the JVM. The Thread class provides methods to get and set the name of a thread.

Methods to Get and Set Thread Name

Example:


class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();  // Create a thread instance
        thread.setName("MyCustomThread");  // Set the thread's name
        thread.start();                    // Start the thread
        System.out.println("Thread name: " + thread.getName());  // Get the thread's name
    }
}
      

In this example, the thread's name is set before starting the thread, and the name is then retrieved and displayed.

Getting the Current Executing Thread

The Thread.currentThread() method returns a reference to the currently executing thread.

Example:


public class Main {
    public static void main(String[] args) {
        // Get the reference to the currently executing thread
        Thread currentThread = Thread.currentThread();
        
        // Display the name of the current thread
        System.out.println("Current thread name: " + currentThread.getName());
        
        // You can also set a new name for the current thread
        currentThread.setName("MainThread");
        
        // Display the new name of the current thread
        System.out.println("Updated thread name: " + currentThread.getName());
        
        // Example of starting a new thread using an anonymous inner class
        Thread newThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("New thread is running.");
                System.out.println("New thread name: " + Thread.currentThread().getName());
            }
        });
        
        newThread.start();
    }
}
      

This example demonstrates how to get and set the name of the currently executing thread, and also how to start a new thread with an anonymous inner class.

Thread Priorities

Every thread in Java has a priority that determines the order in which threads are scheduled for execution. Thread priorities range from 1 (lowest) to 10 (highest), with the default priority being 5.

Standard Priority Constants

Getting and Setting Thread Priority

Error Handling for Invalid Priority

If an invalid priority (less than 1 or greater than 10) is set, the IllegalArgumentException will be thrown.

Example:


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

public class Main {
    public static void main(String[] args) {
        // Create threads with different priorities
        Thread highPriorityThread = new Thread(new MyRunnable(), "HighPriorityThread");
        Thread mediumPriorityThread = new Thread(new MyRunnable(), "MediumPriorityThread");
        Thread lowPriorityThread = new Thread(new MyRunnable(), "LowPriorityThread");
        
        // Set priorities
        highPriorityThread.setPriority(Thread.MAX_PRIORITY); // Priority 10
        mediumPriorityThread.setPriority(Thread.NORM_PRIORITY); // Priority 5
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY); // Priority 1
        
        // Start the threads
        lowPriorityThread.start();
        mediumPriorityThread.start();
        highPriorityThread.start();
    }
}
      

This example demonstrates how threads with different priorities are executed. The thread with the highest priority will be favored by the thread scheduler, although the exact execution order may vary depending on the platform.

Summary

Concept Method Description
Thread Name setName(String name) & getName() Set and get the name of a thread.
Current Thread currentThread() Get a reference to the currently executing thread.
Thread Priority setPriority(int newPriority) & getPriority() Set and get the priority of a thread. Valid priorities range from 1 to 10.
Invalid Priority IllegalArgumentException Thrown when an invalid priority value is set outside the range of 1 to 10.

The Methods to Prevent a Thread from Execution:

We can prevent(stop) a Thread execution by using the following methods. 1. sleep(); 2. join(); 3. yield();

1. sleep() Method

The sleep() method pauses the current thread for a specified amount of time. If the thread is interrupted while sleeping, it throws an InterruptedException.


class SleepExample extends Thread {
    public void run() {
        try {
            System.out.println("Thread sleeping for 2 seconds...");
            Thread.sleep(2000); // Sleep for 2 seconds
            System.out.println("Thread woke up!");
        } catch (InterruptedException e) {
            System.out.println("Thread was interrupted during sleep.");
        }
    }

    public static void main(String[] args) {
        SleepExample t1 = new SleepExample();
        t1.start();
    }
}
      

2. join() Method

The join() method allows one thread to wait for another thread to finish its execution.


class JoinExample extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running...");
    }

    public static void main(String[] args) throws InterruptedException {
        JoinExample t1 = new JoinExample();
        JoinExample t2 = new JoinExample();

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

        t1.join(); // Main thread waits for t1 to finish
        t2.join(); // Main thread waits for t2 to finish

        System.out.println("Both threads have finished.");
    }
}
      

3. yield() Method

The yield() method is a suggestion to the thread scheduler that the current thread is willing to yield its CPU time slice. This method does not guarantee that other threads will execute immediately.


class YieldExample extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName() + " started.");
        Thread.yield(); // Yield to other threads
        System.out.println(Thread.currentThread().getName() + " resumed.");
    }

    public static void main(String[] args) {
        YieldExample t1 = new YieldExample();
        YieldExample t2 = new YieldExample();

        t1.start();
        t2.start();
    }
}
      

Differences Between sleep(), join(), yield()

Method Description Behavior
sleep() Pauses the current thread for a specified time. Thread is paused for the specified duration, can be interrupted.
join() Allows the current thread to wait for another thread to complete. Blocks the current thread until the thread it is called on finishes executing.
yield() Suggests the current thread yield control to other threads of the same priority. Thread gives up its CPU time slice, but no guarantee another thread will run.

Interrupting Threads

Interrupting a thread allows you to stop its execution. The interrupt() method sets the interrupt flag for a thread, but the thread itself must periodically check this flag to handle the interruption appropriately.


class InterruptExample extends Thread {
    public void run() {
        try {
            Thread.sleep(5000); // Sleep for 5 seconds
        } catch (InterruptedException e) {
            System.out.println("Thread was interrupted.");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InterruptExample t1 = new InterruptExample();
        t1.start();
        Thread.sleep(2000); // Wait for 2 seconds before interrupting
        t1.interrupt(); // Interrupt the thread
    }
}
      

In this example, the thread is interrupted after 2 seconds, and the interrupt flag is set, causing the thread to handle the interruption.

Thread Methods

Method Description
start() Starts the thread and calls the run() method.
run() Contains the code to be executed by the thread.
sleep() Pauses the thread for a specified time.
join() Waits for the thread to finish execution.
isAlive() Checks if the thread is still running.

Thread Life Cycle

Each thread in Java can be in one of several states:

Example of Thread State Transitions:


          class ThreadStateExample extends Thread {
              public void run() {
                  System.out.println("Thread is running...");
              }
          
              public static void main(String[] args) throws InterruptedException {
                  ThreadStateExample t1 = new ThreadStateExample();
          
                  System.out.println("State after creation: " + t1.getState()); // NEW
                  t1.start();
                  System.out.println("State after calling start(): " + t1.getState()); // RUNNABLE
                  t1.join();
                  System.out.println("State after thread termination: " + t1.getState()); // TERMINATED
              }
          }
                

Daemon Threads

Daemon threads run in the background to perform tasks like garbage collection. Use the setDaemon() method to mark a thread as a daemon.


class MyDaemonThread extends Thread {
    public void run() {
        System.out.println("Daemon thread running...");
    }
}

public class DaemonExample {
    public static void main(String[] args) {
        MyDaemonThread t1 = new MyDaemonThread();
        t1.setDaemon(true); // Set as daemon thread
        t1.start();
    }
}
      

Synchronization in Java

Synchronization in Java is a mechanism that ensures only one thread can access a critical section of code at a time, preventing concurrent access issues and avoiding data inconsistency. This mechanism is essential when multiple threads access shared resources.

How Synchronization Works

When a thread enters a synchronized method or block, it acquires a lock on the object (or class, in the case of static synchronization), ensuring that only one thread can execute the synchronized code at a time while all other threads are in a waiting state.

Advantages and Disadvantages of Synchronization

Advantages:

Disadvantages:

Example: Synchronizing Methods

The synchronized keyword can be applied to methods to ensure that only one thread can execute the method at a time.


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 SynchronizationExample {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();

        // Create two threads that will increment the 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();
            }
        });

        // Start the threads
        t1.start();
        t2.start();

        // Wait for both threads to finish
        t1.join();
        t2.join();

        // Print the final count after both threads have completed
        System.out.println("Final count: " + counter.getCount());
    }
}
      

In this example, the increment() method is synchronized, ensuring that only one thread can increment the count at a time.

Example: Synchronizing Blocks

Instead of synchronizing an entire method, you can synchronize specific blocks of code within a method. This is often used for fine-grained control over which sections of code need synchronization.


class Counter {
    private int count = 0;

    public void increment() {
        // Synchronizing only the critical section
        synchronized(this) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

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

        // Create two threads that will increment the 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();
            }
        });

        // Start the threads
        t1.start();
        t2.start();

        // Wait for both threads to finish
        t1.join();
        t2.join();

        // Print the final count after both threads have completed
        System.out.println("Final count: " + counter.getCount());
    }
}
      

This example demonstrates how to use a synchronized block to protect only the critical section of code where shared resources are accessed, providing more control over synchronization.

Static Synchronization

Static synchronization is used when a thread needs to lock the class itself (not just the instance). This is done by synchronizing static methods.


class Counter {
    private static int count = 0;

    // Synchronized static method
    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

public class SynchronizationExample {
    public static void main(String[] args) throws InterruptedException {
        // Create two threads that will increment the 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();
            }
        });

        // Start the threads
        t1.start();
        t2.start();

        // Wait for both threads to finish
        t1.join();
        t2.join();

        // Print the final count after both threads have completed
        System.out.println("Final count: " + Counter.getCount());
    }
}
      

In this example, the static increment() method is synchronized, which ensures that only one thread can access this method at a time across all instances of the class.

Inter-Thread Communication

Inter-thread communication allows threads to communicate using the wait(), notify(), and notifyAll() methods. These methods allow threads to cooperate and coordinate their execution, especially when one thread needs to wait for a condition to be met by another thread.

The classic example of inter-thread communication is the producer-consumer problem, where one thread produces items and another consumes them. The consumer thread waits until the producer thread produces an item.

Producer-Consumer Example


class SharedResource {
private boolean available = false;

public synchronized void produce() throws InterruptedException {
  while (available) {
      wait(); // Wait until the item is consumed
  }
  System.out.println("Produced");
  available = true; // Item is now produced
  notify(); // Notify the consumer that the item is produced
}

public synchronized void consume() throws InterruptedException {
  while (!available) {
      wait(); // Wait until the item is produced
  }
  System.out.println("Consumed");
  available = false; // Item is now consumed
  notify(); // Notify the producer that the item is consumed
}
}

public class InterThreadExample {
public static void main(String[] args) {
  SharedResource resource = new SharedResource();

  Thread producer = new Thread(() -> {
      try {
          for (int i = 0; i < 5; i++) {
              resource.produce();
          }
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  });

  Thread consumer = new Thread(() -> {
      try {
          for (int i = 0; i < 5; i++) {
              resource.consume();
          }
      } catch (InterruptedException e) {
          e.printStackTrace();
      }
  });

  producer.start();
  consumer.start();
}
}
      

In this example, the producer thread produces an item and notifies the consumer thread, which consumes the item. The consumer thread waits if the item is not available. The producer and consumer threads synchronize their work using the wait() and notify() methods.

How It Works:

This mechanism helps ensure that the producer and consumer threads work in sync, without one thread running ahead or being blocked unnecessarily.

Thread Safety and Deadlock

Thread safety refers to the ability of a program or a section of code to function correctly when multiple threads access it simultaneously. Deadlock occurs when two or more threads are blocked forever due to mutual resource locking.

Thread Safety Example:

Use synchronization to prevent thread interference when multiple threads access shared resources simultaneously.


      class SafeCounter {
          private int count = 0;
      
          public synchronized void increment() {
              count++;
          }
      
          public int getCount() {
              return count;
          }
      }
      
      public class ThreadSafetyExample {
          public static void main(String[] args) throws InterruptedException {
              SafeCounter counter = new SafeCounter();
      
              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()); // Ensure thread safety
          }
      }
            

Deadlock Example:

Deadlock occurs when two or more threads are blocked forever due to circular resource dependency.


      class Resource1 {
          public synchronized void method1(Resource2 resource2) {
              System.out.println("Thread 1: Holding Resource 1...");
              try { Thread.sleep(100); } catch (InterruptedException e) {}
              resource2.last();
          }
      
          public synchronized void last() {
              System.out.println("Thread 1: In method last");
          }
      }
      
      class Resource2 {
          public synchronized void method2(Resource1 resource1) {
              System.out.println("Thread 2: Holding Resource 2...");
              try { Thread.sleep(100); } catch (InterruptedException e) {}
              resource1.last();
          }
      
          public synchronized void last() {
              System.out.println("Thread 2: In method last");
          }
      }
      
      public class DeadlockExample {
          public static void main(String[] args) {
              final Resource1 resource1 = new Resource1();
              final Resource2 resource2 = new Resource2();
      
              Thread t1 = new Thread(() -> resource1.method1(resource2));
              Thread t2 = new Thread(() -> resource2.method2(resource1));
      
              t1.start();
              t2.start();
          }
      }
            

In this example, two threads hold resources that the other thread needs, resulting in a deadlock.

Thread Pool Executor

The ExecutorService interface provides methods to manage a pool of threads efficiently. The ThreadPoolExecutor class manages a pool of worker threads to execute tasks.

Types of Executors in Java

In Java, executors are part of the java.util.concurrent package, and they provide a high-level mechanism for managing and controlling threads. Executors manage a pool of worker threads and allow tasks to be executed asynchronously, improving the efficiency and responsiveness of multithreaded programs.

Executor Factory Methods

The Executor class offers factory methods to create thread pools for managing and executing tasks. The following are the most commonly used factory methods:

How Executors Work

The basic workflow of how tasks are managed by executors is as follows:

  1. You submit a task to the executor using methods like submit() or execute().
  2. The executor places the task in a task queue.
  3. A worker thread from the pool retrieves the task from the queue and executes it.
  4. Once the task is complete, the worker thread returns to the pool, ready to execute another task.

Benefits of Using Executors

Examples of Executors

1. Using newFixedThreadPool

The newFixedThreadPool() method creates a fixed-size thread pool.


        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        
        public class FixedThreadPoolExample {
            public static void main(String[] args) {
                ExecutorService executor = Executors.newFixedThreadPool(3);
        
                for (int i = 0; i < 5; i++) {
                    executor.submit(() -> {
                        System.out.println(Thread.currentThread().getName() + " is executing a task.");
                    });
                }
        
                executor.shutdown();
            }
        }
              

In this example, we create a thread pool with 3 threads. The first 3 tasks are executed immediately, and the remaining 2 tasks are queued until a thread becomes available.

2. Using newCachedThreadPool

The newCachedThreadPool() method creates a thread pool that dynamically adjusts the number of threads based on demand.


        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        
        public class CachedThreadPoolExample {
            public static void main(String[] args) {
                ExecutorService executor = Executors.newCachedThreadPool();
        
                for (int i = 0; i < 5; i++) {
                    executor.submit(() -> {
                        System.out.println(Thread.currentThread().getName() + " is executing a task.");
                    });
                }
        
                executor.shutdown();
            }
        }
              

In this example, threads are created as needed, and idle threads are removed after 60 seconds of inactivity.

3. Using newSingleThreadExecutor

The newSingleThreadExecutor() method creates a thread pool with a single worker thread that executes tasks sequentially.


        import java.util.concurrent.ExecutorService;
        import java.util.concurrent.Executors;
        
        public class SingleThreadExecutorExample {
            public static void main(String[] args) {
                ExecutorService executor = Executors.newSingleThreadExecutor();
        
                for (int i = 0; i < 5; i++) {
                    executor.submit(() -> {
                        System.out.println(Thread.currentThread().getName() + " is executing a task.");
                    });
                }
        
                executor.shutdown();
            }
        }
              

In this example, even though multiple tasks are submitted, they are executed sequentially by the single worker thread in the pool.

Summary

Concept Description
Multithreading Allows concurrent execution of multiple threads within the same process, sharing the same memory space.
Multiprocessing Allows multiple processes to run simultaneously, each with its own memory space.
Thread Safety Ensures that shared resources are properly managed when accessed by multiple threads.
Daemon Threads Background threads that do not prevent the JVM from exiting.
Synchronization Prevents thread interference when multiple threads access shared resources.
Inter-Thread Communication Allows threads to communicate using wait(), notify(), and notifyAll().
Thread Pool Executor Manages a pool of worker threads to efficiently handle multiple tasks concurrently.