Introduction to Multithreading

Multithreading is a feature in Java that allows concurrent execution of two or more parts of a program, known as threads. Each thread runs in parallel and can handle tasks simultaneously, making programs more efficient, especially in multi-core systems.

Key Concepts:

  • Thread: The smallest unit of processing. In Java, every thread has a lifecycle: New, Runnable, Running, Blocked, and Terminated.
  • Concurrency: Executing multiple threads simultaneously. Helps in performing multiple tasks at the same time.
  • Parallelism: Actual simultaneous execution of tasks in a multi-core processor.

Benefits of Multithreading

  • Improved performance: By utilizing multiple threads, a program can perform several tasks concurrently, potentially improving its performance.
  • Resource sharing: Threads within the same process share resources, such as memory, which can lead to more efficient resource usage.
  • Simplified modeling: Multithreading can simplify the modeling of certain tasks, like handling user interfaces or performing background tasks.

Drawbacks of Multithreading

  • Complexity: Managing multiple threads can make a program more complex and harder to debug.
  • Synchronization issues: Threads that share resources can lead to synchronization problems if not managed correctly.
  • Increased resource consumption: Creating and managing multiple threads can consume more resources than a single-threaded approach.

Creating and Running Threads

In Java, you can create threads in two ways:

  1. Extending the Thread class:
  2. Implementing the Runnable interface:

Extending the Thread Class

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

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // Starts the thread, which calls the run() method
    }
}
  

Implementing the Runnable Interface

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

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start(); // Starts the thread, which calls the run() method
    }
}
  

Thread Life Cycle

A thread in Java goes through various states in its life cycle:

  1. New: When a thread is created but not yet started.
  2. Runnable: When a thread is ready to run but waiting for CPU time.
  3. Blocked: When a thread is blocked waiting for a monitor lock.
  4. Waiting: When a thread is waiting indefinitely for another thread to perform a particular action.
  5. Timed Waiting: When a thread is waiting for a specified amount of time.
  6. Terminated: When a thread has finished its execution.

Synchronization

Synchronization in Java is a mechanism to control the access of multiple threads to shared resources. This is crucial to avoid inconsistent data and ensure thread safety.

Synchronized Method

  class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}
  

Synchronized Block

  class Counter {
    private int count = 0;
    private final Object lock = new Object();

    public void increment() {
        synchronized (lock) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}
  

Inter-Thread Communication

Java provides methods like wait(), notify(), and notifyAll() for communication between threads.

Example

  class SharedResource {
    private int value = 0;
    private boolean available = false;

    public synchronized void produce(int newValue) {
        while (available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        value = newValue;
        available = true;
        notify();
    }

    public synchronized int consume() {
        while (!available) {
            try {
                wait();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        available = false;
        notify();
        return value;
    }
}
  

Concurrency Utilities

Java’s java.util.concurrent package provides higher-level concurrency utilities.

Executor Framework

The Executor framework provides a way to manage a pool of threads, making it easier to manage multiple threads.

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

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

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            executor.execute(new MyTask());
        }
        executor.shutdown();
    }
}
  

Locks

Java provides the Lock interface and its implementation classes (ReentrantLock, ReadWriteLock) to have more control over synchronization.

  import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        return count;
    }
}
  

Atomic Variables

The java.util.concurrent.atomic package provides classes like AtomicInteger, AtomicLong, etc., for lock-free thread-safe operations on single variables.

  import java.util.concurrent.atomic.AtomicInteger;

class Counter {
    private final AtomicInteger count = new AtomicInteger();

    public void increment() {
        count.incrementAndGet();
    }

    public int getCount() {
        return count.get();
    }
}
  

By understanding and utilizing these concepts and tools, you can effectively manage concurrency and multithreading in Java applications.