Multi-threading and Concurrency in Java

Loading

Multi-threading and concurrency in Java allow you to execute multiple threads simultaneously, enabling efficient utilization of CPU resources and improving application performance. Java provides robust support for multi-threading through its Thread class, Runnable interface, and the java.util.concurrent package.


1. Basics of Multi-threading

  • Thread: A lightweight sub-process that executes independently.
  • Main Thread: The default thread created when a Java program starts.
  • Concurrency: The ability to run multiple threads in parallel or in an interleaved manner.

2. Creating Threads

There are two ways to create threads in Java:

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

a. Extending the Thread Class

class MyThread extends Thread {
    @Override
    public void run() {
        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
    }
}

b. Implementing the Runnable Interface

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

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

3. Thread Lifecycle

A thread can be in one of the following states:

  1. New: The thread is created but not started.
  2. Runnable: The thread is executing or ready to execute.
  3. Blocked/Waiting: The thread is waiting for a resource or another thread.
  4. Timed Waiting: The thread is waiting for a specified time.
  5. Terminated: The thread has completed execution.

4. Thread Synchronization

When multiple threads access shared resources, synchronization is required to avoid race conditions and ensure thread safety.

a. Synchronized Methods

class Counter {
    private int count = 0;

    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();

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

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

b. Synchronized Blocks

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

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

    public int getCount() {
        return count;
    }
}

5. Thread Communication

Threads can communicate with each other using wait(), notify(), and notifyAll() methods.

Example: Producer-Consumer Problem

class SharedResource {
    private int data;
    private boolean available = false;

    public synchronized void produce(int value) throws InterruptedException {
        while (available) {
            wait(); // Wait for the consumer to consume
        }
        data = value;
        available = true;
        notify(); // Notify the consumer
    }

    public synchronized int consume() throws InterruptedException {
        while (!available) {
            wait(); // Wait for the producer to produce
        }
        available = false;
        notify(); // Notify the producer
        return data;
    }
}

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

        Runnable producer = () -> {
            try {
                for (int i = 0; i < 5; i++) {
                    resource.produce(i);
                    System.out.println("Produced: " + i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Runnable consumer = () -> {
            try {
                for (int i = 0; i < 5; i++) {
                    int value = resource.consume();
                    System.out.println("Consumed: " + value);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Thread producerThread = new Thread(producer);
        Thread consumerThread = new Thread(consumer);

        producerThread.start();
        consumerThread.start();
    }
}

6. Thread Pools

Thread pools manage a pool of worker threads to execute tasks efficiently. Java provides the ExecutorService interface for thread pool management.

Example: Using ExecutorService

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(2);

        Runnable task1 = () -> System.out.println("Task 1 executed by " + Thread.currentThread().getName());
        Runnable task2 = () -> System.out.println("Task 2 executed by " + Thread.currentThread().getName());
        Runnable task3 = () -> System.out.println("Task 3 executed by " + Thread.currentThread().getName());

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

        executor.shutdown(); // Shutdown the executor
    }
}

7. Concurrent Collections

Java provides thread-safe collections in the java.util.concurrent package, such as ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue.

Example: Using ConcurrentHashMap

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        map.put("A", 1);
        map.put("B", 2);
        map.put("C", 3);

        System.out.println("Map: " + map);
    }
}

8. Atomic Variables

Atomic variables (e.g., AtomicInteger, AtomicLong) provide thread-safe operations without synchronization.

Example: Using AtomicInteger

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicExample {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);

        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet();
            }
        };

        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println("Counter: " + counter.get()); // Output: 2000
    }
}

9. Deadlock

Deadlock occurs when two or more threads are blocked forever, waiting for each other to release resources.

Example: Deadlock

public class DeadlockExample {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("Thread 1: Holding lock 1");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock2) {
                    System.out.println("Thread 1: Holding lock 1 and lock 2");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("Thread 2: Holding lock 2");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lock1) {
                    System.out.println("Thread 2: Holding lock 2 and lock 1");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

10. Best Practices

  • Prefer Runnable over extending the Thread class.
  • Use thread pools for efficient thread management.
  • Use concurrent collections for thread-safe data structures.
  • Avoid deadlock by acquiring locks in a consistent order.
  • Use atomic variables for simple thread-safe operations.

By mastering multi-threading and concurrency in Java, you can build high-performance, scalable, and responsive applications!

Leave a Reply

Your email address will not be published. Required fields are marked *