Layers of Logic

14.5

The Lock Every Object Already Has

One keyword fixes all three failures from the last section. The thing it locks is not obvious, and two methods that both say synchronized can be locking completely different objects and protecting nothing.

Core21 min read4 exercises
01

Previously on

Section 14.4 left you with three problems and a keyword that fixes two.

volatile makes writes visible and stops reordering. It cannot make counter++ into one step, because that is three instructions and no keyword changes the count.

What is missing is a way to say: one thread at a time through here.

02

The problem

You need the read, the change and the write to happen without anyone else getting in between.

counter++;      // read, add, write

Nothing about a single variable can give you that. The gap between the read and the write is real. Closing it means stopping other threads from entering the same code while you are in it.

So you need a door with one key. A thread takes the key, does the work, and puts it back. Anyone else who arrives waits.

That raises two questions, and the second one is where people go wrong.

Where does the key live? You could make one:

class Counter {
    private boolean inUse = false;

    void increment() {
        while (inUse) { }          // wait
        inUse = true;              // take it
        counter++;
        inUse = false;             // put it back
    }
}

That is broken for the exact reason you are here. Two threads can both read inUse as false and both set it to true. The lock has a race condition in it.

Locking cannot be built out of ordinary reads and writes. It needs help from below.

And which key protects which data? If two threads use different keys, neither of them waits, and both of them believe they were careful.

03

The idea

Every object in Java has a lock built into it. Not a special class: every object, going back to Object in Section 8.3. It is called a monitor, and until now you have been carrying one around on every object you have ever made without using it.

synchronized (lock) {
    counter++;
}

One thread at a time inside those braces, for that object. Everyone else waits, in the BLOCKED state from Section 14.2.

Verified, the same counter as last section:

synchronized counter: 200000 (wanted 200000)

Exact, every run.

There are three ways to write it, and two of them hide what is being locked.

What you writeWhat is actually locked
synchronized (obj) { }a blockobj. Visible, and you chose it
synchronized void m()an instance methodthis. The whole method body
static synchronized void m()a static methodMyClass.class, the Class object

Those last two are the same keyword and two different locks.

An instance method locks this, so each object has its own. A static method locks the Class object, because there is no instance, and there is exactly one of those per class in the method area from Section 13.1.

The lock is reentrant. A thread already holding it can take it again:

synchronized void outer() { inner(); }
synchronized void inner() { ... }
inner entered while holding the same lock

The monitor remembers which thread holds it and keeps a count. Entering again increases the count, leaving decreases it, and the lock is released at zero.

Without that, outer calling inner would wait forever for a lock it was already holding. It also makes inheritance work: an overridden synchronized method calling super would otherwise deadlock every time.

And a lock fixes visibility too. From the happens-before rules in Section 14.4:

Unlocking a monitor happens-before any later locking of the same monitor.

So everything a thread wrote before releasing is visible to the next thread that acquires. A lock is not only a queue. It is also a promise about what you will see when your turn comes.

That is the whole reason a lock fixes all three problems and volatile fixes two.

04

Under the hood

Going deeper

In the bytecode it is two instructions:

monitorenter
   ... your code ...
monitorexit

For a synchronized method there are no instructions at all. A flag on the method, ACC_SYNCHRONIZED, tells the JVM to take the monitor before the first line and release it after the last. The effect is the same and there is nothing to see in the disassembly.

The release is wrapped in an implicit finally. A method that throws still releases its lock, which is the one thing Thread.stop() got right and everything else about it wrong.

Locks are cheap until they are contended. The JVM does not go to the operating system unless it has to:

How a monitor gets more expensive

  1. Biased, then thinWith no contention, taking the lock is a single compare-and-swap on the object header from Section 13.1. Close to free.
  2. Two threads want itThe loser spins for a short while, on the guess that the holder will finish quickly. Still no operating system involved.
  3. It stays contestedThe lock inflates. The waiting thread is parked by the operating system, which means a context switch, and the mark word now points at a real monitor object.

Which is why “synchronized is slow” is only true under contention. A lock nobody is fighting over costs almost nothing. The fix for a slow lock is usually to hold it for less time, not to remove it.

Deadlock is the failure this creates. Two threads, two locks, opposite order:

// thread 1
synchronized (a) { synchronized (b) { } }

// thread 2
synchronized (b) { synchronized (a) { } }

Thread 1 holds a and wants b. Thread 2 holds b and wants a. Neither will let go, and neither can be interrupted out of it, because a thread waiting on synchronized ignores the flag from Section 14.3.

In a thread dump both threads sit in BLOCKED forever. That is what makes the states in Section 14.2 worth knowing.

The fix is a fixed order. If every thread takes a before b, the cycle cannot form. Ordering locks by something stable, like an account id, is the standard answer.

05

What it costs

The most expensive thing about a lock is that nothing checks you used the right one. Every access to the data has to take the same lock, and the compiler does not know which data a lock is meant to protect. The 145,403 above was two people both writing synchronized and both being wrong.

Holding one too long is the usual performance mistake. A synchronized method that also does a network call has turned a shared resource into a queue. Every thread now waits for the slowest request in the system.

Deadlock is the failure you cannot recover from. There is no timeout, no exception and no way to interrupt out of it. The program stops, and the only evidence is a thread dump full of BLOCKED.

synchronized is also inflexible in ways you notice later. There is no way to try for a lock and give up, and no timeout. You cannot be interrupted while waiting, and you cannot let many readers in while excluding writers. Section 14.7 exists because of that list.

And it is easy to lock the wrong object without any sign. this is the default that a synchronized method gives you, it is public to anyone holding a reference, and it is what most code does.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. `synchronized void update()` on an instance method. What object is being locked?

    Show the answer

    this. The instance the method was called on.

    Which means two different objects have two different locks, and two threads calling update() on two different instances never wait for each other. That is usually what you want and it is worth knowing rather than assuming.

    On a static method the lock is the Class object, MyClass.class, because there is no instance to lock. There is one of those per class, so all static synchronized methods of a class share one lock.

    Those are two different locks. A static synchronized method and an instance synchronized method can run at the same time, on the same data, protecting nothing.

  2. Why can a synchronized method call another synchronized method on the same object without deadlocking?

    Show the answer

    Because the lock is reentrant. It remembers which thread holds it and keeps a count.

    A thread already holding the lock walks straight into another synchronized block on the same object and the count goes up. It is released only when the count reaches zero again.

    Without that, any synchronized method calling another one on the same object would wait forever for a lock it was already holding. Inheritance would make it worse, since an overridden method calling super would deadlock every time.

  3. How does a lock fix visibility, when it looks like it only stops two threads running at once?

    Show the answer

    Because releasing a lock and acquiring it are a happens-before pair, from Section 14.4.

    When a thread releases a monitor, everything it wrote before that point is published. When the next thread acquires the same monitor, it sees all of it.

    So a lock is not only about taking turns. It is also a promise about what you will see when your turn comes, and that is why a lock fixes all three problems while volatile fixes two.

    It only works if both sides use the same lock. A thread reading without locking gets no promise at all, however careful the writer was.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises105 pointsabout 105 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Two Keys, No Protection

Real work·25 min·25 points

ex-14-5-a

Both methods say synchronized. Both authors were careful. Fifty thousand updates are gone.

Run it five times and look at the numbers before working out why. Nothing here is a missing keyword or a forgotten block, which is what makes it worth doing: the mistake is invisible at the point where it is made.

Finish by naming the exact object each method locks. Once you can do that, this whole class of bug stops being possible for you.

What your program must do

  • Run the mixed version five times and record the totals
  • Say which object each of the two methods actually locks
  • Fix it two different ways so both use one lock
  • Say what in the language would have warned you, and what did not
TwoKeys.java
public class TwoKeys {
    static int shared = 0;
    int instanceField = 0;

    static synchronized void staticWay() { shared++; }
    synchronized void instanceWay() { shared++; }

    public static void main(String[] args) throws Exception {
        // TODO: one thread calling staticWay, one calling instanceWay on an object.
        //       Both update `shared`. Both say synchronized. Predict the total.
        // TODO: run it five times
        // TODO: fix it so both use the same lock. Two ways to do that.
        // TODO: say exactly which object each of the two methods locks
    }
}
Hint 1
An instance method locks this. A static method locks the Class object, TwoKeys.class. Those are two different objects, so neither thread ever waits.
Hint 2
One run gave 145403 out of 200000. Both threads were inside synchronized the whole time.
Hint 3almost the answer
The fixes are to make both static synchronized, or to have both use an explicit private static final Object lock. The second is clearer, because the lock is visible in the code rather than implied.
What this is really testing

Whether you know what synchronized locks. Two methods that both say the word can be taking different keys, and neither author will suspect a thing.

B

Deadlock in Ten Lines

Real work·25 min·25 points

ex-14-5-b

Build one on purpose. It takes ten lines and it is the only way to recognise one quickly later.

Add the sleep between the two acquisitions, or you will spend twenty minutes running a program that mostly works. The point is to make it happen every time.

Then take a real thread dump and read it. The JVM detects this shape and names both threads and both locks, which is the single most useful thing it does for you all phase. The interrupt attempt at the end tells you why deadlock is worse than every other failure here.

What your program must do

  • Build a reliable deadlock with two locks and two threads
  • Print both thread states and say what they are
  • Take a thread dump and find where it names the deadlock
  • Fix it with a consistent lock order, and confirm interrupting does not help
Stuck.java
public class Stuck {
    static final Object a = new Object();
    static final Object b = new Object();

    public static void main(String[] args) throws Exception {
        // TODO: thread 1 takes a then b. Thread 2 takes b then a.
        //       Put a small sleep between the two acquisitions so it happens reliably.
        // TODO: after two seconds, print both threads' states from main
        // TODO: take a thread dump with jstack and read what it says
        // TODO: fix it by giving both threads the same lock ORDER
        // TODO: try to interrupt the deadlocked threads. Does it help?
    }
}
Hint 1
A small sleep between taking the first lock and the second makes the deadlock happen every run instead of occasionally.
Hint 2
Both threads will be BLOCKED. jstack <pid> prints the stacks, and the JVM detects this case and prints 'Found one Java-level deadlock' with both threads named.
Hint 3almost the answer
Interrupting does nothing, because a thread waiting on synchronized ignores the flag entirely. That gap is exactly why lockInterruptibly exists in Section 14.7.
What this is really testing

Whether you can build a deadlock deliberately and then see it in a thread dump. It is the failure that has no timeout and no exception, and recognising it fast is the whole skill.

C

Lock Something Nobody Else Can Reach

Real work·25 min·25 points

ex-14-5-c

Three ways to accidentally share a lock with code you have never seen.

The Integer pair is the sharpest, because the two failures are opposite. valueOf(1) is cached, so you share a lock with strangers. valueOf(1000) is a fresh object every time, so nobody can ever contend with you and the block protects nothing at all.

The this case is the one you will actually meet, because every synchronized method does it by default. Show outside code taking that lock and holding it, and the argument for a private field makes itself.

What your program must do

  • Show two unrelated classes accidentally sharing a String literal lock
  • Show the same with a small Integer, and say why a large one differs
  • Show outside code holding an object's own lock while it uses synchronized(this)
  • Rewrite all three with a private final lock object
WhoseLock.java
public class WhoseLock {
    // TODO: three classes that each lock something shared by accident:
    //   one that does synchronized (this)
    //   one that does synchronized ("key")
    //   one that does synchronized (Integer.valueOf(1))

    public static void main(String[] args) throws Exception {
        // TODO: show that two UNRELATED classes locking "key" block each other
        // TODO: show the same for Integer.valueOf(1), and say why 1000 behaves differently
        // TODO: show outside code taking the lock of an object that used
        //       synchronized (this), and holding it
        // TODO: rewrite all three with a private lock object
    }
}
Hint 1
String literals are interned, so "key" in two different classes is the same object, from Section 9.1. Both classes now share one lock and neither author knows.
Hint 2
Integer.valueOf(1) comes from the cache in Section 13.1, so it is shared. Integer.valueOf(1000) is a new object each time, which means it is a different bug: a lock nobody else can ever take, so it protects nothing.
Hint 3almost the answer
private final Object lock = new Object(); is unreachable from outside and shared with nobody. It costs one field and removes the entire category.
What this is really testing

Whether the object you lock is something you chose. Locking this, a pooled String or a small Integer means sharing a lock with strangers, and the language will not mention it.

D

Hold It for Less Time

Hard·30 min·30 points

ex-14-5-d

The lock is not the problem. What it is wrapped around is.

Time the first version and notice that eight threads doing unrelated work took eight times as long as one. Nothing was contended except the door, and the door was held for the whole job rather than for the part that needed it.

Shrinking the lock introduces a real trade, and you should be able to say what it is before deciding it is fine. Then compare with ConcurrentHashMap, which solves it a third way, and pick what you would actually ship.

What your program must do

  • Time the version that holds the lock across the slow call
  • Rewrite it so the lock covers only the shared map, and time it again
  • Name the new problem your rewrite introduced and decide whether it matters
  • Compare against ConcurrentHashMap and say which you would ship
TooLong.java
import java.util.*;

public class TooLong {
    static final Map<String, String> cache = new HashMap<>();

    static synchronized String slowWay(String key) {
        String hit = cache.get(key);
        if (hit != null) return hit;
        String value = expensive(key);        // 50 ms, and it touches nothing shared
        cache.put(key, value);
        return value;
    }

    static String expensive(String key) {
        try { Thread.sleep(50); } catch (Exception e) { }
        return key.toUpperCase();
    }

    public static void main(String[] args) throws Exception {
        // TODO: eight threads asking for eight DIFFERENT keys. Time it.
        // TODO: rewrite so the lock is held only around the map, not around expensive().
        //       Time it again.
        // TODO: what new problem did you just create, and is it acceptable?
        // TODO: try ConcurrentHashMap.computeIfAbsent instead. Time that too.
    }
}
Hint 1
Eight threads, eight different keys, 50 ms each. Held across the slow call they queue and take about 400 ms. Held only around the map they overlap and take about 50.
Hint 2
Your rewrite lets two threads compute the same key at once, because the check and the store are no longer one step. For a cache that usually costs a little duplicate work and nothing else.
Hint 3almost the answer
computeIfAbsent on a ConcurrentHashMap locks only the affected bucket, so different keys do not block each other at all. It is the answer for a cache, and knowing why is the point of the exercise.
What this is really testing

Whether you can make a lock cheaper without removing it. Almost every slow lock is a lock held around work that did not need protecting.

08

After the credits

A lock lets threads take turns. It has no way for one thread to tell another that something has happened.

while (queue.isEmpty()) {
    // now what? Spin? Sleep? For how long?
}

Spinning burns a core doing nothing. Sleeping means the thread is late by however long it guessed. And doing either while holding the lock means the thread that would fill the queue cannot get in.

Section 14.6 is wait and notify, which are on Object for the same reason the lock is: every object has them. wait() releases the lock and steps aside, which is the difference from sleep() mentioned in Section 14.3 and the reason the two are not interchangeable.

That section also covers the rule that looks like a typo and is not. wait must always be called in a loop, never in an if, and the reason is that a thread can wake up when nothing has happened at all.

Threads you opened in this section

Monitor lock will return in 14.6 - Waiting for Something to Happen