Thread

Java provides built-in support for multithreaded programming, which allows you to write programs that can perform many tasks concurrently. Here’s a comprehensive overview of Java threads and related topics, covering basic concepts and problem solutions.

Basic Concepts of Threads

Thread: A thread is a lightweight sub-process, the smallest unit of processing. It is a separate path of execution.

Multithreading: The capability of a CPU to manage multiple threads simultaneously. It improves the performance of applications by allowing concurrent execution.

Creating Threads

In Java, there are two main ways to create a thread:

  1. Extending the Thread class

    class MyThread extends Thread {
        public void run() {
            System.out.println("Thread is running");
        }
    }
    
    public class TestThread {
        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 TestRunnable {
        public static void main(String[] args) {
            MyRunnable myRunnable = new MyRunnable();
            Thread t1 = new Thread(myRunnable);
            t1.start(); // Start the thread
        }
    }

Thread States

A thread in Java can be in one of the following states:

  • New: A thread that has been created but not yet started.

  • Runnable: A thread that is ready to run is moved to the runnable state.

  • Blocked: A thread that is blocked waiting for a monitor lock.

  • Waiting: A thread that is waiting indefinitely for another thread to perform a particular action.

  • Timed Waiting: A thread that is waiting for another thread to perform an action up to a specified waiting time.

  • Terminated: A thread that has exited.

Thread Lifecycle Methods

  • start(): Starts the execution of the thread.

  • run(): The entry point for the thread.

  • sleep(long milliseconds): Causes the currently executing thread to sleep for the specified number of milliseconds.

  • join(): Waits for a thread to die.

  • yield(): Causes the currently executing thread object to temporarily pause and allow other threads to execute.

  • interrupt(): Interrupts a thread.

Thread Synchronization

To avoid thread interference and memory consistency errors, synchronization is used. Synchronization ensures that only one thread can access a resource at a time.

  1. Synchronized Method

    public synchronized void method() {
        // synchronized code
    }
  2. Synchronized Block

    public void method() {
        synchronized (this) {
            // synchronized code
        }
    }

Inter-thread Communication

Java provides a way for threads to communicate with each other via wait(), notify(), and notifyAll() methods.

  • wait(): Tells the calling thread to give up the monitor and go to sleep until some other thread enters the same monitor and calls notify().

  • notify(): Wakes up a single thread that is waiting on this object's monitor.

  • notifyAll(): Wakes up all the threads that called wait() on the same object.

Deadlock

A situation where two or more threads are blocked forever, waiting for each other. It is a condition where a set of threads are waiting for resources that other threads in the set hold.

Example of Deadlock:

class A {
    synchronized void methodA(B b) {
        b.last();
    }
    synchronized void last() {}
}

class B {
    synchronized void methodB(A a) {
        a.last();
    }
    synchronized void last() {}
}

public class DeadlockDemo {
    A a = new A();
    B b = new B();

    DeadlockDemo() {
        new Thread(() -> a.methodA(b)).start();
        new Thread(() -> b.methodB(a)).start();
    }

    public static void main(String[] args) {
        new DeadlockDemo();
    }
}

Thread Pools

Managing multiple threads manually can be difficult. Java provides the Executor framework, which simplifies thread management.

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

public class TestThreadPool {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10); // creating a pool of 10 threads
        for (int i = 0; i < 100; i++) {
            executor.execute(new WorkerThread("task " + i));
        }
        executor.shutdown();
    }
}

class WorkerThread implements Runnable {
    private String message;
    public WorkerThread(String s) {
        this.message = s;
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + " (Start) message = " + message);
        // process message
        System.out.println(Thread.currentThread().getName() + " (End)");
    }
}

Thread Safety in Java

Thread safety in Java refers to the property of an object or code segment to behave correctly when accessed by multiple threads concurrently. Thread-safe code ensures that shared data is accessed and modified correctly, preventing inconsistent or unpredictable behavior. Here are the key aspects of thread safety:

  1. Synchronization: Using synchronized blocks or methods to control access to shared resources. This ensures that only one thread can access the resource at a time.

    public class Counter {
        private int count = 0;
    
        public synchronized void increment() {
            count++;
        }
    
        public synchronized int getCount() {
            return count;
        }
    }
  2. Volatile Keyword: Declaring a variable as volatile ensures that its updates are visible to all threads. This prevents threads from caching the variable’s value locally.

    public class VolatileExample {
        private volatile boolean flag = true;
    
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    
        public boolean getFlag() {
            return flag;
        }
    }
  3. Atomic Classes: Using classes from java.util.concurrent.atomic package like AtomicInteger, AtomicBoolean, etc., which provide methods to perform atomic operations on single variables.

    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicCounter {
        private AtomicInteger count = new AtomicInteger(0);
    
        public void increment() {
            count.incrementAndGet();
        }
    
        public int getCount() {
            return count.get();
        }
    }
  4. Concurrent Collections: Utilizing thread-safe collections from the java.util.concurrent package, such as ConcurrentHashMap, CopyOnWriteArrayList, etc.

    import java.util.concurrent.ConcurrentHashMap;
    
    public class ConcurrentMapExample {
        private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    
        public void put(String key, Integer value) {
            map.put(key, value);
        }
    
        public Integer get(String key) {
            return map.get(key);
        }
    }

Immutable Objects in Java

An immutable object is an object whose state cannot be modified after it is created. Immutability is an effective way to achieve thread safety since immutable objects can be shared freely among threads without synchronization.

Characteristics of Immutable Objects:

  • Final Class: The class should be declared as final so that it cannot be subclassed.

  • Final Fields: All fields should be final so that they are initialized only once.

  • Private Fields: Fields should be private to prevent direct access.

  • No Setters: There should be no setter methods to modify the fields.

  • Initialization via Constructor: Fields should be initialized through a constructor.

  • Defensive Copies: If the class has mutable fields, return defensive copies instead of the actual objects.

Example of an Immutable Class:

public final class ImmutablePerson {
    private final String name;
    private final int age;
    private final List<String> hobbies;

    public ImmutablePerson(String name, int age, List<String> hobbies) {
        this.name = name;
        this.age = age;
        this.hobbies = new ArrayList<>(hobbies); // Defensive copy
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public List<String> getHobbies() {
        return new ArrayList<>(hobbies); // Defensive copy
    }
}

Understanding Java threads involves knowing how to create, manage, and synchronize them effectively. It’s also important to be aware of potential issues like deadlock and to use thread pools to manage large numbers of threads. By mastering these concepts, you can write efficient and robust multithreaded applications.

Last updated