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.
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.
The problem
You need the read, the change and the write to happen without anyone else getting in between.
counter++; // read, add, writeNothing 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.
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 write | What is actually locked | |
|---|---|---|
| synchronized (obj) { } | a block | obj. Visible, and you chose it |
| synchronized void m() | an instance method | this. The whole method body |
| static synchronized void m() | a static method | MyClass.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 lockThe 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.
Under the hood
Going deeperIn the bytecode it is two instructions:
monitorenter
... your code ...
monitorexitFor 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
- 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.
- Two threads want itThe loser spins for a short while, on the guess that the holder will finish quickly. Still no operating system involved.
- 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.
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.
Check yourself
Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.
`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
staticmethod 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.
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
synchronizedblock 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
superwould deadlock every time.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
volatilefixes 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.
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
Two Keys, No Protection
ex-14-5-aBoth 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
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
this. A static method locks the Class object, TwoKeys.class. Those are two different objects, so neither thread ever waits.Hint 2
synchronized the whole time.Hint 3almost the answer
private static final Object lock. The second is clearer, because the lock is visible in the code rather than implied.Deadlock in Ten Lines
ex-14-5-bBuild 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
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
Hint 2
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
synchronized ignores the flag entirely. That gap is exactly why lockInterruptibly exists in Section 14.7.Lock Something Nobody Else Can Reach
ex-14-5-cThree 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
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
"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.Hold It for Less Time
ex-14-5-dThe 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
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
Hint 2
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.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 lockReentrantLock exists because synchronized has no timeout, no tryLock and no interrupting.14.7 - Locks You Can Give Up On
Monitor lock will return in 14.6 - Waiting for Something to Happen