3 min read

Java Multithreading & Concurrency Mechanics

Multithreading allows Java applications to execute multiple tasks concurrently, maximizing CPU core utilization and hardware throughput. However, concurrent execution introduces complex challenges: shared mutable state, race conditions, memory visibility issues, and deadlocks. To build concurrent systems, developers must understand thread lifecycles, JVM object monitor locks, and inter-thread communication mechanisms.

In this comprehensive guide, we analyze thread state transitions, dissect JVM object monitor mechanics (EntrySet vs. WaitSet), contrast object-level vs. class-level locks, and detail deadlock prevention strategies.


1. Java Thread Lifecycle & State Machine

In Java, a thread is represented by an instance of java.lang.Thread. The JVM maps Java threads to operating system kernel threads (1:1 threading model). A Java thread transitions across six explicit states defined in java.lang.Thread.State.


stateDiagram-v2
    [*] --> NEW: new Thread()
    NEW --> RUNNABLE: thread.start()
    RUNNABLE --> BLOCKED: Waiting for synchronized monitor lock
    BLOCKED --> RUNNABLE: Lock acquired
    RUNNABLE --> WAITING: wait() / join() / LockSupport.park()
    WAITING --> RUNNABLE: notify() / notifyAll() / unpark()
    RUNNABLE --> TIMED_WAITING: Thread.sleep(ms) / wait(ms)
    TIMED_WAITING --> RUNNABLE: Time elapsed / interrupted
    RUNNABLE --> TERMINATED: Run method completes or uncaught exception

Thread States Explained

  • NEW: The Thread object has been instantiated, but start() has not yet been invoked.
  • RUNNABLE: The thread is executing inside the JVM. It may be actively consuming CPU cycles or waiting for operating system CPU scheduler dispatch.
  • BLOCKED: The thread is waiting to acquire a JVM object monitor lock to enter or re-enter a synchronized block/method.
  • WAITING: The thread is waiting indefinitely for another thread to perform a specific action (e.g., calling Object.wait() or Thread.join()).
  • TIMED_WAITING: The thread is waiting for a specified timeout period (e.g., Thread.sleep(1000) or Object.wait(5000)).
  • TERMINATED: Execution has finished or terminated due to an uncaught exception.

2. JVM Object Monitors: EntrySet vs. WaitSet

Every object in Java has an associated intrinsic lock or monitor lock. The monitor controls mutual exclusion for synchronized blocks.


flowchart LR
    subgraph ObjectMonitor["JVM Object Monitor Structure"]
        Owner["Owner Thread\n(Currently executing synchronized code)"]
        EntrySet["EntrySet Queue\n(Threads waiting to acquire monitor lock - BLOCKED)"]
        WaitSet["WaitSet Queue\n(Threads that called wait() - WAITING)"]
    end

    Threads["Arriving Threads"] --> EntrySet
    EntrySet --> Owner
    Owner -- "wait()" --> WaitSet
    WaitSet -- "notify()" --> EntrySet

Inter-Thread Communication with wait(), notify(), and notifyAll()

  1. wait(): Executed by the thread currently owning the monitor. The thread immediately releases the lock, transitions to the WAITING state, and moves into the monitor's WaitSet queue.
  2. notify(): Selects an arbitrary thread from the monitor's WaitSet and moves it to the EntrySet. The woken thread transitions to BLOCKED and competes to re-acquire the monitor lock.
  3. notifyAll(): Moves all threads from the WaitSet to the EntrySet, preventing thread starvation bugs.

3. Object-Level Lock vs. Class-Level Lock

Synchronization scope depends on the target instance or reference used for locking.

public class SharedResource {

    // 1. Object-Level Lock (Locks current instance 'this')
    public synchronized void instanceMethod() {
        // Only ONE thread can execute this method per SharedResource object instance
    }

    public void blockInstanceMethod() {
        synchronized(this) { // Explicit object-level lock
            // Critical section
        }
    }

    // 2. Class-Level Lock (Locks java.lang.Class object in Metaspace)
    public static synchronized void staticMethod() {
        // Locks SharedResource.class; blocks ALL threads across ALL object instances!
    }

    public void blockClassMethod() {
        synchronized(SharedResource.class) { // Explicit class-level lock
            // Critical section across all instances
        }
    }
}

4. Deadlocks & Concurrency Pitfalls

A deadlock occurs when two or more threads are blocked forever, each waiting for a lock held by the other.


flowchart LR
    ThreadA["Thread A\n(Holds Lock 1)"] -- "Waiting for Lock 2" --> Lock2["Lock 2"]
    ThreadB["Thread B\n(Holds Lock 2)"] -- "Waiting for Lock 1" --> Lock1["Lock 1"]
    Lock1 --> ThreadA
    Lock2 --> ThreadB

Deadlock Prevention Rules

  • Global Lock Ordering: Always acquire multiple locks in a strict, uniform sequence across all threads.
  • Lock Timeouts: Use explicit lock primitives like java.util.concurrent.locks.ReentrantLock.tryLock(timeout, timeUnit) instead of intrinsic synchronized blocks to prevent indefinite blocking.
  • Daemon Threads: Use daemon threads (thread.setDaemon(true)) for background tasks (e.g., GC monitoring or cleanup), as the JVM terminates automatically when all non-daemon user threads finish.

Related Guides:
← PreviousJava Exception Handling: Hierarchy & PerformanceNext →Java Collections Framework: Lists, Maps & Trees