- 13.1 Concurrentgate
- 13.2 A Brief History of Data Sharing
- 13.3 Look, Ma, No (Default) Sharing
- 13.4 Starting a Thread
- 13.5 Exchanging Messages between Threads
- 13.6 Pattern Matching with receive
- 13.7 File Copyingwith a Twist
- 13.8 Thread Termination
- 13.9 Out-of-Band Communication
- 13.10 Mailbox Crowding
- 13.11 The shared Type Qualifier
- 13.12 Operations with shared Data and Their Effects
- 13.13 Lock-Based Synchronization with synchronized classes
- 13.14 Field Typing in synchronized classes
- 13.15 Deadlocks and the synchronized Statement
- 13.16 Lock-Free Coding with shared classes
- 13.17 Summary
13.15 Deadlocks and the synchronized Statement
If the bank account example is the "Hello, world!" of threaded programs, the bank account transfer example must be the corresponding (if grimmer) introduction to threads that deadlock. The example goes like this: Assume you have two BankAccount objects, say, checking and savings. The challenge is to define an atomic transfer of some money from one account to another.
The naïve approach goes like this:
// Transfer version 1: non-atomic void transfer(shared BankAccount source, shared BankAccount target, double amount) { source.withdraw(amount); target.deposit(amount); }
This version is not atomic, however; between the two calls there is a quantum of time when money is missing from both accounts. If just at that time a thread executes the inspectForAuditing function, things may get a little tense.
To make the transfer atomic, you need to acquire the hidden mutexes of the two objects outside their methods, at the beginning of transfer. You can effect that with the help of synchronized statements:
// Transfer version 2: PROBLEMATIC void transfer(shared BankAccount source, shared BankAccount target, double amount) { synchronized (source) { synchronized (target) { source.withdraw(amount); target.deposit(amount); } } }
The synchronized statement acquires an object's hidden mutex through the execution of the statement's body. Any method call against that object benefits from an already acquired lock.
The problem with the second version of transfer is that it's prone to deadlock: if two threads attempt to execute a transfer between the same accounts but in opposite directions, the threads may block forever. A thread attempting to transfer money from checking to savings locks checking exactly as another thread attempting to transfer money from savings to checking manages to lock savings. At that point, each thread holds a lock, and each thread needs the other thread's lock. They will never work out an understanding.
To really fix the problem, you need to use synchronized with two arguments:
// Transfer version 3: correct void transfer(shared BankAccount source, shared BankAccount target, double amount) { synchronized (source, target) { source.withdraw(amount); target.deposit(amount); } }
Synchronizing on several objects in the same synchronized statement is different from successively synchronizing on each. The generated code always acquires mutexes in the same order in all threads, regardless of the syntactic order in which you specify the objects. That way, deadlock is averted.
The actual order in the reference implementation is the increasing order of object addresses. Any global ordering would work just as well.
Multi-argument synchronized is helpful but, unfortunately, not a panacea. General deadlock may occur non-locally—one mutex is acquired in one function, then another in a different function, and so on, until a deadlock cycle closes. But synchronized with multiple arguments raises awareness of the issue and fosters correct code with modular mutex acquisition.