Multithreading in Java


1. Key Concepts

  • Thread: A lightweight process with its own execution path.
  • Concurrency: Multiple tasks making progress within the same time frame.
  • Parallelism: Tasks running simultaneously on multiple processors.

2. Thread Creation

Using Thread class

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

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

Using Runnable interface

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

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

3. Thread Lifecycle

  1. New: Thread is created but not yet started.
  2. Runnable: Thread is ready but not yet executed.
  3. Blocked: Thread is waiting for a resource.
  4. Waiting: Thread is waiting for another thread.
  5. Timed Waiting: Thread is waiting for a specific period.
  6. Terminated: Thread has completed its execution.

4. Thread Methods

  • start(): Starts the thread.
  • run(): Code executed by the thread.
  • sleep(long millis): Pauses the thread for a given time.
  • yield(): Gives up CPU time to allow other threads to execute.
  • join(): Waits for the thread to finish before proceeding.
  • interrupt(): Interrupts the thread.
  • getName(): Gets the thread’s name.
  • setPriority(int priority): Sets the thread’s priority.

5. Thread Synchronization

Using synchronized keyword

  • Method-level synchronization:
class Counter {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}
  • Block-level synchronization:
class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

Using ReentrantLock

import java.util.concurrent.locks.*;

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;
    }
}

6. Deadlock Prevention

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

Preventing Deadlock:

  1. Avoid circular dependencies.
  2. Use timeouts with ReentrantLock.
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();

Thread t1 = new Thread(() -> {
    try {
        if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
            if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
                // Perform work
            } else {
                lock1.unlock();
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

7. Executor Framework

Manages a pool of threads for efficient task execution.

Using ExecutorService

import java.util.concurrent.*;

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

        executor.submit(() -> System.out.println("Task 1"));
        executor.submit(() -> System.out.println("Task 2"));

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

Common Executors:

  • Executors.newFixedThreadPool(int nThreads)
  • Executors.newCachedThreadPool()
  • Executors.newSingleThreadExecutor()

8. Callable and Future

  • Callable: Returns a result or throws an exception.
  • Future: Represents the result of an asynchronous computation.
import java.util.concurrent.*;

public class CallableExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();
        Callable<Integer> task = () -> 5 + 3;

        Future<Integer> future = executor.submit(task);

        System.out.println("Result: " + future.get());  // Output: 8

        executor.shutdown();
    }
}

9. Concurrent Collections

Java provides thread-safe collections in java.util.concurrent:

  • CopyOnWriteArrayList
  • ConcurrentHashMap
  • BlockingQueue
  • ConcurrentSkipListMap

Example with ConcurrentHashMap:

import java.util.concurrent.*;

public class ConcurrentMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
        map.put("key1", "value1");
        map.put("key2", "value2");

        System.out.println(map.get("key1"));  // Output: value1
    }
}

10. Thread Communication

  • wait(): Makes the current thread wait until another thread signals.
  • notify(): Wakes up a waiting thread.
  • notifyAll(): Wakes up all waiting threads.
class Counter {
    private int count = 0;

    public synchronized void increment() throws InterruptedException {
        while (count == 1) {
            wait();  // Wait for other threads
        }
        count++;
        notify();  // Notify other threads
    }

    public synchronized void decrement() throws InterruptedException {
        while (count == 0) {
            wait();
        }
        count--;
        notify();
    }
}

11. Concurrency Utilities

CountDownLatch

  • Used to make one or more threads wait until a set of operations completes.
import java.util.concurrent.*;

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

        Thread t1 = new Thread(() -> {
            System.out.println("Task 1 is working");
            latch.countDown();
        });
        Thread t2 = new Thread(() -> {
            System.out.println("Task 2 is working");
            latch.countDown();
        });

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

        latch.await();  // Main thread waits
        System.out.println("All tasks are finished.");
    }
}

CyclicBarrier

  • Makes threads wait for each other to reach a common barrier point.
import java.util.concurrent.*;

public class CyclicBarrierExample {
    public static void main(String[] args) throws InterruptedException {
        CyclicBarrier barrier = new CyclicBarrier(2, () -> System.out.println("Both threads have reached the barrier"));

        Thread t1 = new Thread(() -> {
            System.out.println("Thread 1 reached the barrier");
            try {
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            System.out.println("Thread 2 reached the barrier");
            try {
                barrier.await();
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            }
        });

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

Semaphore

  • Controls access to a shared resource with a set number of permits.
import java.util.concurrent.*;

public class SemaphoreExample {
    public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);  // Allow 2 threads

        Runnable task = () -> {
            try {
                semaphore.acquire();  // Acquire permit
                System.out.println(Thread.currentThread().getName() + " acquired a permit");
                Thread.sleep(2000);  // Simulate work
                System.out.println(Thread.currentThread().getName() + " releasing a permit");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();  // Release permit
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);

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

Exchanger

  • Allows two threads to exchange objects.
import java.util.concurrent.*;

public class ExchangerExample {
    public static void main(String[] args) throws InterruptedException {
        Exchanger<String> exchanger = new Exchanger<>();

        Thread t1 = new Thread(() -> {
            try {
                String data = "Data from t1";
                System.out.println("t1 exchanging: " + data);
                data = exchanger.exchange(data);


                System.out.println("t1 received: " + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                String data = "Data from t2";
                System.out.println("t2 exchanging: " + data);
                data = exchanger.exchange(data);
                System.out.println("t2 received: " + data);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

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

12. volatile Keyword

  • volatile guarantees visibility of the value to all threads. It doesn’t provide atomicity.
class VolatileExample {
    private volatile boolean flag = false;

    public void toggleFlag() {
        flag = !flag;
    }

    public boolean isFlag() {
        return flag;
    }
}

public class Main {
    public static void main(String[] args) {
        VolatileExample example = new VolatileExample();

        new Thread(() -> {
            while (!example.isFlag()) {
                // Wait until flag is changed
            }
            System.out.println("Flag was changed!");
        }).start();

        // Changing flag in main thread
        example.toggleFlag();
    }
}
  • Important: Use volatile when you only need to ensure visibility (e.g., for flags or single variables) but not atomicity. For more complex operations, use Atomic classes.

13. atomic Classes

Java provides atomic classes in the java.util.concurrent.atomic package to perform thread-safe operations on single variables.

AtomicInteger

  • Supports atomic increments, decrements, and updates.
import java.util.concurrent.atomic.AtomicInteger;

class AtomicExample {
    private AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // Atomically increment the value
    }

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

public class Main {
    public static void main(String[] args) {
        AtomicExample example = new AtomicExample();

        example.increment();
        example.increment();

        System.out.println("Count: " + example.getCount());  // Output: Count: 2
    }
}

AtomicBoolean

  • Atomic operation on boolean values.
import java.util.concurrent.atomic.AtomicBoolean;

class AtomicBooleanExample {
    private AtomicBoolean flag = new AtomicBoolean(false);

    public void toggleFlag() {
        flag.set(!flag.get());
    }

    public boolean isFlag() {
        return flag.get();
    }
}

public class Main {
    public static void main(String[] args) {
        AtomicBooleanExample example = new AtomicBooleanExample();

        example.toggleFlag();
        System.out.println("Flag: " + example.isFlag());  // Output: Flag: true
    }
}

14. Best Practices

  • Use thread pools instead of manually managing threads.
  • Minimize synchronization to avoid performance bottlenecks.
  • Use timeout mechanisms to prevent deadlocks.
  • Use volatile for variables shared between threads that don’t require atomicity.
  • Use atomic classes for thread-safe operations on single variables.
  • Properly release resources (e.g., ReentrantLock.unlock(), finally blocks).
  • Prefer higher-level concurrency utilities for complex scenarios.

Leave a Reply