Java Multithreading: Thread States, Pools, and Concurrency Utilities

  1. Thread Lifecycle and States

In Java, a thread does not immediately enter an executing state upon instantiation. It transitions through various states defined by the Thread.State enum. Understanding these states is crucial for debugging and managing concurrent applications.

There are six distinct states:

  • NEW: The thread has been created but not yet started. The start() method has not been invoked.
  • RUNNABLE: The thread is executing in the JVM or is waiting for OS resources (like CPU time). It is ready to run.
  • BLOCKED: The thread is waiting to acquire a monitor lock to enter a synchronized block/method. It is blocked because another thread currently holds the lock.
  • WAITING: The thread is waiting indefinitely for another thread to perform a specific action. This occurs via methods like Object.wait(), Thread.join(), or LockSupport.park().
  • TIMED_WAITING: The thread is waiting for a specifeid period. This is triggered by methods like Thread.sleep(), Object.wait(long), or Thread.join(long).
  • TERMINATED: The thread has completed its execution.

1.1 Monitoring Thread State Transitions

We can observe these transitions by querying the thread's state during its lifecycle. Below is a demonstration of the TIMED_WAITING state using Thread.sleep().

public class TimedWaitingStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            System.out.println("2. After start, state: " + Thread.currentThread().getState());
            try {
                TimeUnit.MILLISECONDS.sleep(200);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("4. After sleep, state: " + Thread.currentThread().getState());
        };

        Thread worker = new Thread(task);
        System.out.println("1. Initialized but not started, state: " + worker.getState());

        worker.start();
        
        TimeUnit.MILLISECONDS.sleep(100); // Check while worker is sleeping
        System.out.println("3. During sleep, state: " + worker.getState());

        worker.join(); // Wait for completion
        System.out.println("5. After termination, state: " + worker.getState());
    }
}

Output:

1. Initialized but not started, state: NEW
2. After start, state: RUNNABLE
3. During sleep, state: TIMED_WAITING
4. After sleep, state: RUNNABLE
5. After termination, state: TERMINATED

1.2 Demonstrating BLOCKED and WAITING States

To demonstrate the BLOCKED state, we need two threads competing for the same lock. For WAITING, we use Object.wait().

public class BlockedWaitingDemo {
    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();

        // Thread 1 holds the lock and waits
        Thread t1 = new Thread(() -> {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // Thread 2 tries to acquire the lock held by T1's monitor (conceptually) or competes for entry
        Thread t2 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 2 acquired lock and notifying.");
                lock.notify();
            }
        });
        
        // Scenario for BLOCKED: T1 holds lock, T3 tries to enter
        Thread t3 = new Thread(() -> {
            synchronized (lock) {
                System.out.println("Thread 3 entered critical section.");
            }
        });

        t1.start();
        Thread.sleep(100); // Ensure t1 waits
        
        t3.start(); 
        Thread.sleep(50); // t3 will likely block if t1 is considered 'holding' or simply blocked on entry monitor
        System.out.println("Thread 3 state while waiting for lock: " + t3.getState());
        
        t2.start(); // Wake up t1
    }
}
  1. Thread Pools

Creating and destroying threads is expensive due to OS interaction. Thread pools manage a collection of reusable threads, improving performance by recycling threads for multiple tasks.

2.1 ExecutorService and Executors

The Executors class provides factory methods for common pool types.

  • newCachedThreadPool(): Creates threads as needed, reusing idle ones.
  • newFixedThreadPool(int n): Limits the pool to a fixed number of threads.
public class BasicPoolDemo {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(3);
        
        for (int i = 0; i < 5; i++) {
            final int taskId = i;
            service.submit(() -> {
                System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
            });
        }
        
        service.shutdown();
    }
}

2.2 ThreadPoolExecutor Parameters

For fine-grained control, we use the ThreadPoolExecutor constructor.

public ThreadPoolExecutor(
    int corePoolSize,        // Minimum threads to keep alive
    int maximumPoolSize,     // Max threads allowed
    long keepAliveTime,      // Idle thread lifetime
    TimeUnit unit,           // Time unit for keepAliveTime
    BlockingQueue<Runnable> workQueue, // Queue for pending tasks
    ThreadFactory threadFactory,       // Factory to create new threads
    RejectedExecutionHandler handler   // Policy for tasks when queue is full
)

2.3 Rejection Policies

When the queue is full and maximum threads are active, new tasks are rejected based on the policy:

  • AbortPolicy (Default): Throws RejectedExecutionException.
  • DiscardPolicy: Silently discards the task.
  • DiscardOldestPolicy: Discards the oldest unhandled task to make room.
  • CallerRunsPolicy: The thread submitting the task executes it itself.
  1. Atomicity and Concurrency

3.1 The Volatile Keyword

The volatile keyword ensures visibility of changes to variables across threads. It prevents threads from caching variables in CPU registers, forcing reads/writes to main memory. However, it does not guarantee atomicity for compound actions (like i++).

class SharedResource {
    // volatile ensures changes are seen immediately by other threads
    static volatile boolean running = true;
}

public class VolatileDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            while (SharedResource.running) {
                // Busy wait
            }
            System.out.println("Thread detected shutdown signal.");
        }).start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        SharedResource.running = false;
        System.out.println("Main set running to false.");
    }
}

3.2 AtomicInteger and CAS

For atomic operations on integers with out using locks, AtomicInteger is ideal. It uses Compare-And-Swap (CAS) logic.

  • CAS Algorithm: Updates the memory value only if the current value matches the expected value. If they differ (meaning another thread modified it), the operation fails and retries (spin).
import java.util.concurrent.atomic.AtomicInteger;

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

        Runnable incrementTask = () -> {
            for (int i = 0; i < 1000; i++) {
                counter.incrementAndGet(); // Atomic operation
            }
        };

        Thread t1 = new Thread(incrementTask);
        Thread t2 = new Thread(incrementTask);

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

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final Count: " + counter.get()); // Will be 2000
    }
}

3.3 Synchronized vs. CAS

synchronized is a pessimistic locking mechanism; it assumes conflicts will happen and blocks other threads. CAS (used in Atomic classes) is optimistic; it assumes conflicts are rare and retries the operation if a conflict is detected.

  1. Concurrent Collections and Utilities

4.1 ConcurrentHashMap

Hashtable is thread-safe but synchronizes on the whole map, causing bottlenecks. ConcurrentHashMap offers high concurrency by locking only segments (JDK 7) or buckets (JDK 8).

Map<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("key1", 100); // Thread-safe put
int val = concurrentMap.get("key1");

4.2 CountDownLatch

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

import java.util.concurrent.CountDownLatch;

public class CountdownDemo {
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(3); // Wait for 3 services

        Thread service1 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
                System.out.println("Service 1 initialized");
                latch.countDown();
            } catch (InterruptedException e) {}
        });

        Thread service2 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(800);
                System.out.println("Service 2 initialized");
                latch.countDown();
            } catch (InterruptedException e) {}
        });

        Thread service3 = new Thread(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(300);
                System.out.println("Service 3 initialized");
                latch.countDown();
            } catch (InterruptedException e) {}
        });

        service1.start();
        service2.start();
        service3.start();

        try {
            latch.await(); // Main thread waits here
            System.out.println("All services are up. Main application starting.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4.3 Semaphore

Semaphores control access to a specific resource by limiting the number of permits. It's useful for connection pools or rate limiting.

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {
    public static void main(String[] args) {
        Semaphore connectionPool = new Semaphore(2); // Only 2 connections allowed

        Runnable accessDB = () -> {
            try {
                connectionPool.acquire();
                System.out.println(Thread.currentThread().getName() + " acquired connection.");
                TimeUnit.SECONDS.sleep(2); // Simulate work
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println(Thread.currentThread().getName() + " released connection.");
                connectionPool.release();
            }
        };

        for (int i = 0; i < 5; i++) {
            new Thread(accessDB, "Client-" + i).start();
        }
    }
}

Posted on Fri, 06 Nov 2026 16:26:35 +0000 by foochuck