对于可变引用字段中的不可变类型,使用 volatile 并在本地缓存或同步?

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);
      }
    }
  }
}

以上是完成几乎同一件事的两种方法的示例,但我不太清楚何时更喜欢其中一种方法。

在哪些场景下一个比另一个更有用?

我认为这个问题与Volatile or synchronized for primitive type?不同,因为那里的问题和答案没有提到拥有本地缓存​​变量的做法。

use volatile and cache locally or synchronize?

我认为您误解了 volatile 关键字的作用。对于所有现代 OS 处理器,每个处理器都有一个本地内存缓存。 volatile 关键字不启用本地缓存——您将 "for free" 作为现代计算机硬件的一部分。这种缓存是多线程程序性能提升的重要组成部分。

volatile 关键字确保当您读取字段时,读取内存屏障被越过,确保所有更新的中央内存块都在处理器缓存中更新。写入 volatile 字段意味着跨越写入内存屏障,确保将本地缓存更新写入中央内存。此行为与您在 synchronized 块中跨越的内存障碍完全相同。当您进入 synchronized 块时,将越过读取内存屏障,而当您离开时,将越过写入。

synchronizedvolatile最大的区别是synchronized需要支付锁定费用。 synchronized 必需的 当同时发生多个操作并且您需要将这些操作包装在互斥锁中时。如果您只是想让 name 字段与主内存正确更新,那么 volatile 是正确的选择。

另一个选项是 AtomicReference,它包装了一个 private volatile Object 字段并提供了像 compareAndSet(...) 这样的原子方法。即使您不使用特殊方法,许多程序员也觉得将需要内存同步的字段封装起来是一种很好的方法。

最后,volatilesynchronized 还提供 "happens-before" 保证控制指令重新排序,这对于确保程序中正确的操作顺序很重要。

就您的代码而言,您永远不应该这样做:

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 块更昂贵,它必须在进出块的路上测试和更新锁定状态 跨越影响所有缓存内存的内存屏障。我的信封测试证明了这种性能差异。