Most Read This Week
Recognizing and Eliminating Errors in Multithreaded Java
Recognizing and Eliminating Errors in Multithreaded Java
By: Mark Dykstra
Sep. 1, 2001 12:00 AM
Errors in multithreaded programs may not be easy to reproduce. The program may deadlock or encounter other thread-related errors under only very specific circumstances, or may behave differently when running different VMs.
If you use multithreading in your client- or server-side Java, you should seriously consider a detection solution for the most common problems with threaded programming, including:
This behavior results from improper use of the synchronized keyword to manage thread interaction with specific objects. The synchronized keyword ensures that only one thread is permitted to execute a given block of code at a time. A thread must therefore have exclusive access to the class or variable before it can proceed. When it accesses the object, the thread locks the object, and the lock causes other threads that want to access that object to be blocked until the first thread releases the lock.
Since this is the case, by using the synchronized keyword you can easily be caught in a situation where two threads are waiting for each other to do something.
A classic example for a deadlock situation is shown in Listing 1. Now consider this situation:
It is possible that events could unfold as follows:
A fully featured thread analysis tool would track the order in which locks are acquired, and warn of any problematic lock ordering. A lock order analysis feature should issue warnings whenever the VM scheduler might deadlock, while deadlock detection should report only actual deadlocks.
Hold While Waiting
This code is problematic in that Consumer can hold the lock on the queue, denying Pro- ducer the access it needs. This can occur even if Consumer is waiting for Producer to send notification that another item has been added to the queue. Since Producer can't add items to the queue, and Consumer is waiting on Producer for new items to process, the program is effectively deadlocked.
Locks held while waiting are only potential deadlocks because events could transpire in such a way that the notifying thread does not need the lock held by the waiting thread. However, such programming practice is risky unless you are absolutely sure that the notifying thread will never need the lock. Locks held while waiting can also cause cascading stalls, where one thread idles while holding a lock needed by another thread, which in turn holds a lock needed by yet another thread, and so on.
To correct the previous example, modify the Consumer class by moving wait() outside of synchronized(), as follows:
public class Consumer
Because threads can be preempted at any time, you can't safely assume that a thread executing at start-up will have accessed the data it needs before other threads begin to run. As well, the order in which threads are executed may differ from one VM to the next, making it impossible to determine a standard succession of events.
Sometimes, data races may be insignificant in the outcome of the program, but more often than not they can lead to unexpected results that are hard to debug. In short, data races are concurrency problems waiting to rear their ugly heads. A good thread analysis tool will identify any data race it encounters while executing your program, and flag it for you to fix.
A Benign Data Race
However, the Java VM specification dictates that Boolean values are read and written atomically, meaning that the VM can't interrupt a thread in the middle of a read or write, and that once the value has been changed, it's never changed back. This is a benign data race, and the code is safe.
A Malignant Data Race
What happens if a wife and husband simultaneously attempt to deposit money to a joint account, from two different banking machines? Let's call them Alice and Bob. At the beginning of our scenario, their joint account has $100.
Alice deposits $25. Her banking machine starts to execute deposit(). It gets the current balance ($100), and stores that in a temporary local variable. It then adds $25 to that balance, and the temporary variable holds $125. Then, before it can call setBalance(), the thread scheduler interrupts her thread.
Bob deposits $50. While Alice's thread is still in limbo, his thread starts to execute deposit(). The getBalance() returns $100 (remember, Alice's thread hasn't written the updated balance yet), and the thread adds $50 to obtain a value (in its temporary local variable) of $150. Then, before it can call setBalance(), Bob's thread is interrupted.
Alice's thread now resumes, and writes its temporary local variable's contents ($125) to the balance. The banking machine informs Alice that the transaction is complete. Bob's thread resumes, and writes the contents of its temporary local variable ($150) to the balance. The banking machine informs Bob that the transaction is complete.
Net effect? The system has lost Alice's deposit.
Your first instinct might be to protect the Account.balance_ field by making getBalance() and setBalance() synchronized methods. This will not solve the problem. The synchronized keyword will ensure that only one thread can execute getBalance() or setBalance() at a time, but that won't prevent one thread from modifying the balance of an account while the other is halfway through a deposit.
How to Fix the Race
In our example, the developer must ensure that once a thread has obtained the current balance no other thread can alter that balance until the first thread has finished using that value. This can be accomplished by making deposit() and withdraw() synchronized methods.
The Synchronized Keyword
This article is meant to be an introduction to the most common Java multithreading development errors. For more information on concurrent programming, refer to the References. Christian Jaekl was particularly helpful and I am grateful for his support and advice.
Reader Feedback: Page 1 of 1
Subscribe to the World's Most Powerful Newsletters
Today's Top Reads