Shared Resources in Multithreaded Programs
Use synchronization when multiple threads share a resource and their access must be exclusive.
A Counter Shared by Several Threads
Imagine several threads that each want to add 1 to the same counter. Each thread runs its own code, but they all touch one shared value. At first glance it seems enough to give the counter an increment method. The real question is different: is the operation that changes the shared value protected when different threads use it? This article follows a counter through that question, step by step, and shows how synchronization answers it.
Tracing Two Threads on One Counter
Watch what happens when two threads each add 1 to a counter that starts at 0 and nothing coordinates their access. Each thread first reads the current value, then computes a new value, then writes it back. The steps of the two threads can interleave, so both threads may read the same old value before either one writes. When that happens, one increment is lost. The diagram below shows one such interleaving. The final value is 1, even though two increments were requested.
Mutual Exclusion and the Lock
Synchronization is the mechanism used when multiple threads share a resource and their access must be exclusive. Exclusive access means that only one thread at a time may be inside the protected section. This property is called mutual exclusion.
A useful way to picture mutual exclusion is a lock. A thread that enters the protected section holds the lock. A second thread that reaches the same synchronized code cannot enter while the first thread still holds the lock; it has to wait. When the first thread leaves the protected section, the lock becomes available again. The state diagram below traces that cycle for two threads.
Notice that Thread B does not fail or skip the code. It simply waits until the lock is free, then enters. Every thread still runs the same protected operation exactly once; the lock only decides the order in which they run it, so that no two of them are inside the section at the same moment.
Declaring a Synchronized Method
The first way to protect a shared operation in Java is the synchronized modifier on a method. The keyword goes into the method signature. This approach fits when the whole method is the operation that must be protected, such as incrementing or reading a shared counter.
- The synchronized keyword sits in the method signature of increment.
- The read in
getCountis also synchronized, so reads and updates of count follow the same rule. - The count field is the shared resource; both methods operate on it.
For a synchronized instance method such as increment, the lock is the object the method is called on. If two threads call increment on the same Counter object, they contend for that object's lock. The protected data and the lock belong to the same object, which is what makes the protection meaningful.
Protecting One Object with a Synchronized Statement
Sometimes the whole method is not the right unit to protect. A synchronized statement lets you choose a particular object and a particular block of code. The object in parentheses is part of the statement's meaning: it names the object whose lock guards the block. Everything inside the braces is the protected operation.
- lock is a dedicated object; the synchronized statement names it in parentheses.
- Only the block inside the braces is protected by that lock.
- Both methods use the same lock object, so they protect the same counter.
The source's own example uses an object named tsc with synchronized. In that example, the value check plus the conditional action sit inside the protected block. The same pattern applies here: the check and the action that depends on it belong together inside one synchronized block, so that another thread cannot change the value between them.
Predict the result: two threads each call increment once on the same Counter object, with the synchronized method version. Before reading on, decide whether the final count can still be 1, and say what would have to change for it to be 2.
Hints
- Ask whether both threads can be inside increment at the same time.
- Think about which object's lock is used when both calls target the same Counter.
Why Every Access Path Must Synchronize
A synchronized method or block only protects the threads that go through it. The guarantee depends on every thread using synchronization when it accesses the shared resource. If one code path reads or writes the counter without the lock, that path bypasses mutual exclusion, and the guarantee no longer covers it. The comparison below contrasts the two situations.
| Access path | Uses synchronization | Protection for that path |
|---|---|---|
| increment() called by Thread A | Yes | Mutual exclusion applies |
| increment() called by Thread B | Yes | Mutual exclusion applies |
| Direct update of count from a third path | No | Not covered by the lock |
Each access path must follow the rule for the guarantee to hold
Mistakes Beginners Make with Shared Counters
Common mistake Assuming that a counter having an increment method is enough
Why it failsThe question is whether the operation that changes the shared value is protected when different threads use it, not whether a method exists.
Do this insteadCheck that the method or block that changes the value uses synchronization.
Common mistake Synchronizing only the writes and leaving a read unprotected
Why it failsA path that bypasses synchronization is not covered by the mutual-exclusion guarantee.
Do this insteadApply the same synchronization rule to every thread's access to the shared value.
Common mistake Using different lock objects for the same shared data
What it looks likeTwo methods each synchronize on their own new Object() while both change count.
Why it failsThe lock is tied to the object named in the statement, so two different locks do not make one thread wait for the other.
Do this insteadName one shared lock object for all accesses to the same resource.
Choosing Between a Method and a Statement
Use a synchronized method when the whole method is the protected operation, as with a counter's increment or read. Use a synchronized statement when one particular object and one block of code need synchronized access, especially when only part of a method touches the shared resource. Either way, the rule is the same: every thread that accesses the shared resource must follow it.
A class holds a shared total and has one method that adds an amount to it and another method that prints the total. Decide which methods must use synchronization, and explain how you would choose between a synchronized method and a synchronized statement for each.
Hints
- Both methods touch the same shared total, so consider every access path.
- A method that only prints part of its work may only need a block around the shared read.
Key Takeaways
- Synchronization protects a shared resource when multiple threads need exclusive access to it, giving mutual exclusion: only one thread at a time is inside the protected section.
- Declare a synchronized method by placing the synchronized modifier in the signature when the whole method is the protected operation, such as incrementing a shared counter.
- Use a synchronized statement,
synchronized(object)followed by a block, when a particular object and a particular block of code need protection. - The guarantee holds only when every thread that accesses the shared resource follows the synchronization rule; one unsynchronized access path is not covered.
- When debugging a shared-resource result, look first at the access path that touches the resource, then check whether each access is synchronized.