Java Concurrency Fundamentals and Implementation Details

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(), or LockSupport.park()).
  • TIMED_WAITING: Thread is waiting for a specified time (e.g., via Thread.sleep(), timed Object.wait(), or LockSupport.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 Owner field 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 = 1 in 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’s Owner to the WaitSet (WAITING state).
  • 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 in AQS to 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 state to represetn resource availability.
  • A FIFO CLH-style wait queue of Node instances.
  • 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:

  1. Attempt CAS on state (fast path).
  2. On failure, enqueue thread as a Node (lazy initialization; head is a dummy node).
  3. Repeatedly try acquiring if predecessor is head.
  4. If still failing, set predecessor’s waitStatus to SIGNAL (-1) and park via LockSupport.

Lock release flow:

  1. Update state and clear owner.
  2. Call unparkSuccessor(head):
    • Clears head’s waitStatus.
    • Finds the next valid (non-cancelled) node from tail backward (to handle concurrent enqueues where next pointers may be stale).
    • Unparks that thread.

Fair vs. Non-fair Locks:

  • Non-fair (ReentrantLock default): New threads can bypass the queue if the lock is free.
  • Fair: Checks hasQueuedPredecessors() before granting the lock—ensures FIFO order.

Reentrancy:

  • state tracks hold count. Only when decremented to 0 is the lock fully released.

Interruptible Locking:

  • doAcquireInterruptibly() throws InterruptedException immediately upon interruption.
  • Standard acquire() merely records interruption and continues.

Thread Creation Methods

  1. Extend Thread:

    class Worker extends Thread {
        public void run() { /* task */ }
    }
    new Worker().start();
    
  2. Implement Runnable:

    Runnable task = () -> { /* task */ };
    new Thread(task).start();
    
  3. 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
    
  4. 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): Returns Future; exceptions wrapped and rethrown via Future.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.

Tags: java Concurrency multithreading JVM Locks

Posted on Thu, 21 May 2026 22:11:38 +0000 by webmazter