Eric Cochran Eric Cochran - 2 months ago 7
Java Question

For an immutable type in a mutable reference field, use volatile and cache locally or synchronize?

final class NameVolatile {
@Nullable private volatile String name;

void setName(String name) {
this.name = name
}

void run() {
String name = name;
if (name != null) {
print(name);
}
}
}

final class NameSynchronized {
private final Object guard = new Object();
@Nullable private String name;

void setName(String name) {
synchronized(guard) {
this.name = name
}
}

void run() {
synchronized(guard) {
if (name != null) {
print(name);
}
}
}
}


The above is an example of two ways to accomplish almost the same thing, but I do not have a good grasp on when to prefer one or the other.

What are the scenarios where one is more useful than the other?

I believe this question is different from Volatile or synchronized for primitive type? because the question and answers there do not mention the practice of having a local cache variable.

Answer

use volatile and cache locally or synchronize?

I think you are mistaken about what the volatile keyword does. With all modern OS processors, there is a local per-processor memory cache. The volatile keyword doesn't enable the local caching -- you get that "for free" as part of modern computer hardware. This caching is an important part of multi-threaded program performance increases.

The volatile keyword ensures that when you read the field, a read memory-barrier is crossed ensuring that all updated central memory blocks are updated in the processor cache. A write to a volatile field means that a write memory-barrier is crossed ensuring that local cache updates are written to central memory. This behavior is exactly the same as the memory barriers that you cross in a synchronized block. When you enter a synchronized block, a read memory-barrier is crossed and when you leave a write is crossed.

The biggest difference between synchronized and volatile is that you pay for locking with synchronized. synchronized is necessary when there are more than a single operation happening at the same time and you need to wrap the operations in a mutex lock. If you are just trying to keep your name field properly updated with main memory then volatile is the way to go.

Another option is an AtomicReference which wraps a private volatile Object field and provides atomic methods like compareAndSet(...). Even if you are not using the special methods, many programmers feel that it is a good way to encapsulate the fields you need to be memory synced.

Lastly, both volatile and synchronized also provide "happens-before" guarantees which control instruction reordering which is important to ensure proper order of operations in your program.

In terms of your code, you should never do something like:

synchronized(guard) {
   if (name != null) {
       print(name);
   }
}

You don't want to do expensive IO inside of a synchronized block. It should something like:

// grab a copy of the name so we can do the print outside of the sync block
String nameCopy;
synchronized(guard) {
   nameCopy = name;
}
if (nameCopy != null) {
   print(nameCopy);
}

With volatile, you want to do one volatile field lookup so something like the following is recommended:

void run() {
   // only do one access to the expensive `volatile` field
   String nameCopy = name;
   if (nameCopy != null) {
      print(nameCopy);
   }
}

Lastly, from the comments, volatile is significantly more expensive than a normal operation (which can use cached memory) but volatile is significantly less expensive than a synchronized block which has to test and update the lock status on the way in and way out of the block and cross the memory barriers that affect all cached memory. My back of the envelope testing demonstrates this performance difference.

Comments