Java Spring 应用程序中缺少 volatile 变量及其后果

edu*_*edu 1 java spring java-threads

那些开发过专业的多线程 Java Spring 应用程序的人可能可以证明 volatile 关键字的使用几乎不存在(以及与此相关的其他线程控制),尽管在需要时错过它可能会带来灾难性的后果。

让我提供一个非常常见的代码示例

@Service
public class FeatureFlagHolder {
   private boolean featureFlagActivated = false;

   public void activateFeatureFlag() {
      featureFlagActivated = true;
   }

   // similar code to de-activate

   public boolean isFeatureFlagActivated() {
      return featureFlagActivated;
   }
}
Run Code Online (Sandbox Code Playgroud)

假设改变和读取状态的线程featureFlagActivated不同。AFAIK,读取布尔值的线程可以根据 JVM 缓存其值并且从不刷新它。在实践中,我从未见过这种情况发生。事实上,我什至从未见过布尔值在读取时没有立即更新。

这是为什么?

Joa*_*uer 5

在最基本的层面上,必须说缺乏并volatile 不能保证它会失败。它只是意味着允许 JVM 进行可能导致失败的优化。但这些优化是否发生以及是否会导致失败受到许多不同因素的影响。因此,通常很难真正发现这些问题,直到它们变成灾难性的。

首先,我想总结一下当出现问题时经常发生的情况

  • 非易失性变量通常在紧密循环中读取
  • 非易失性变量很少改变,但当它改变时,它在某种意义上是“重要的”。
  • 该循环内执行的代码量很小(大致小到足以被激进的编译器完全内联)
  • 紧密循环过度运行具有非常明显的效果(例如,它会导致异常,而不仅仅是默默地做不必要的工作)。

请注意,并非所有这些都是必要的,但当我实际观察该问题时,它们往往是正确的。

我个人的解释(加上一些关于该主题的阅读)引导我得出以下经验法则:

  • 如果读取错误的值不会被注意到,那么您根本不会注意到易失性是否丢失。如果发生的唯一不好的事情是您不必要地运行了几次循环,那么您很可能永远不会意识到它发生了。
  • 当对易失性变量的读取发生在它们之间有足够的“距离”时(其中距离是通过对内存其他部分的其他读取访问来测量的),那么它通常会表现得好像它是易失性的,仅仅是因为它从缓存中删除
  • 循环内任何内容的任何类型的同步都会至少导致某些缓存失效,从而导致变量表现得好像它是易失性的一样。

仅凭这三个因素就很难真正发现问题,除非在非常极端的情况下(即执行太多会导致系统严重崩溃)。

在您的具体示例中,我假设功能标志不会每秒切换多次。它更有可能是每个进程设置一次,然后保持不变。

例如,如果您在同一秒内有多个传入请求,并且在切换功能标志的那一秒中途有多个传入请求,则可能会发生切换后发生的某些请求仍将使用旧值,因为它是从之前缓存

你会注意到吗?不太可能。很难区分“此请求是在更改之前传入的”与“此请求是在更改之后传入的,并且错误地使用了旧值”。如果 10 个请求中有 6 个使用旧值,而不是 10 个请求中的 5 个使用正确的值,那么很可能没有人会注意到。