Back to Home

Threads in Java: architecture and synchronization

The article breaks down the structure of threads in Java from the hardware processor level to JVM. Describes synchronization mechanisms, Race Condition, Deadlock, Livelock problems and asynchronous CompletableFuture for effective development.

Java Multithreading: from CPU cores to Deadlock
Advertisement 728x90

Java Thread Architecture and Synchronization: From CPU to JVM

Java code compiles to bytecode, which the JVM converts into machine instructions for the processor. Each CPU core executes instructions sequentially: a simple operation like bit-shifting in a register happens in one clock cycle. Registers are ultra-fast memory cells for storing numbers, addresses, and data.

Modern processors use superscalar architecture. Hyper-Threading lets a single hardware thread execute 2–6 instructions per cycle, creating the illusion of multiple logical threads per core through rapid context switching.

The number of threads running simultaneously is limited by the core count. Threads share heap memory for objects, while each has its own stack for local variables and object references.

Google AdInline article slot

Race Conditions and Memory Structure

A race condition happens when multiple threads read and modify the same heap object at the same time. The outcome is unpredictable, hinging on the order of read-modify-write operations.

Code often runs fine locally without load, but in production under multithreading, collisions pop up irregularly. An OS process allocates shared memory; threads within it are lightweight execution units competing for resources.

Synchronization Mechanisms: wait/notify and Monitors

Every Java object has a built-in monitor (lock). The synchronized block acquires the object's monitor, ensuring:

Google AdInline article slot
  • Mutual Exclusion: Only one thread executes the code inside the block.
  • Visibility: Changes are visible to other threads after exiting the block (with volatile support).

The monitor includes:

  • Entry Set: Queue of threads waiting to acquire it.
  • Wait Set: Threads in a waiting state.

wait(), notify(), and notifyAll() only work inside a synchronized block on the same object—otherwise, you get IllegalMonitorStateException.

  • wait(): Moves the thread to the Wait Set.
  • notify(): Wakes a random thread (risk of lost wakeup).
  • notifyAll(): Wakes all threads (preferred approach).

Static synchronized methods lock the class monitor (Class), without conflicting with instance methods (this).

Google AdInline article slot

Advanced Locking: ReentrantLock and Semaphore

For more control, use java.util.concurrent.locks.ReentrantLock:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();
}

Key methods:

  • tryLock(): Acquire without waiting (returns false on failure).
  • tryLock(timeout, unit): Wait with a timeout.
  • lockInterruptibly(): Interruptible acquire.
  • new ReentrantLock(true): Fair locking (FIFO order, slower).

Semaphore limits access:

Semaphore sem = new Semaphore(3); // 3 permits
sem.acquire(); // acquire
sem.release(); // release

Multithreading Pitfalls: Deadlock and Livelock

Deadlock: Cyclic lock dependencies. Thread A holds lock1 and waits for lock2; Thread B does the opposite. The system freezes.

Avoid it by:

  • Locking in a fixed order.
  • Using timeouts.
  • Minimizing nested locks.

Livelock: Threads are busy but make no progress. Example: Two threads politely yield resources to each other in a loop, spiking CPU without advancing.

Synchronous vs Asynchronous in Threads

Synchronous methods block the thread on I/O (e.g., DB query): the thread idles until the response.

Asynchronous uses CompletableFuture—a container for deferred results (pending, completed, or failed).

Operation chains:

  • thenApply(): Transform the result.
  • thenCompose(): Compose with another Future.
  • thenAccept(): Consume without return.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "result")
    .thenApply(s -> s.toUpperCase())
    .thenAccept(System.out::println);

This frees the thread for other tasks, boosting throughput in multithreaded environments.

Key Takeaways

  • Each core runs one hardware thread; Hyper-Threading simulates multiples.
  • Race conditions stem from shared heap; synchronization ensures mutual exclusion and visibility.
  • Prefer notifyAll() over notify() to avoid lost wakeups.
  • ReentrantLock extends synchronized with timeouts, interrupts, and fairness.
  • CompletableFuture enables async pipelines without blocking threads.

— Editorial Team

Advertisement 728x90

Read Next