对于可变引用字段中的不可变类型,请使用易失性和本地缓存或同步


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块时,将越过读取内存屏障,当您离开时,将越过写入。

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

另一种选择是包装private volatile Object字段并提供原子方法的AtomicReference,如compareAndSet(...)。 即使你没有使用特殊方法,许多程序员也认为这是封装你需要内存同步的字段的好方法。

最后,volatilesynchronized还提供"发生之前"保证,这些保证控制指令重新排序,这对于确保程序中正确的操作顺序非常重要。

就您的代码而言,您永远不应该执行以下操作:

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比正常操作(可以使用缓存内存(昂贵得多,但volatilesynchronized块便宜得多,后者必须在进出块的途中测试和更新锁定状态,并跨越影响所有缓存内存的内存障碍。 我的信封背面测试表明,这证明了这种性能差异。

最新更新