Java Synchronization Primitives: synchronized, wait/notify, and Locks

Understanding Java synchronization: synchronized blocks, wait/notify, ReentrantLock, ReadWriteLock, StampedLock, and common concurrency patterns.

published: reading time: 22 min read author: GeekWorkBench

Java Synchronization: synchronized, wait/notify, and Lock Implementations

Java gives you multiple tools for synchronization, and most developers reach for synchronized without thinking twice. Sometimes that’s the right call. Sometimes you’re leaving performance on the table or creating deadlock risks you didn’t need to take. Let’s break down what each primitive actually does under the hood.

Introduction

Java offers multiple synchronization mechanisms — synchronized blocks, wait()/notify(), and the java.util.concurrent.locks hierarchy (ReentrantLock, ReadWriteLock, StampedLock) — and most developers reach for synchronized without considering the trade-offs. The choice matters more than it first appears: wrong synchronization primitives cause deadlocks, thread starvation, priority inversion, and performance bottlenecks that are notoriously difficult to reproduce in testing. Understanding what each primitive actually does to the JVM’s internal monitors and memory ordering guarantees is essential for writing correct high-concurrency code.

The core difference between synchronized and the explicit Lock implementations is that synchronized is non-interruptible, non-timeoutable, and locks arbitrarily, while ReentrantLock gives you tryLock() with timeout, interruptibility, and fair scheduling policies. ReadWriteLock and StampedLock go further by allowing concurrent read access — something synchronized cannot do. This post dissects each primitive, explains the monitor mechanics and happens-before guarantees behind them, and shows the concurrency patterns where each excels.

The synchronized Keyword

synchronized is the oldest synchronization mechanism in Java. It provides mutual exclusion on an object monitor and establishes happens-before relationships.

public class Counter {
    private long count = 0;

    // Instance method - locks on 'this'
    public synchronized void increment() {
        count++;
    }

    public synchronized long getCount() {
        return count;
    }

    // Static method - locks on the Class object
    public static synchronized void incrementStatic() {
        // ...
    }

    // Explicit block - more control over what you're locking
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }
}

What synchronized Guarantees

  1. Mutual exclusion: Only one thread can execute the synchronized block at a time
  2. Visibility: Changes made before exiting a synchronized block are visible to the next thread entering a synchronized block on the same monitor
  3. Atomicity: Compound actions within synchronized blocks are atomic

Limitations of synchronized

  • No try-lock capability
  • No fairness policy
  • Cannot be interrupted while waiting
  • Only works within the same JVM

wait() and notify()/notifyAll()

These are the classic Java coordination mechanisms. They require holding the monitor lock and are used for condition-based waiting.

public class BlockingQueue<E> {
    private final Queue<E> queue = new LinkedList<>();
    private final int capacity;

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    public synchronized void put(E element) throws InterruptedException {
        while (queue.size() == capacity) {
            // Releases lock and waits until notified
            wait();
        }
        queue.add(element);
        // Wake up one waiting thread
        notifyAll(); // Always use notifyAll() unless you're certain only one thread cares
    }

    public synchronized E take() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        E element = queue.remove();
        notifyAll(); // Signal that space is now available
        return element;
    }
}

Why always notifyAll()?

Using notify() is risky. If multiple threads are waiting for different conditions, you might wake the wrong thread:

// RISKY - using notify()
synchronized (lock) {
    if (conditionA) {
        // Only one waiting thread cares about conditionA
        notify();
    }
    if (conditionB) {
        // But what if a thread waiting for conditionB gets woken?
    }
}

// SAFER - use notifyAll() when in doubt
synchronized (lock) {
    conditionA = true;
    conditionB = true;
    notifyAll(); // Everyone wakes up, each checks what they care about
}

The Spurious Wakeup Problem

Always use while() loops around wait(), not if():

// BAD - susceptible to spurious wakeups
if (queue.isEmpty()) {
    wait();
}

// GOOD - loop protects against spurious wakeups
while (queue.isEmpty()) {
    wait();
}

Spurious wakeups can occur on some platforms where the JVM wakes a thread without the usual notify() call.

Lock Implementations

The java.util.concurrent.locks package provides more flexible locking.

ReentrantLock

ReentrantLock provides the same mutual exclusion as synchronized but with additional features:

public class Counter {
    private final ReentrantLock lock = new ReentrantLock();
    private long count = 0;

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock(); // Always in finally - don't let exceptions leak the lock
        }
    }

    public long getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    // Try-lock with timeout - doesn't block forever
    public boolean tryIncrementWithTimeout(long timeout, TimeUnit unit)
            throws InterruptedException {
        boolean acquired = lock.tryLock(timeout, unit);
        if (acquired) {
            try {
                count++;
                return true;
            } finally {
                lock.unlock();
            }
        }
        return false;
    }
}

Fairness Policy

ReentrantLock has a fair constructor parameter:

// Fair lock - threads acquire in order of request
ReentrantLock fairLock = new ReentrantLock(true);

// Unfair lock (default) - better throughput, but starvation possible
ReentrantLock unfairLock = new ReentrantLock(false);

Fair locks hand the lock to the longest-waiting thread. Unfair locks let new threads “barge in” if the lock is available, which is faster but can starve waiting threads.

ReadWriteLock

Separate read and write locks let multiple readers proceed concurrently:

public class Cache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public V get(K key) {
        rwLock.readLock().lock();
        try {
            return cache.get(key);
        } finally {
            rwLock.readLock().unlock();
        }
    }

    public void put(K key, V value) {
        rwLock.writeLock().lock();
        try {
            cache.put(key, value);
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

Multiple threads can read simultaneously, but writes require exclusive access.

StampedLock

StampedLock improves on ReadWriteLock with optimistic reads:

public class Point {
    private double x, y;
    private final StampedLock sl = new StampedLock();

    public void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    public double distanceFromOrigin() {
        // Optimistic read - no lock acquired yet
        long stamp = sl.tryOptimisticRead();
        double dx = x;
        double dy = y;

        // Validate that the read was consistent
        if (!sl.validate(stamp)) {
            // Wasn't consistent - get a proper read lock
            stamp = sl.readLock();
            try {
                dx = x;
                dy = y;
            } finally {
                sl.unlockRead(stamp);
            }
        }
        return Math.sqrt(dx * dx + dy * dy);
    }
}

StampedLock can be faster when reads frequently succeed without contention.

Architecture Diagram

Monitor Lock

graph TD
    subgraph MonitorLock
        S1[synchronized] --> S2[Mutual Exclusion]
        S2 --> S3[Happens-Before Guarantee]
    end

Condition Coordination

graph TD
    subgraph ConditionCoordination
        W1[wait] --> W2[Release Monitor]
        W2 --> W3[Wait for notify]
        W3 --> W4[notify/notifyAll]
        W4 --> W5[Re-acquire Monitor]
    end

java.util.concurrent Locks

graph TD
    subgraph java.util.concurrent
        R1[ReentrantLock] --> R2[Try-lock Support]
        R2 --> R3[Timeout Support]
        R2 --> R4[Fairness Configurable]

        RW1[ReadWriteLock] --> RW2[Concurrent Reads]
        RW2 --> RW3[Exclusive Writes]

        ST1[StampedLock] --> ST2[Optimistic Reads]
        ST2 --> ST3[Write Lock]
        ST2 --> ST4[Read Lock]
    end

Production Failure Scenarios

Scenario 1: Lock Leak via Exception

// BROKEN - lock never released if doSomething throws
public void brokenMethod() {
    lock.lock();
    doSomething(); // If this throws, unlock() never called!
    lock.unlock();
}

// FIXED - always unlock in finally
public void fixedMethod() {
    lock.lock();
    try {
        doSomething();
    } finally {
        lock.unlock();
    }
}

Scenario 2: Deadlock via Nested Locks

// BROKEN - deadlock if otherMethod also locks in opposite order
public void methodA() {
    synchronized (lockA) {
        synchronized (lockB) {
            doSomething();
        }
    }
}

public void methodB() {
    synchronized (lockB) { // If thread1 holds lockB and thread2 holds lockA...
        synchronized (lockA) { // ...both wait forever
            doSomethingElse();
        }
    }
}

// FIX: Always acquire locks in the same order
public void methodA() {
    synchronized (lockA) {
        synchronized (lockB) { // Same order as methodB
            doSomething();
        }
    }
}

Scenario 3: Lost Wakeup with notify()

// BROKEN - signal can be missed
public void waitForSignal() {
    synchronized (lock) {
        while (!signalReceived) {
            lock.wait();
        }
        // Process signal
    }
}

public void sendSignal() {
    synchronized (lock) {
        signalReceived = true;
        lock.notify(); // If no thread is waiting, this is lost!
    }
}

// FIX: Always use notifyAll() or a queue of signals
public void sendSignal() {
    synchronized (lock) {
        signalReceived = true;
        lock.notifyAll(); // All waiting threads wake up and check the condition
    }
}

Trade-off Table

PrimitiveTry-LockTimeoutFairnessCondition SupportPerformance
synchronizedNoNoNowait/notifyGood
ReentrantLockYesYesConfigurableCondition.await()Good
ReadWriteLockYesYesConfigurableRead/Write conditionsGood for read-heavy
StampedLockYesYesNoRead/Write/OptimisticBest for read-heavy

Implementation Snippets

Condition Variables with ReentrantLock

public class BoundedBuffer<T> {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await(); // Wait until space available
            }
            queue.add(item);
            notEmpty.signal(); // Signal that item available
        } finally {
            lock.unlock();
        }
    }

    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await(); // Wait until item available
            }
            T item = queue.remove();
            notFull.signal(); // Signal that space available
            return item;
        } finally {
            lock.unlock();
        }
    }
}

Upgrading ReadLock to WriteLock

public class UpgradableExample {
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
    private String data = "initial";

    // Read operation
    public String read() {
        rwLock.readLock().lock();
        try {
            return data;
        } finally {
            rwLock.readLock().unlock();
        }
    }

    // Write operation - must upgrade
    public void write(String newData) {
        rwLock.writeLock().lock();
        try {
            data = newData;
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Careful upgrade (requires write lock first)
    public boolean updateIf(String expected, String newData) {
        rwLock.writeLock().lock();
        try {
            if (expected.equals(data)) {
                data = newData;
                return true;
            }
            return false;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
}

Observability Checklist

  • Are all lock acquisitions matched with try-finally?
  • Is lock acquisition order consistent across all code paths?
  • Are you using tryLock() with timeouts to prevent deadlocks?
  • Do you use notifyAll() instead of notify() for condition variables?
  • Are conditions checked in while() loops, not if() statements?
  • Can you identify lock contention via thread dumps?
  • Are you using fair locks only when necessary (they’re slower)?

Security Notes

Lock-related security issues:

  • Denial of service: Lock contention can be exploited to slow down critical sections
  • Lock ordering deadlocks: Can be triggered intentionally by attackers
  • Timing attacks: Lock acquisition timing can leak information about operation patterns

When NOT to Use Synchronization Primitives

Synchronization primitives solve real problems, but they bring complexity that is not always necessary. If you are reaching for ReentrantLock when synchronized would work, you are writing more code for the same guarantee and more chances to forget to unlock when exceptions fire. ReadWriteLock sounds appealing for read-heavy workloads, but it adds bookkeeping overhead on every read — if writes are frequent or reads are cheap, the lock manager costs more than the concurrency you get.

Over-synchronization kills performance. When every method in a class acquires a lock, even reads serialize. The JVM’s bias and thin lock optimizations help under low contention, but once threads start competing, they queue up and your parallelism evaporates. If profiling shows high lock contention, consider splitting your data structure into separate synchronized components or switching to lock-free atomics. StampedLock and java.util.concurrent.atomic exist precisely for read-heavy scenarios where you want concurrency without the serialization of exclusive locks.

Do not use locks when a higher-level construct already handles your problem. BlockingQueue gives you producer-consumer coordination with built-in blocking — layering raw wait() and notify() on top of it creates bugs for no gain. If you find yourself thinking about monitor ownership, spurious wakeups, or lock ordering, check whether java.util.concurrent already solved it. See java.util.concurrent: Thread Pools, ForkJoinPool, Synchronizers for the higher-level tools that make raw synchronization a last resort.

Common Pitfalls / Anti-Patterns

  1. Forgetting to unlock: Always use try-finally
  2. Locking at wrong granularity: Too coarse wastes parallelism, too fine risks deadlock
  3. Ignoring starvation: Unfair locks improve throughput but can starve threads
  4. Calling wait() without holding the lock: Throws IllegalMonitorStateException
  5. Not handling InterruptedException: Always restore interrupt status or propagate
  6. Condition variables with synchronized: You can’t use wait/notify with Lock

Quick Recap Checklist

  • synchronized provides mutual exclusion + happens-before
  • wait()/notify() require holding the monitor lock
  • Always use while() loops around wait() to handle spurious wakeups
  • ReentrantLock offers tryLock(), timeout, and fairness options
  • ReadWriteLock separates read and write access for better concurrency
  • StampedLock adds optimistic read mode for read-heavy workloads
  • Always release locks in finally blocks
  • Lock ordering matters - same order everywhere prevents deadlock

Interview Questions

1. What's the difference between synchronized and ReentrantLock?

Synchronized is simpler and automatically releases locks when exceptions occur. ReentrantLock requires manual unlock() in a finally block, but offers tryLock() with timeout, interruptible lock acquisition, and configurable fairness policies.

ReentrantLock also supports multiple condition variables via newCondition(), while synchronized uses wait()/notify()/notifyAll() on the object monitor. For most cases, synchronized is sufficient and clearer. Use ReentrantLock when you need the extra features.

2. Why should wait() always be called in a while loop?

Two reasons: spurious wakeups and multiple consumers.

Spurious wakeups occur when a thread wakes up without being notified. This is platform-dependent but real. Using while() ensures the condition is actually true before proceeding.

Multiple consumers is the more common case. When notifyAll() wakes all waiting threads, each must re-check the condition. If only one item is available and three threads wake up, only one should take it. The others loop back to wait() when they find the queue is empty.

3. What is a deadlock and how do you prevent it?

A deadlock occurs when two or more threads are waiting for each other indefinitely. Classic example: thread A holds lock X and needs lock Y, while thread B holds lock Y and needs lock X.

Prevention strategies include:

  • Lock ordering: Always acquire locks in the same order everywhere
  • Try-lock with timeout: Give up if you can't get all locks
  • Hold fewer locks: Reduce lock granularity
  • Lock-free data structures: Avoid locks entirely when possible
4. When would you use ReadWriteLock over ReentrantLock?

ReadWriteLock makes sense when you have workloads that are read-heavy and write-light. Multiple threads can read simultaneously without blocking each other, which improves throughput. A write still requires exclusive access.

If your reads and writes are roughly equal, or if writes are frequent, ReadWriteLock won't help and adds overhead. StampedLock is even better for read-heavy workloads because it supports optimistic reads that don't lock at all.

5. What is the happens-before relationship with synchronized?

When you exit a synchronized block, all memory operations (reads and writes) that happened in that block become visible to any thread that later enters a synchronized block on the same monitor. This happens-before relationship extends across threads.

It works because the unlock action happens-before the subsequent lock action on the same monitor. Combined with program order within each thread, this chains together to create consistent visibility guarantees across complex thread interactions.

6. What is the difference between notify() and notifyAll() and why is notifyAll() generally safer?

notify() wakes a single waiting thread, but which one is JVM-dependent. If multiple threads are waiting for different conditions on the same monitor, notify() might wake the wrong thread, causing it to wait indefinitely for a condition that will never be true.

notifyAll() wakes all waiting threads. Each then checks its condition in a while() loop and goes back to wait() if the condition is not met. This guarantees that at least one thread capable of proceeding will wake up. Use notify() only when you are certain only one thread is waiting for a specific condition.

7. How does ReentrantLock implement reentrancy?

ReentrantLock tracks which thread currently holds the lock and how many times it has been acquired. When a thread calls lock() on an already-held lock, the hold count increments instead of blocking. The thread must call unlock() the same number of times to release the lock.

This allows a thread to safely re-enter a synchronized block that calls other methods that also require the same lock. Without reentrancy, a thread would deadlock with itself when trying to reacquire a lock it already holds.

8. What is the purpose of StampedLock's optimistic read mode?

StampedLock's tryOptimisticRead() returns a stamp without acquiring a read lock. You then read fields and call validate() to check whether the stamp is still valid (meaning no exclusive write occurred in the meantime).

If validation fails, you upgrade to a proper read lock. Under low-write contention, optimistic reads succeed frequently and let readers proceed without blocking, which is significantly faster than reader locks. If writes are frequent, the validation fails often and you pay the cost of fallback.

9. Why is it dangerous to call wait() outside of a synchronized block?

Calling wait() without owning the monitor throws IllegalMonitorStateException. The wait() method requires that the calling thread holds the object's monitor—obtained via synchronized. Without it, the JVM refuses to release the lock and throws.

This is a common mistake: mistakenly using object.wait() in an unsynchronized context, expecting the thread to wait. It will fail immediately.

10. How does a fair lock differ from an unfair lock in ReentrantLock?

A fair lock ensures that the longest-waiting thread acquires the lock first, in FIFO order. An unfair lock lets a new thread "barge in" and acquire the lock if it is available, even if threads have been waiting longer. Unfair locks generally have better throughput because they avoid the overhead of maintaining a wait queue. However, they can cause thread starvation under continuous contention. Specify fairness via new ReentrantLock(true) for fair, new ReentrantLock(false) or default for unfair.

11. What is biased locking and what are the implications of disabling it in Java?

Biased locking optimizes the case where one thread repeatedly enters a synchronized block on the same object. Instead of atomic operations on the mark word, the JVM biased the object toward that thread and simply checks a flag on lock entry. In Java 6+, biased locking is disabled by default via -XX:-UseBiasedLocking because it adds overhead in highly contented multi-threaded scenarios and can cause performance problems during lock escalation. Disabling it forces the JVM to use thin locks immediately, which performs better under contention.

12. How does Condition.await() differ from Object.wait() in lock-based coordination?

Both suspend a thread until signaled, but Condition.await() is used with explicit Lock objects while Object.wait() requires synchronized. Condition provides more control: you can have multiple independent conditions on the same lock via lock.newCondition(), whereas Object.wait() uses a single monitor condition per object. Condition also supports timed waits with awaitUntil(Date deadline) and await(long time, TimeUnit unit). When using Lock, you MUST use Condition.await() - you cannot call Object.wait() because you do not hold the monitor lock.

13. What is the difference between CountDownLatch and CyclicBarrier as coordination mechanisms?

CountDownLatch is one-shot: the count starts at N, threads call countDown() to decrement, and await() returns when count reaches zero. The latch cannot be reset - once triggered, it stays triggered. CyclicBarrier can be reset and reused: threads call await() at the barrier and all wait until N threads arrive, then all proceed together. If a thread fails to reach the barrier, you get a BrokenBarrierException. Use CountDownLatch for "wait for N tasks to complete", use CyclicBarrier when "N threads must meet at a point before proceeding".

14. What is lock escalation in the JVM and why does it matter for synchronized performance?

Lock escalation is when the JVM escalates from thin lock to fat (monitor) lock when contention increases. A thin lock uses a simple CAS on the object's mark word; if multiple threads compete, the JVM inflates the lock to a heavier monitor lock with awaiting queue. This adds overhead and can cause longer pauses. Under high contention, biased locking (if enabled) saves the first thread the escalation cost, but disabling biased locking avoids that bonus overhead and tail latency on uncontended cases.

15. How does ReentrantReadWriteLock handle read-write conflicts and when does it improve performance?

ReadWriteLock maintains separate read and write locks. Multiple readers can hold the read lock simultaneously when no writer holds the write lock. A writer must acquire an exclusive lock and blocks until all readers release. This improves throughput when reads vastly outnumber writes (e.g., caching scenarios). If reads and writes are roughly equal, or writes are frequent, the overhead of managing read-write coordination can exceed the benefit. Upgrade from read to write lock is not supported directly - you must release the read lock and acquire write lock, which creates a window where another writer could modify.

16. What is a monitorenter and monitorexit and how do they relate to synchronized blocks?

monitorenter and monitorexit are the bytecode instructions for acquiring and releasing a monitor lock. When entering a synchronized block, the JVM executes monitorenter on the target object (or Class for static synchronized). The mark word is updated to indicate the lock is held. On normal exit, monitorexit releases the monitor. If an exception is thrown, the JVM implicitly executes monitorexit via the exception handler. A blocked or waiting thread is parked until the monitor becomes available. The monitor is automatically released when the thread exits the synchronized block or throws an exception.

17. What is the double-checked locking idiom and why does it require volatile to be correct in Java?

Double-checked locking attempts to avoid synchronization overhead for lazy initialization:

if (instance == null) {           // Check 1
    synchronized (lock) {
        if (instance == null) {     // Check 2
            instance = new Singleton();
        }
    }
}

Without volatile, the JVM might reorder the write to instance before the constructor completes. A second thread could see a non-null instance but an uninitialized object. Volatile prevents reordering (happens-before guarantees) for both initialization and subsequent reads. In modern Java with safe publication guarantees, consider using holder pattern or enum singleton instead.

18. How does StampedLock handle the upgrade from read lock to write lock safely?

StampedLock does not support direct upgrade from read to write lock. You must release the read lock first, then acquire the write lock. This creates a race condition where another writer could modify the value between the read and write lock acquisition. StampedLock provides tryConvertToWriteLock() which attempts an optimistic upgrade that succeeds if no write has occurred since the read lock was acquired. This is safer than releasing and re-acquiring but still can fail if concurrent modifications occurred.

19. What are the semantics of Lock.lockInterruptibly() and when should you use it?

lockInterruptibly() acquires the lock like lock(), but allows the thread to be interrupted while waiting. If interrupted, it throws InterruptedException and does not acquire the lock. Use this when your application needs to react to thread interruption - for example, in a thread pool that should shut down gracefully when interrupted. Regular lock() ignores interruption status and acquires the lock regardless. If you do not use lockInterruptibly(), an interrupt just sets the flag and the thread continues waiting, potentially holding the lock indefinitely.

20. What is Phaser and how does it differ from CountDownLatch and CyclicBarrier for multi-phase coordination?

Phaser supports dynamic registration (parties can register and deregister at runtime), multiple phases (each phase is numbered and all threads must complete before the next phase begins), and reusable barriers across phases. Unlike CountDownLatch which is one-shot, Phaser moves through multiple phases. Unlike CyclicBarrier which waits for a fixed number of parties, Phaser supports variable party count. Use Phaser when the number of participants changes between phases or when you have multi-phase workflows like load-process-write where threads need to synchronize at each phase boundary.

Further Reading

Conclusion

You now understand Java’s full suite of synchronization primitives from synchronized to StampedLock. Apply the right tool for your scenario: synchronized for simple mutual exclusion, ReentrantLock when you need try-lock or fairness, ReadWriteLock for read-heavy workloads, and StampedLock for optimistic reads. Always acquire locks in a consistent order to prevent deadlocks, and prefer higher-level constructs from java.util.concurrent wherever they fit. Continue with java.util.concurrent: Thread Pools, ForkJoinPool, Synchronizers to learn how to pool and coordinate threads at scale.

Category

Related Posts

Java Atomics and VarHandle: Low-Level Concurrency

Understanding Java atomic operations: AtomicInteger, AtomicReference, VarHandle, compareAndSet, atomics vs locks, and lock-free programming patterns.

#java #jvm #concurrency

Java Concurrent Collections: ConcurrentHashMap, BlockingQueue

Java concurrent collections deep dive: ConcurrentHashMap, BlockingQueue, CopyOnWriteArrayList, ConcurrentLinkedQueue, and choosing the right structure.

#java #jvm #concurrency

Java Memory Model: Happens-Before, Volatile, and Final Fields

Understanding happens-before guarantees, volatile field semantics, and final field safety in the Java Memory Model for correct concurrent code.

#java #jvm #concurrency