Java Spring 应用程序中缺少 volatile 变量及其后果
lack of volatile variables in Java Spring applications and its consequences
那些开发过专业的多线程 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;
}
}
假设改变和读取featureFlagActivated
状态的线程是不同的。读取布尔值的线程可以,AFAIK,根据 JVM 缓存它的值并且从不刷新它。实际上,我从未见过这种情况发生。实际上,我什至从未见过布尔值在读取时没有立即更新。
这是为什么?
在最基本的层面上,必须说缺少 volatile
并不能保证它会失败 。这只是意味着允许 JVM 进行 可能 导致失败的优化。但是这些优化是否发生以及它们是否会导致失败受到许多不同因素的影响。因此,通常很难真正检测到这些问题,直到它们变成灾难性的。
首先,我想总结一下 出错时经常发生的情况。
- 通常在紧密循环中读取 non-volatile 变量
- non-volatile 变量很少更改,但是当它更改时在某种意义上是“重要的”。
- 该循环内执行的代码量很小(大致小到可以被激进的编译器完全内联)
- 紧密循环 over-running 具有非常明显的效果(例如,它会导致异常,而不仅仅是默默地做不必要的工作)。
请注意,并非所有这些都是必要的,但当我实际观察到该问题时,它们往往是正确的。
我个人的解释(加上一些关于该主题的阅读)使我得出了这些经验法则:
- 如果读取错误的值不会被注意到,那么您根本不会注意到 volatile 是否丢失。如果发生的唯一坏事是您 运行 不必要地循环了几次,那么您可能永远不会意识到它的发生。
- 当 volatile 变量的读取发生在它们之间有足够的“距离”时(其中距离由对内存其他部分的其他读取访问来测量)那么它通常表现得好像它是 volatile 的,仅仅是因为它下降了超出缓存
- 在循环内 anything 上的任何类型的同步往往会至少使 some 缓存无效,从而导致变量表现得好像它是不稳定的。
仅这三个就很难真正发现问题,除非在非常极端的情况下(即执行一次太多会导致系统严重崩溃)。
在您的具体示例中,我假设功能标志不会每秒切换多次。它更可能是每个进程设置一次,然后保持不变。
例如,如果您在同一秒内有多个传入请求,并且在第二秒的中途切换功能标志,则可能会发生 一些 之后发生的请求切换仍将使用旧值,因为它已从早期缓存。
你会注意到吗?不太可能。很难区分“这个请求在更改之前出现”和“这个请求在更改之后出现并且错误地使用了旧值”。如果 10 个请求中有 6 个使用旧值而不是 10 个请求中的 5 个使用正确的值,那么很可能没有人会注意到。
那些开发过专业的多线程 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;
}
}
假设改变和读取featureFlagActivated
状态的线程是不同的。读取布尔值的线程可以,AFAIK,根据 JVM 缓存它的值并且从不刷新它。实际上,我从未见过这种情况发生。实际上,我什至从未见过布尔值在读取时没有立即更新。
这是为什么?
在最基本的层面上,必须说缺少 volatile
并不能保证它会失败 。这只是意味着允许 JVM 进行 可能 导致失败的优化。但是这些优化是否发生以及它们是否会导致失败受到许多不同因素的影响。因此,通常很难真正检测到这些问题,直到它们变成灾难性的。
首先,我想总结一下 出错时经常发生的情况。
- 通常在紧密循环中读取 non-volatile 变量
- non-volatile 变量很少更改,但是当它更改时在某种意义上是“重要的”。
- 该循环内执行的代码量很小(大致小到可以被激进的编译器完全内联)
- 紧密循环 over-running 具有非常明显的效果(例如,它会导致异常,而不仅仅是默默地做不必要的工作)。
请注意,并非所有这些都是必要的,但当我实际观察到该问题时,它们往往是正确的。
我个人的解释(加上一些关于该主题的阅读)使我得出了这些经验法则:
- 如果读取错误的值不会被注意到,那么您根本不会注意到 volatile 是否丢失。如果发生的唯一坏事是您 运行 不必要地循环了几次,那么您可能永远不会意识到它的发生。
- 当 volatile 变量的读取发生在它们之间有足够的“距离”时(其中距离由对内存其他部分的其他读取访问来测量)那么它通常表现得好像它是 volatile 的,仅仅是因为它下降了超出缓存
- 在循环内 anything 上的任何类型的同步往往会至少使 some 缓存无效,从而导致变量表现得好像它是不稳定的。
仅这三个就很难真正发现问题,除非在非常极端的情况下(即执行一次太多会导致系统严重崩溃)。
在您的具体示例中,我假设功能标志不会每秒切换多次。它更可能是每个进程设置一次,然后保持不变。
例如,如果您在同一秒内有多个传入请求,并且在第二秒的中途切换功能标志,则可能会发生 一些 之后发生的请求切换仍将使用旧值,因为它已从早期缓存。
你会注意到吗?不太可能。很难区分“这个请求在更改之前出现”和“这个请求在更改之后出现并且错误地使用了旧值”。如果 10 个请求中有 6 个使用旧值而不是 10 个请求中的 5 个使用正确的值,那么很可能没有人会注意到。