Java Lock Optimization Techniques and Thread Safety Mechanisms

Use‑Level Lock Optimization

Reduce Lock Holding Time

Code that does not require synchronization should be kept outside the critical section so that the lock is released as quickly as possible.

Reduce Lock Granularity

The idea is to split a single physical lock into multiple logical locks, increasing parallelism and reducing contention. This is a classic space‑for‑time trade‑off. Many Java data structures, notably ConcurrentHashMap, apply this approach.
Before JDK 1.8, ConcurrentHashMap used an array of Segment<K,V>. Each Segment extended ReentrantLock and contained its own HashEntry<K,V> array. A put operation only locked the target segment, leaving other segments accessible, so multiple threads could write concurrently to different segments.

Lock Coarsening

While minimising lock granularity is usually desired, sometimes enlarging its beneficial. If a loop body repeatedly acquires and releases the same lock, moving the lock outside the loop avoids repeated context switches and improves throughput dramatically.

Use Read‑Write Locks

ReentrantReadWriteLock allows concurrent reads under the read lock while writes are exclusive under the write lock.

Use CAS (Compare‑And‑Swap)

When the synchronised operation completes very quickly and thread contention is low, volatile + CAS can outperform traditional locking because it avoids thread context switches. If a context switch costs more than the operation itself, CAS is a highly efficient choice.

System‑Level Lock Optimization

Adaptive Spinning

Spinning avoids the overhead of blocking and waking a thread, which involves costly user‑mode/kernel‑mode transitions. However, if a lock is held for a long time, spinning wastes CPU. The default spin count is 10. Adaptive spinning goes further: the spin count is no longer fixed but is determined by the previous spin duration on the same lock and the state of the lock owner, mitigating the drawbacks of simple spinning.

Lock Elimination

Lock elimination is a JIT compiler optimisation that removes synchronisation on objects that are detected to be free of data races. For example, Netty’s pipeline design allows lock elimination for ChannelHandler instances that are never shared.

Lock Upgrading

Biased Lock

If a thread already holds a lock, subsequent re‑acquisition is extremely fast because it avoids heavy monitor operations. Since most lock acquisitions are uncontended, biased locking boosts performance.

Lightweight Lock

Under low contention, CAS‑based lightweight locks avoid thread context switches and can significantly improve performance.

Heavyweight Lock

The overhead of acquiring and releasing a heavyweight lock is fixed. It is suitable for high contention, high concurrency, and long‑running critical sections.

ThreadLocal Internals

Introduction

Normally, variables can be accessed and modified by any thread. To give each thread its own private copy, JDK provides ThreadLocal. It is conceptually similar to TLAB (Thread‑Local Allocation Buffer) in operating systems.

How It Works

ThreadLocal is a generic class. Because a thread can have multiple ThreadLocal objects, ThreadLocal internally maintains a static inner class called ThreadLocalMap. The actual value is stored in the current thread’s ThreadLocalMap, not inside ThreadLocal itself. ThreadLocal acts as a wrapper around ThreadLocalMap, delegating the value. The get() and set() methods simply forward to the corresponding ThreadLocalMap methods.

Examples:

1. Holding a user session:

private static final ThreadLocal<Session> currentSession = new ThreadLocal<>();

2. Solving SimpleDateFormat thread‑safety issues:

private static final ThreadLocal<DateFormat> dateFormatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

ThreadLocal Memory Leak

In ThreadLocalMap, the key is a weak reference to ThreadLocal, while the value is a strong reference. A weakly reachable object is collected at the next GC. Thus, if a ThreadLocal is no longer strongly referenced externally, its key is cleared. However, the value remains strongly referenced and cannot be collected. This leaves behind entries with null keys. If the thread lives long, these values may never be reclaimed, causing a memory leak.

ThreadLocal reference diagram

ThreadLocalMap already mitigates this: set(), get() and remove() calls proactively purge entries with null keys. A leak can only occur if such entries are created but none of these methods are called afterwards. Therefore, always invoke remove() manually after using a ThreadLocal.

HashMap Thread Safety

Infinite Loop Causing 100% CPU

HashMap can enter an infinite loop and consume 100% CPU during resize (internal rehashing). Resizing reverses the order of nodes inside a bucket. When multiple threads resize simultaneously, two reversals can create a circular linked list, e.g., A→B and B→A. Subsequent lookups on that key traverse the list forever. Therefore HashMap is not thread‑safe. In concurrent scenarios prefer ConcurrentHashMap, which is both safe and performant.

Why String Is Immutable

  1. Enables the string constant pool; identical string literals share the same memory.
  2. Makes String a natural fit as a HashMap key; immutable objects are recommended for map keys.
  3. The hashCode is computed once and never changes, avoiding recalculation.
  4. Immutable objects are inherently thread‑safe.

Tags: java Concurrency lock optimization ThreadLocal hashmap

Posted on Wed, 28 Oct 2026 16:21:48 +0000 by sethcox