Fundamentals 11 min read

When to Use synchronized vs. Lock: Mastering Java Locks

This article explains Java's lock mechanisms, comparing synchronized and the Lock interface, detailing their usage, implementation via AQS, system‑level primitives, and providing code examples to help developers choose the appropriate synchronization tool.

Youzan Coder
Youzan Coder
Youzan Coder
When to Use synchronized vs. Lock: Mastering Java Locks

1. Using Java Locks

Before Java 5, synchronization was achieved with the synchronized keyword. Since Java SE 5, the java.util.concurrent.locks.Lock interface and its implementations provide explicit lock acquisition and release, offering features such as interruptible lock acquisition, non‑blocking tryLock, timed lock attempts, and multiple waiting queues.

Example of using Lock

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

Note: always release the lock in a finally block to avoid IllegalMonitorStateException if lock acquisition fails.

Interruptible lock acquisition : Lock.lockInterruptibly() throws InterruptedException when the waiting thread is interrupted.

Non‑blocking acquisition : Lock.tryLock() returns false immediately if the lock is not available.

Timed acquisition : Lock.tryLock(long time, TimeUnit unit) attempts to acquire the lock within a given timeout.

Multiple waiting queues can be created on the same object via Condition, supporting fair lock mode.

Lock’s advantage is that most of its implementation is in Java, allowing developers to study the source code, but it is more complex and slightly slower than synchronized, which benefits from JVM optimizations such as biased locking and lightweight locking.

1.1 synchronized

synchronized

is implemented in the JVM by inserting monitorenter and monitorexit bytecode instructions. Every object has an associated monitor; entering the monitor acquires the lock, exiting releases it. The lock state is stored in the object header (two words for normal objects, three for arrays).

JOL (Java Object Layout) can be used to print object‑header information.

1.2 Lock Upgrade

Since Java 1.6, the JVM introduces biased locking, lightweight locking, and heavyweight locking to reduce the cost of lock acquisition and release. Locks transition from no‑lock → biased → lightweight → heavyweight, and upgrades are one‑way.

Locks can be upgraded but not downgraded.

2. JDK Lock Implementations

The Lock interface is built on top of AbstractQueuedSynchronizer (AQS), which uses an int state and a FIFO queue to manage thread contention. AQS provides exclusive and shared acquisition, release, and timeout support, and is the foundation for most JDK concurrency utilities.

AQS separates the lock API (used by developers) from the synchronizer implementation (which handles state, queuing, and thread parking). LockSupport is the low‑level utility used by AQS for parking and unparking threads, wrapping native pthread_mutex and pthread_cond primitives on Linux.

LockSupport Methods

// LockSupport
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, nanos);
        setBlocker(t, null);
    }
}

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

3. System‑Level Locks

At the native level, Java’s park / unpark rely on pthread_mutex and pthread_cond. The following C example shows how a thread waits on a condition variable and another thread signals it.

void *r1(void *arg) {
    pthread_mutex_t* mutex = (pthread_mutex_t *)arg;
    static int cnt = 10;
    while(cnt--) {
        printf("r1: I am wait.
");
        pthread_mutex_lock(mutex);
        pthread_cond_wait(&cond, mutex);
        pthread_mutex_unlock(mutex);
    }
    return "r1 over";
}

void *r2(void *arg) {
    pthread_mutex_t* mutex = (pthread_mutex_t *)arg;
    static int cnt = 10;
    while(cnt--) {
        pthread_mutex_lock(mutex);
        printf("r2: I am send the cond signal.
");
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(mutex);
        sleep(1);
    }
    return "r2 over";
}

Java avoids the “thundering herd” problem because it signals only one waiting thread per notify call.

Java calls pthread_cond_signal only once per wake‑up, ensuring a single thread is awakened.

Conclusion

After understanding Java lock mechanisms, prefer synchronized for most synchronization needs; resort to explicit Lock only when synchronized cannot satisfy the requirements.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyThreadLockAQSsynchronized
Youzan Coder
Written by

Youzan Coder

Official Youzan tech channel, delivering technical insights and occasional daily updates from the Youzan tech team.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.