All reads and writes are atomic actions for all volatile fields, regardless of whether they are object references or are of primitive data types. An atomic action is guaranteed to be performed by a thread without any interleaving—that is, without any race conditions. Such an action either performs in its entirety or not at all—akin to a database transaction. Most program actions are not actually atomic; even something as simple as an arithmetic operator takes more than one CPU cycle, and therefore can be interrupted before the action completes.

Note that atomicity holds only for read and write operations, not for non-atomic operations like the increment operator (++), that are actually executed in several steps. For example, the expression ++i is equivalent to the following code:

int tmp = i;
tmp = tmp + 1;
i = tmp;

The expression ++i actually is being evaluated as a read of the value in i, and then a write to i after the value has been incremented by 1. If i was a shared volatile field, a different thread could read it between the atomic read and write operations, resulting in potential memory consistency errors, unless intermediate values in i did not matter. Computing the next value in i is dependent on the previous value in i. Declaring a variable with such data dependencies as volatile may not be adequate, as demonstrated by Example 23.9 that implements the counter as a volatile field. Not surprisingly, the output from the program shows an incorrect result of incrementing the counter due to memory consistency errors.

The volatile keyword is enough to combat memory consistency problems. That is, it solves the visibility problem (writing to main memory), but not interleaving of operations on a shared variable (i.e., thread interference/race conditions). Solving both problems requires mutual exclusion. We explore solutions for implementing mutual exclusion other than synchronized code later in this section: atomic variables for mutual exclusion on shared single variables (p. 1456) and programmatic locks for mutual exclusion on shared resources (p. 1460).

Example 23.9 Volatile Counter

Click here to view code image

package safe;
public class VolatileCounter implements ICounter {
  private volatile int counter = 0;
  @Override public int getValue()   { return counter; }
  @Override public void increment() { ++counter; }
}

Probable output from the program in Example 23.7, p. 1451:

Click here to view code image

Volatile Counter:                   87672

There is no need to use a volatile field unless it is shared between threads, or if only atomic reads and writes are necessary on a field. A field of primitive type long or double does not guarantee atomic reads and writes, unless it is declared volatile. Also, a final field cannot be declared volatile, as it is immutable.

It is worth noting the differences between the volatile and the synchronized keywords. The keyword volatile is only applicable to fields, whereas the keyword synchronized is only applicable to a statement block or method. A synchronized statement cannot synchronize on null, but a volatile field may be null.

Since there are no locks involved when accessing a volatile field, there is no blocking of threads either. Performance overhead is also lightweight, compared to synchronized code which is bound by the whole regime of thread management, although the keyword signals to the compiler not to undertake certain optimizations. And while the volatile keyword cannot replace synchronized code in all situations, it is more efficient in certain situations where the visibility and atomicity of a shared field are overriding factors.