Fundamentals 18 min read

Why Does synchronized Fail with Integer Locks? Deep Dive into Java Concurrency

This article examines why Java's synchronized keyword can become ineffective when locking on an Integer object, explores how autoboxing and Integer caching cause lock objects to change, and presents reliable alternatives such as class‑level locks or explicit lock maps for safe multithreaded programming.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Why Does synchronized Fail with Integer Locks? Deep Dive into Java Concurrency

Hello, I'm Su San. I recently saw a question about the usage of synchronized that I had encountered in an interview three years ago, so I share the code and analysis with you.

public class SynchronizedTest {

    public static void main(String[] args) {
        Thread why = new Thread(new TicketConsumer(10), "why");
        Thread mx = new Thread(new TicketConsumer(10), "mx");
        why.start();
        mx.start();
    }
}

class TicketConsumer implements Runnable {

    private volatile static Integer ticket;

    public TicketConsumer(int ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
            synchronized (ticket) {
                System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
                if (ticket > 0) {
                    try {
                        // simulate delay
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
                } else {
                    return;
                }
            }
        }
    }
}

The program simulates a ticket‑grabbing process with 10 tickets and two threads. The shared ticket variable is used as the lock object inside synchronized. The expected result is that each ticket is taken by only one thread.

In practice the logs show that both threads can acquire the lock for ticket number 9, which means synchronized appeared to fail.

Why didn’t it work?

During the first round the lock works correctly. In the second round the two threads each acquire a different monitor address, so the lock is effectively broken. Using jstack we can see the monitor IDs:

First dump: mx is BLOCKED waiting for lock 0x000000076c07b058; why holds that lock and is TIMED_WAITING.

Second dump: both why and mx are TIMED_WAITING, holding different locks ( 0x000000076c07b058 and 0x000000076c07b048 respectively).

The change of lock objects is caused by the fact that ticket is an Integer. Each time ticket-- is executed, autoboxing creates a new Integer instance (or retrieves one from the Integer cache). Even when the value stays within the cache range (‑128 to 127), the original and the decremented values are distinct objects, so the monitor associated with the lock changes.

Who moved my lock?

Because the lock object changes, the two threads can synchronize on different monitors and both enter the critical section. A simple fix is to lock on a stable object, e.g. the class object:

synchronized (TicketConsumer.class) {
    // critical section
}

This guarantees a single lock for all threads.

Why does Integer caching matter?

When the ticket count exceeds the cache range (e.g., 200), each decrement creates a brand‑new Integer instance, leading to completely different lock objects from the very first iteration. When the count is within the cache (e.g., 10), the same cached object is reused for the initial value, but the decremented value is still a separate instance, so the lock still changes after the first decrement.

The root cause is the bytecode instruction Integer.valueOf used during autoboxing, which returns cached objects only for values in the range, otherwise creates new objects.

What if I really need to lock on an Integer?

One approach is to maintain a ConcurrentHashMap<Integer, Object> that maps each integer to a unique lock object, using putIfAbsent to ensure only one instance per key. Example:

private static final ConcurrentHashMap<Integer, Object> locks = new ConcurrentHashMap<>();

public static Object getLock(Integer id) {
    return locks.computeIfAbsent(id, k -> new Object());
}

Then synchronize on getLock(id). In distributed systems a Redis‑based lock is often preferred.

Conclusion

Do not use Integer (or any mutable boxed primitive) as a lock object because its identity can change due to autoboxing and caching, breaking mutual exclusion. Use a stable object such as a class literal, a dedicated lock instance, or a map‑based lock registry.

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.

concurrencylockingmultithreadingsynchronizedinteger
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.