10.6 Atomic Access and Assignment

Variables shared between multiple threads (e.g., instance variables of objects) have atomic assignment guaranteed by the Java language specification for all data types except longs and doubles. Actually, the storing of a value into a variable takes two primitive operations, a store and a write. However, the language specification also states that once a store operation occurs on a particular variable, no other store operation is allowed on that variable until the write operation has occurred. The (original[5]) specification allows longs and doubles to be stored in two separate sets of store+write operations, hence their exception to atomicity. A similar atomic specification applies for reading variables.

[5] The Java memory model specification is being changed. Java Specification Request 133, "Java Memory Model and Thread Specification Revision," addresses the various problems in the existing specification. Chapter 17 of the Java language specification and Chapter 8 of the Java Virtual Machine specification are proposed to be revised substantially. I'm very grateful to Brian Goetz for clarifying several points in this section.

This means that access and update of individual variables does not need to be synchronized simply to avoid corruption (as long as they are not longs or doubles). If a method consists solely of a single variable access or assignment, there is no need to make it synchronized for thread-safety, and every reason not to do so for performance.

Note that I'm talking about using synchronization for thread-safety here, not synchronization for visibility of updates and accesses of variables. First, read the later Synchronization Ordering sidebar to ensure that you understand that synchronization provides no guarantees about the ordering of execution among different threads. Bearing that in mind, atomic access and update once again do not provide any guarantees about ordering of variables. Furthermore, unlike synchronization, atomic access and update do not provide any guarantee about timely synchronization between values of a variable held in different threads. When a synchronized block is passed, all the variables in a thread have been updated to the values in the "master" memory: they are synchronized. However, that synchronization does not occur with a simple atomic access or update. This means that for a variable that is atomically assigned to outside of a synchronized block, theoretically a thread could see a different value from the "master" memory for that variable for an extended period of time. Some other Java authors have suggested that this is dangerous. And while this book is full of dangerous advice, I don't believe this particular performance-tuning technique is any more dangerous than many other techniques. Specifically, if two or more variables don't need to be consistent with each other or vary consistently among themselves over time, you can trade some delay in propagation between threads for some extra performance. If you have two variables that have to be consistent with each other at all times across threads, such as the X and Y values of a shared point or a variable that has to vary consistently over time between multiple threads, then you need to synchronize. And in the case of variables needing to remain consistent with each other, atomic assignment is still fine for the individual variables, but the combined assignment needs to be synchronized (see the examples discussed shortly).

The concern here is primarily that you might read a variable in one thread, thread1, but meanwhile that variable has been updated in another thread, thread2, and the value has not propagated to thread1. The lack of guarantee of any ordering between threads means that this can occur even with synchronized variables because the read can occur prior to the update. But the difference is that with atomic access, even after thread2 updates its variable, thread1 doesn't necessarily see that updated value until a synchronization takes place. But note that the synchronization does take place as soon as any synchronized block is passed in thread1. For the vast majority of programs, there is no issue with using atomic access and update. The only place I have found where atomic access and update might easily be used but could cause a problem is where a Runnable thread has a stopping condition dependent on a variable being set from another thread, and the thread never enters a synchronized block. For example:

class SomeClass implements Runnable
{
  boolean dontStop = true;
  public void run(  )
  {
    //dontStop is set to false from another thread
    //when it wants this thread to terminate.
    while(dontStop)
    {
      //Loop body NEVER enters ANY synchronized blocks
      //which is pretty unusual except for animations
    }
  }

In this situation, you are actually better off declaring the dontStop variable as volatile rather than changing the test to access a synchronized dontStop( ) method because that is precisely the appropriate usage for volatile, and you don't incur synchronization overhead.

The volatile keyword specifies that the variable declared volatile must always have its value updated to the "main" memory value. So threads are not allowed to have a value for that variable that is different from the main memory value at any time. As a consequence, volatile variables can be slower than non-volatile variables because of the extra requirement to always synchronize the variable, but faster than synchronized access and updates because volatile synchronizes only one variable whereas synchronization synchronizes all variables.

The thread-safety of atomic accesses and updates extends further to any set of statements that are accessing or assigning to a variable independently of any other variable values. The exclusion here precludes setting a variable that depends on the value of another variable being thread-safe; this would be two separate operations, which is inherently not thread-safe. For example, these methods:

public void setMe(Object o) {me = o;}
public Object getMe(  ) {return me;}

are individually thread-safe, with no need for synchronized modifiers to be added to the method declaration. On the other hand, this method:

public void setMe(Object o) {if(overwrite) me = o;}

is not thread-safe: overwrite may be true at the time of checking in the if statement, but false by the time of the subsequent assignment statement. Anything more complex than simple assignments and accesses is probably not thread-safe: it depends on whether any particular intermediate state that can be accessed is considered corrupt by the application. Consider the code being halted before or after any particular atomic statement, and decide whether or not another thread could now access a corrupt application state.

Combining several calls to methods that atomically assign variables is the same problem as combining several calls to synchronized methods. The individual calls are executed atomically, but the combination is not necessarily atomic:

public void setMe1(Object o) {me = o;}
public void setMe2(Object o) {me = o;}
public void setBoth(Object o1, Object o2) {setMe1(o1);setMe2(o2);}

For these three methods, it does not matter whether setMe1( ) and setMe2( ) are synchronized. setBoth( ) is not synchronized, so it can be interrupted between the calls to setMe1( ) and setMe2( ), allowing another thread to update one of the instance variables. This can leave the object in a potentially corrupt application state if both instance variables are always supposed to be updated together. Specifically, if two threads call the setBoth( ) method simultaneously, the outcome is not predictable unless setBoth( ) is synchronized. Even the simple example of setInt(getInt( )+1) is not thread-safe; without synchronizing the whole statement you could lose the increment.

Synchronization Ordering

It is easy to misunderstand exactly what synchronization does. Synchronization ensures that a set of statements executes exclusively for a particular monitor. Synchronization does not guarantee the relative order of execution of synchronized blocks between threads. If two threads try to execute a synchronized block simultaneously, one succeeds first, but there is no guarantee about which one that is.

Atomic assignment is similar to the case where the set of synchronized statements is one statement, and the synchronization is set by the VM. When considering atomic assignment, you might ask the question, "What if a context switch occurs during the method call setup or tear down? When does the synchronization happen, and what happens with the context switch?" The actual moment when the synchronization occurs does not matter. It does not matter if a context switch happens at any time before or after a set of synchronized statements. Either the synchronized set has not been entered, or it has been completed. Only the actual granting of the lock matters, and that is atomic with respect to all interested threads.

Until you reach an atomic assignment statement, it makes no difference whether another atomic assignment on the same variable occurs. This is purely the ordering of assignments, which is not guaranteed with synchronization anyway. After the atomic assignment is finished, it is complete. A context switch hitting the method tear down does not matter. The usual reason to synchronize a simple updator is to avoid a corrupt assignment (two threads simultaneously updating the same variable, and the resulting value being neither of the updated values). This can indeed occur for doubles and longs, but not for other data types.

For serious number crunching involving doubles and longs, I recommend using separate data structures for each thread or using a VM that guarantees atomic assignment for doubles and longs.

A longer discussion about Java's atomicity can be found in an article by Art Jolin,[6] where he discusses unsynchronized thread-safe data structures, including why a binary tree (specifically the AWTEventMulticaster class) can be thread-safe without any synchronized methods.

[6] "Java's Atomic Assignment," Java Report, August 1998.