- 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(), orLockSupport.park(). - TIMED_WAITING: The thread is waiting for a specifeid period. This is triggered by methods like
Thread.sleep(),Object.wait(long), orThread.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
}
}
- 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.
- 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.
- 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();
}
}
}