Level: Introductory Peter Haggar (haggar@us.ibm.com), Senior Software Engineer, IBM
01 Oct 2000
Editor's note: The following article is an excerpt from the book "Practical Java" published by Addison-Wesley. You can order this book from Borders.com.
When variables are shared between threads, they must always be accessed properly
in order to ensure that correct and valid values are manipulated. The JVM is
guaranteed to treat reads and writes of data of 32 bits or less as atomic. This might lead some programmers to believe that access to shared variables does not need to be synchronized or the variables declared volatile . Consider this code:
class RealTimeClock
{
private int clkID;
private long clockTime;
public int clockID()
{
return clkID;
}
public void setClockID(int id)
{
clkID = id;
}
public long time()
{
return clockTime;
}
public void setTime(long t)
{
clockTime = t;
}
//...
} |
Now contemplate an implementation that uses the previous code. It might create
an object of the RealTimeClock class and two threads. It then could call the methods of this class from the two threads.
The variables clkID and clockTime are stored in main memory. However, the Java language allows threads to keep private working copies of these variables. This enables a more efficient execution of the two threads. For example, when each thread reads and writes these variables, they can do so on the private working copies instead of accessing the variables from main memory. The private working copies are reconciled with main memory only at specific synchronization points.
The clockID and setClockID methods perform only a read and a write, respectively, on data of type int. Therefore, the operation of these methods is automatically atomic. However, given that the clkID variable could be stored in private working memory for each thread, consider the following possible sequence of events:
- Thread 1 calls the
setClockID method, passing a value of 5.
- Thread 2 calls the
setClockID method, passing a value of 10.
- Thread 1 calls the
clockID method, which returns the value 5.
This sequence of events is possible because the clkID variable is not guaranteed to be reconciled with main memory. During step 1, thread 1 places the value 5 in its working memory. When step 2 executes, thread 2 places the value 10 in its working memory. When step 3 executes, thread 1 reads the value from its working memory and returns 5 . At no point are the values reconciled with main memory.
There are two ways to fix this problem.
- Access the
clkID variable from a synchronized method or block.
- Declare the
clkID variable volatile.
Either solution requires the clkID variable to be reconciled with main memory. Accessing the clkID variable from synchronized method or block does not
allow that code to execute concurrently, but it does guarantee that the clkID variable
in main memory is updated appropriately. Main memory is updated when the
object lock is obtained before the protected code executes, and then when the lock
is released after the protected code executes.
Declaring the clkID variable volatile allows the code to execute concurrently and also guarantees that the private working copy of the clkID variable is reconciled with main memory. This reconciliation, however, occurs each time the variable is accessed.
 | | Implementations of JVMs are encouraged to treat 64-bit operations as atomic but are not required to do so. This is because some popular microprocessors currently do not provide efficient atomic memory transactions on 64-bit values. |
|
Also consider the time and setTime methods that operate on a variable of type
long. These methods can exhibit the same problem described previously. They also have an additional problem. Data of type long is typically represented by 64 bits spread across two 32-bit words. An implementation of the JVM might treat the operation on a 64-bit value as atomic, but most JVM implementations today do not, instead treating such operations as two distinct 32-bit operations. Consider the following possible sequence of events with the clockTime instance
variable:
- Thread 1 calls the
time method.
- Thread 1 begins to read the
clockTime instance variable and reads the first 32 bits.
- Thread 1 is preempted by thread 2.
- Thread 2 calls the
setTime method. The setTime method performs two writes of 32 bits each to the clockTime instance variable, replacing both 32-bit values with different values.
- Thread 2 is preempted by thread 1.
- Thread 1 reads the second 32 bits of the
clockTime instance variable and returns the result.
According to this sequence of events, the value returned by the time method is
made up of the first 32 bits of the old value of the clockTime instance variable,
and the second 32 bits of the new value of the same instance variable. The value
returned is not correct. This is because of the multiple reads and writes necessary
for 64-bit data in the JVM. The private working memory and main memory issue
discussed earlier is also a problem. You have the same two options to fix this problem:
synchronize access to the clockTime variable or declare it volatile.
In summary, it is important to understand that atomic operations do not automatically
mean thread-safe operations. In addition, whenever multiple threads share
variables it is important that they are accessed in a synchronized method or block, or are declared with the volatile keyword. This ensures that the variables
are properly reconciled with main memory, thereby guaranteeing correct values at
all times.
Whether you use volatile or synchronized depends on several factors. If concurrency is important and you are not updating many variables, consider using volatile. If you are updating many variables, however, using volatile might be slower than using synchronization. Remember that when variables are declared
volatile, they are reconciled with main memory on every access. By contrast, when synchronized is used, the variables are reconciled with main memory only when the lock is obtained and when the lock is released.
Consider using synchronized if you are updating many variables and do not want the cost of reconciling each of them with main memory on every access, or you want to eliminate concurrency for another reason.
The following table summarizes the differences between the synchronized and volatile keywords.
Differences between volatile and synchronized
| Technique | Advantages | Disadvantages | synchronized | Private working memory is reconciled with main memory when the lock is obtained and when the lock is released. | Eliminates concurrency. | volatile | Allows concurrency. | Private working memory is reconciled with main memory on each variable access. |
About the author  | |  | Peter Haggar is a Senior Software Engineer with IBM. He currently works on emerging Java and Internet technology and is the project lead for IBM's real-time Java reference implementation. He has a broad range of programming experience having worked on development tools, class libraries, and operating systems. He is also a frequent technical speaker on Java and other technologies at numerous industry conferences. He received a B.S. in Computer Science from Clarkson University in New York in 1987. He can be reached at haggar@us.ibm.com. |
Rate this page
|