Monitors, Thread Lifecycle, Concurrent Programming, and Thread-Safe Collections
In Java, a thread goes through various states from the moment it
is created until it finishes execution. Understanding these states
is crucial for debugging concurrent programs. You can check a
thread's current state using the t.getState() method.
new operator (e.g.,
Thread t = new Thread()) but the
start() method has not been called yet.
start(),
the thread becomes runnable. It is ready to be scheduled for
execution by the thread scheduler. Note that "Runnable" doesn't
necessarily mean it is actively running on the CPU at that exact
moment; it could be waiting for its time-slice.
synchronized block or method. It is unblocked when
the lock becomes available.
wait(). It will remain in this state until another
thread wakes it up by calling notify() or
notifyAll().
sleep(milliseconds) or
wait(timeout). It returns to the Runnable state
once the timer expires.
run() method naturally, or it was
forcefully stopped due to an unhandled exception. Once a thread
is dead, it cannot be restarted.
Java natively supports the concept of Monitors. Every object in Java can act as a monitor, meaning it can use a built-in lock to enforce mutual exclusion. This ensures that only one thread can execute a critical section of code at a time.
synchronized Keyword
You apply the synchronized keyword to methods or code
blocks. When a thread invokes a synchronized method, it must first
acquire the intrinsic lock of that object. If another thread
already holds the lock, the new thread is
Blocked.
public class BankAccount {
private double balance = 1000.0;
// Only one thread can execute this method at a time on a given object
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
// Unsynchronized method - multiple threads can call this simultaneously
public double getBalance() {
return balance;
}
}
Sometimes you don't want to lock an entire method, just a small critical section. You can use a synchronized block, explicitly specifying the object whose lock you want to acquire.
public class DataProcessor {
private final Object lockObject = new Object();
public void process() {
System.out.println("Doing some independent work...");
// Acquiring the lock on a specific object
synchronized(lockObject) {
System.out.println("Inside critical section.");
// Shared resource modification goes here
}
}
}
wait() and
notify()
Inside a synchronized area, a thread might realize it cannot
proceed (e.g., trying to withdraw money, but the balance is too
low). Instead of looping endlessly ("busy waiting"), it should
call wait().
wait(): The thread gives up the
lock and enters the Waiting state. It joins the object's
internal waiting queue.
notifyAll(): Wakes up
all threads that are waiting on this object's
lock. They will then compete to re-acquire the lock.
notify(): Wakes up a single,
arbitrary thread from the waiting queue.
public class SharedBuffer {
private boolean dataAvailable = false;
public synchronized void produce() {
dataAvailable = true;
System.out.println("Data produced!");
notifyAll(); // Wake up consumers waiting for data
}
public synchronized void consume() {
// Always use a while loop for wait() to handle spurious wakeups
while (!dataAvailable) {
try {
wait(); // Release the lock and wait
} catch (InterruptedException e) {
System.out.println("Thread interrupted.");
}
}
System.out.println("Data consumed!");
dataAvailable = false;
}
}
While the synchronized keyword is convenient, Java
provides a more flexible and explicit locking mechanism in the
java.util.concurrent.locks package: the
ReentrantLock.
A ReentrantLock works similarly to a standard object
lock but requires you to manually call lock() and
unlock(). Because you manually acquire it, you
must ensure it gets released, even if an
exception occurs. Therefore, the unlock() call is
almost always placed inside a finally block.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private Lock bankLock = new ReentrantLock();
private double[] accounts = new double[100];
public void transfer(int from, int to, double amount) {
bankLock.lock(); // Acquire the lock
try {
if (accounts[from] < amount) return;
accounts[from] -= amount;
accounts[to] += amount;
} finally {
bankLock.unlock(); // Always release the lock in finally block
}
}
}
A lock is "reentrant" if a thread that already holds the lock can
acquire it again without blocking itself. The lock maintains a
"hold count." Every time the owning thread calls
lock(), the count increases. Every time it calls
unlock(), the count decreases. The lock is only
released to other threads when the count reaches zero.
This is extremely useful when a locked method calls another method that also requires the same lock. Without reentrancy, the thread would deadlock itself.
When working with multiple threads, you need ways to coordinate their execution, pause them, or signal them to stop.
Thread.sleep(milliseconds) causes the currently
executing thread to pause its execution for the specified
duration. This does not release any locks the thread holds. It is
useful for simulating work or pacing an application. Because
another thread might interrupt the sleeping thread, it throws an
InterruptedException.
join() Method
The join() method allows one thread to wait for the
completion of another. If Thread A calls
ThreadB.join(), Thread A will pause execution until
Thread B finishes completely.
public class JoinExample {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
for (int i = 1; i <= 3; i++) {
System.out.println("Worker thread processing: " + i);
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
});
worker.start();
// The main thread will stop here and wait for 'worker' to finish
worker.join();
System.out.println("Worker finished. Main thread resuming.");
}
}
You cannot safely "kill" a thread from the outside in Java
(methods like stop() are deprecated). Instead, you
politely ask it to stop using the interrupt() method.
If a thread is currently blocked in a sleep() or
wait() call, calling interrupt() on it
will immediately wake it up and throw an
InterruptedException. If the thread is executing
normally, calling interrupt() just sets a flag. The
thread itself should periodically check
isInterrupted() and decide how to terminate
gracefully.
public class InterruptExample {
public static void main(String[] args) throws InterruptedException {
Thread task = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
// Periodically check if we have been asked to stop
if (Thread.currentThread().isInterrupted()) {
System.out.println("Interrupted! Terminating early.");
break;
}
System.out.print(i + " ");
}
});
task.start();
Thread.sleep(1); // Let it run for a tiny bit
// Signal the thread to stop
task.interrupt();
}
}
Standard Java collections like ArrayList,
HashMap, and HashSet are
not thread-safe. If multiple threads try to add
or remove elements from a standard ArrayList at the
same time, it can result in corrupted data, infinite loops, or a
ConcurrentModificationException.
While you could manually wrap every collection operation in a
synchronized block, this is error-prone and severely
hurts performance because it prevents any parallel access. To
solve this, Java provides specialized concurrent collections in
the java.util.concurrent package.
ConcurrentHashMap is a highly optimized, thread-safe
alternative to HashMap. Instead of locking the entire
map during an update, it divides the map into segments (or
buckets) and only locks the specific segment being modified. This
allows multiple threads to read and write to different parts of
the map simultaneously without blocking each other.
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ConcurrentMapExample {
public static void main(String[] args) {
// Safe for multiple threads to access simultaneously
Map<String, Integer> scores = new ConcurrentHashMap<>();
scores.put("Alice", 10);
// Thread-safe atomic operation: only puts if absent
scores.putIfAbsent("Bob", 20);
System.out.println(scores);
}
}
CopyOnWriteArrayList is a thread-safe variant of
ArrayList. It achieves thread safety through a
radical approach: every time an element is added, modified, or
removed, it creates a brand new, completely separate copy of the
underlying array.
When to use it: Because copying arrays is expensive, this collection is only suitable when you have many threads reading the list, but very few threads modifying it (e.g., maintaining a list of event listeners). Reading operations never require locks and are extremely fast.
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
public class CopyOnWriteExample {
public static void main(String[] args) {
List<String> activeUsers = new CopyOnWriteArrayList<>();
activeUsers.add("UserA");
activeUsers.add("UserB");
// A thread can safely iterate over this list even if another
// thread adds/removes elements concurrently. The iterator works
// on a snapshot of the array taken at the time iterator() was called.
for (String user : activeUsers) {
System.out.println("Broadcasting to: " + user);
}
}
}
The BlockingQueue interface represents a queue that
is thread-safe and supports operations that wait for the queue to
become non-empty when retrieving an element, and wait for space to
become available in the queue when storing an element. They are
the backbone of the Producer-Consumer pattern.
Common implementations include
ArrayBlockingQueue (backed by a fixed-size array) and
LinkedBlockingQueue.
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
// A queue with a strict limit of 5 items
BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
// put() will block (pause the thread) if the queue is full
queue.put("Task 1");
// take() will block (pause the thread) if the queue is empty
String task = queue.take();
System.out.println("Processing: " + task);
}
}