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);
}
}
}
}
以上是完成几乎相同事情的两种方法的示例,但我没有很好地掌握何时选择其中一种。
在哪些情况下,一个比另一个更有用?
我相信这个问题与 Volatile 或同步的基元类型不同?因为那里的问题和答案没有提到拥有本地缓存变量的做法。
使用易失性和本地缓存还是同步?
我认为您对volatile
关键字的作用有误。 对于所有现代操作系统处理器,都有一个本地的每处理器内存缓存。 volatile
关键字不启用本地缓存 - 您可以"免费"获得作为现代计算机硬件的一部分。 这种缓存是多线程程序性能提高的重要组成部分。
volatile
关键字可确保在读取字段时越过读取内存屏障,确保处理器缓存中更新所有更新的中央内存块。 写入volatile
字段意味着跨越写入内存屏障,确保将本地缓存更新写入中央内存。 此行为与您在synchronized
块中跨越的内存障碍完全相同。 当您进入synchronized
块时,将越过读取内存屏障,当您离开时,将越过写入。
synchronized
和volatile
的最大区别在于,您需要支付锁定费用 synchronized
. 当同时发生多个操作并且您需要将这些操作包装在互斥锁中时,synchronized
是必需的。 如果您只是想使用主内存正确更新name
字段,那么volatile
就是要走的路。
另一种选择是包装private volatile Object
字段并提供原子方法的AtomicReference
,如compareAndSet(...)
。 即使你没有使用特殊方法,许多程序员也认为这是封装你需要内存同步的字段的好方法。
最后,volatile
和synchronized
还提供"发生之前"保证,这些保证控制指令重新排序,这对于确保程序中正确的操作顺序非常重要。
就您的代码而言,您永远不应该执行以下操作:
synchronized(guard) {
if (name != null) {
print(name);
}
}
您不想在synchronized
块内执行昂贵的 IO。 它应该是这样的:
// 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);
}
使用 volatile
,您希望执行一次volatile
字段查找,因此建议执行以下操作:
void run() {
// only do one access to the expensive `volatile` field
String nameCopy = name;
if (nameCopy != null) {
print(nameCopy);
}
}
最后,从注释中,volatile
比正常操作(可以使用缓存内存(昂贵得多,但volatile
比synchronized
块便宜得多,后者必须在进出块的途中测试和更新锁定状态,并跨越影响所有缓存内存的内存障碍。 我的信封背面测试表明,这证明了这种性能差异。