Thread States in Java
The Thread.State enum defines six states from the Java language perspective:
- NEW: Thread created but not yet started.
- RUNNABLE: Thread is executing or ready to run.
- BLOCKED: Thread is waiting for a monitor lock to enter a synchronized block/method.
- WAITING: Thread is waiting indefinitely for another thread to perform a particular action (e.g., via
Object.wait(),Thread.join(), orLockSupport.park()). - TIMED_WAITING: Thread is waiting for a specified time (e.g., via
Thread.sleep(), timedObject.wait(), orLockSupport.parkNanos()). - TERMINATED: Thread has completed execution.
Monitor Mechanism
Every Java object can be associated with a Monitor (also called a heavyweight lock). The Monitor’s address is stored in the object header’s mark word, specifically in the ptr_to_heavyweight_monitor field.
When a thread acquires a synchronized lock on an object:
- The Monitor’s
Ownerfield is set to that thread. - Other threads attempting to acquire the same lock are placed in the EntryList (a blocking queue).
- Threads that previously held the lock but called
wait()are moved to the WaitSet.
Upon releasing the lock, the owning thread wakes one or more threads from the EntryList to compete for the lock (non-fair by default).
JVM Lock Optimization Techniques
Lightweight Locking
Each thread’s stack frame contains lock records that store the original mark word of locked objects. When acquiring a lock:
- The JVM attempts a CAS operation to replace the object’s mark word with a pointer to the lock record.
- Success indicates lightweight locking (mark word state =
00). - On reentrancy, additional lock records are added without further CAS.
On unlock:
- If the topmost lock record is null, it indicates reentrancy—decrement the count.
- Otherwise, restore the original mark word via CAS. Failure implies the lock has been inflated to heavyweight.
Lock Inflation
If multiple threads contend for a lightweight lock, it inflates to a heavyweight Monitor:
- A Monitor object is allocated.
- The object header is updated to point to this Monitor.
- Contending threads are enqueued in the EntryList.
Unlocking a heavyweight lock involves resetting the Monitor’s Owner to null and waking a thread from the EntryList.
Spin Locks
To reduce OS-level blocking overhead, threads may spin (busy-wait) briefly before blocking. Since Java 6, spin duration is adaptive—based on prior success. Note: spinning is only beneficial on multi-core systems and is no longer user-configurable after Java 7.
Biased Locking
Optimizes for single-threaded lock ownership:
- The mark word stores the thread ID instead of performing repeated CAS operations.
- Enabled when
biased_lock = 1in the mark word.
Biased locks are revoked when:
- Another thread attempts to acquire the lock (upgrades to lightweight).
hashCode()is called (requires space in mark word).wait()/notify()is invoked (requires Monitor support).
Thread Coordination Primitives
Object.wait() and Object.notify()
- Must be called within a synchronized block.
wait()moves the thread from the Monitor’sOwnerto the WaitSet (WAITINGstate).notify()wakes one thread from WaitSet;notifyAll()wakes all.- Woken threads re-enter the EntryList to recompete for the lock.
LockSupport
Provides low-level thread parking:
Thread worker = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) {}
LockSupport.park(); // blocks unless permit available
}, "worker");
worker.start();
Thread.sleep(200);
LockSupport.unpark(worker); // issues permit
Each thread has a Parker with a _counter:
unpark()sets_counter = 1.park()decrements_counter; if it becomes negative, the thread blocks.- Permits can be issued before parking—enabling reliable wake-up.
Interaction with interruption:
interrupt()sets the thread’s interrupt status.park()checks this status; if set, it returns immediately without blocking.Thread.interrupted()clears the status and returns its value—used inAQSto detect interruption during blocking.
Atomic Operations and AQS
Compare-and-Swap (CAS)
CAS (compareAndSet) is implemented via CPU instructions like lock cmpxchg. It atomically updates a memory location only if its current value matches an expected one. The lock prefix ensures atomicity across cores by asserting bus locking.
AbstractQueuedSynchronizer (AQS)
AQS is the foundation for locks like ReentrantLock. It uses:
- An integer
stateto represetn resource availability. - A FIFO CLH-style wait queue of
Nodeinstances. - Condition variables analogous to Monitor’s WaitSet.
Key methods subclasses must implement:
tryAcquire(int)/tryRelease(int)— for exclusive mode.tryAcquireShared(int)/tryReleaseShared(int)— for shared mode.
Lock acquisition flow:
- Attempt CAS on
state(fast path). - On failure, enqueue thread as a
Node(lazy initialization; head is a dummy node). - Repeatedly try acquiring if predecessor is head.
- If still failing, set predecessor’s
waitStatustoSIGNAL (-1)and park viaLockSupport.
Lock release flow:
- Update
stateand clear owner. - Call
unparkSuccessor(head):- Clears head’s
waitStatus. - Finds the next valid (non-cancelled) node from tail backward (to handle concurrent enqueues where
nextpointers may be stale). - Unparks that thread.
- Clears head’s
Fair vs. Non-fair Locks:
- Non-fair (
ReentrantLockdefault): New threads can bypass the queue if the lock is free. - Fair: Checks
hasQueuedPredecessors()before granting the lock—ensures FIFO order.
Reentrancy:
statetracks hold count. Only when decremented to0is the lock fully released.
Interruptible Locking:
doAcquireInterruptibly()throwsInterruptedExceptionimmediately upon interruption.- Standard
acquire()merely records interruption and continues.
Thread Creation Methods
-
Extend
Thread:class Worker extends Thread { public void run() { /* task */ } } new Worker().start(); -
Implement
Runnable:Runnable task = () -> { /* task */ }; new Thread(task).start(); -
Implement
Callable(with result and exception support):Callable<Integer> task = () -> { int sum = 0; for (int i = 0; i < 100; i++) sum += i; return sum; }; FutureTask<Integer> future = new FutureTask<>(task); new Thread(future).start(); Integer result = future.get(); // blocks until done -
Use
ExecutorService(thread pools):- Core pool size: minimum threads kept alive.
- Work queue: holds tasks when all core threads are busy.
- Max pool size: additional threads created if queue is full (for bounded queues).
- Rejection policies:
AbortPolicy,CallerRunsPolicy, etc. - Idle excess threads terminate after
keepAliveTime.
execute() vs submit():
execute(Runnable): No return value; exceptions propagate directly.submit(Runnable|Callable): ReturnsFuture; exceptions wrapped and rethrown viaFuture.get().
Memory Visibility and volatile
Without synchronization, threads may cache variable values in CPU registers or local caches, leading to visibility issues.
The volatile keyword ensures:
- Reads: Always fetch the latest value from main memory.
- Writes: Immediately flush to main memory.
- Memory barriers: Prevent compiler/CPU reordering across volatile accesses.
Correct Singleton Example:
public class Singleton {
private static volatile Singleton INSTANCE;
public static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
The volatile modifier prevents reordering of the instance assignment, ensuring safe publication.