Synchronization, and choosing what to lock
◈ 5 cardsProtecting a multi-step write with the right monitor: synchronized blocks vs methods, why the shared object is what matters, and the shredded access-log record that A2 must avoid.
Sharing is the point, and the problem
Threads are cheap compared with processes precisely because they share one heap. That is also the hazard: two threads holding a reference to the same object can interleave inside it. Synchronization becomes an issue for exactly one reason — two threads reach the same object — and for no other. Not the same class, not the same Runnable type: the same instance. A static field is shared by definition; an instance field is shared whenever the instance is.
synchronized gives you a whole block, not a whole method call
Every Java object has a monitor. synchronized (obj) { … } acquires obj's monitor for the duration of the block and releases it on exit, however the block exits. Two threads that synchronize on the same object run those blocks strictly in series. Two threads synchronizing on different objects run in parallel and protect nothing, even if both are writing to the same file.
Adding the synchronized modifier to a method is shorthand for wrapping the entire body in synchronized (this). It is a shortcut, not a strategy: it locks the object the method belongs to, which may not be the object that is actually shared.
Individually safe calls, collectively broken
Here is the trap that ruins A2's access log. Writer subclasses synchronize internally on a lock field, so a single out.write(...) call is already atomic — no two threads can interleave inside one call. Programmers read that and conclude the log is safe. It is not. A log record is not one call.
Atomic pieces do not compose into an atomic whole. Four safe write() calls in a row are four separate opportunities for another thread to be scheduled in between, and the file ends up with one record's timestamp welded to another record's request line. (Output streams are worse: apart from PrintStream, they have no internal synchronization at all, so even a single write can interleave.)
Which monitor?
Two defensible choices, and one wrong one.
- Lock the shared
Writer—synchronized (out). Correct as long as every thread reaches the file through this sameout. It also composes with the writer's own internal lock instead of fighting it. - Lock the log object —
synchronized (this), i.e. thesynchronizedmodifier. Equally correct here, becauseoutis a private field that never escapes, so the only route to the file is through this object. - Lock something per-thread — a fresh handler object, a local,
thiswhen each connection has its own handler instance. Zero protection: every thread takes a different monitor and they all proceed at once.
Worked example — the access log every handler writes to
One AccessLog object, one BufferedWriter, one line per request in common log format: address, timestamp, request line, status, byte count. Each connection handler calls log(...), which makes four write() calls. Run two handlers and the file interleaves — a record split around another record, unparseable by the analyser you build in L3.6.
Wrap all four calls in synchronized (out) and the second thread blocks at the opening brace until the first has written its terminator. Both records are whole; the order between them is still unspecified, which is fine — a log is a set of records, not a sequence you promised. Keep the block exactly that big: acquiring the monitor before the timestamp is computed adds contention for no safety, and holding it across a flush to a slow disk turns your lock into the bottleneck.