切换运算符是原子的吗?

Posted

技术标签:

【中文标题】切换运算符是原子的吗?【英文标题】:Is switch operator atomic? 【发布时间】:2017-06-14 22:07:32 【问题描述】:

在文档中说你可以同样使用if-else 多次或switch-case

int condition;

setCondition(int condition) 
    this.condition = condition;

任一开关盒

switch (condition) 
  case 1: print("one"); break;
  case 2: print("two"); break;

if (condition == 1)  print("one"); 
else if (condition == 2)  print("two"); 

接下来,condition 被声明为 volatile 并从多个线程调用方法 setCondition()If-else 不是原子的,volatile 变量写入是同步操作。所以“one”和“two”字符串都可以打印在最后一个代码中。

如果使用了一些具有初始值的方法局部变量,则可以避免:

int localCondition = condition;
if (local condition == ..) ..

switch-case 操作员是否持有一些变量的初始副本?它是如何实现跨线程操作的?

【问题讨论】:

您的假设是错误的:If/else 保证即使在竞争条件下也只采用一个分支。这只有在竞争条件导致未定义行为的 c 等语言中才有可能。 @Boris if/else ifif/if 的全部意义在于事实并非如此。 @Voo 我明白你的意思 - 我很厚。把它归咎于周日的宿醉。我想如果值在评估期间发生变化,它可能会打印 nothing - 这是switch 会阻止的竞争条件。 which 文档中说明了多个 if-else 与 switch-case?也许它是一份非正式的文档,涵盖了语言基础知识,但不包括多线程的微妙之处 为保护@Voo 的宿醉,他断言“if/else 保证即使在竞争条件下[确切] 一个分支也会被采用”,这绝不是不真实的。后续地址 if/else ifif/if,虽然可能是切线相关的,但不会改变声明的声明。 【参考方案1】:

来自Java specification on switch 语句:

执行 switch 语句时,首先计算表达式。 [...]

建议表达式被评估一次,结果暂时保存在其他地方,因此不可能出现竞争条件。

但我在任何地方都找不到明确的答案。


快速测试表明情况确实如此:

public class Main 
  private static int i = 0;

  public static void main(String[] args) 
    switch(sideEffect()) 
      case 0:
        System.out.println("0");
        break;
      case 1:
        System.out.println("1");
        break;
      default:
        System.out.println("something else");
    

    System.out.println(i); //this prints 1
  

  private static int sideEffect() 
    return i++;
  

确实,sideEffect() 只被调用一次。

【讨论】:

换个角度想:如果我们写switch(someFunctionWithSideEffect()),那么switch语句最好只评估一次函数 @Todd Sewell "...表达式被评估..." 没有任何建议。这意味着表达式被评估 [根据 JVM 的规范],这意味着给定 JVM 的表达式评估器的实现者不得破坏易失性访问规则。因此,它应该受制于当前正在执行的线程的相同条件。 +1。奇怪的是,虽然规范确实定义了名词“读”和“写”等等,但并没有真正详细说明:docs.oracle.com/javase/specs/jls/se8/html/…。但除了longdouble 的特殊情况之外,它们似乎应该正是你天真地期望的意思;例如,似乎很明显,像int foo = this.mFoo; return foo + foo; 这样的东西只允许执行一个“读取”this.mFoo 我不是 Java 人,但这里的主题称为语言的正式memory model(如果有的话)。这些是语言对内存访问所做的保证,正如这里所指出的,对于给定并发性的正确性而言,这些保证变得很重要——并非必不可少。 @Glenn Slayden,java 有内存模型(参见 JMM),我认为问题是关于它的。但就 switch() 使用局部变量(评估表达式值)而言,这个问题似乎不在 JMM 范围内。请参阅 Shipilev 关于 JMM 的讲座:shipilev.net/blog/2014/jmm-pragmatics“内存模型似乎回答了一个简单的问题:“特定读取可以观察到哪些值?””【参考方案2】:

表达式在进入开关时被计算一次

开关可以根据需要在内部使用结果多次以确定要跳转到的代码。类似于:

int switchValue = <some expression>;
if (switchValue == <some case>)
    <do something>
else if (switchValue == <some other case>
    <do something else>
// etc

其实一个switch会根据case的数量和值的类型,编译成多种字节码样式。

开关只需要计算一次表达式。

【讨论】:

如果表达式(和大小写标签)是字符串,这是真的吗? @carlos 是的。字符串是不可变的。一旦你引用了 switch 的 String 表达式,它实际上就是一个常量 @Bohemian 如果您的说法是“从 x 跟随 y”但 x 不正确,那么整个解释都是可疑的。是的,交换机只能有一个真正的分支,但那是因为 JLS 需要它,而不是因为涉及到跳转表(因为实际上绝大多数交换机都没有跳转表)。但是,是的,我删除了我的评论,因为我想让这个更清楚。一般来说,用实现细节解释合同行为是非常危险的(尤其是很多损坏的并发代码可以追溯到那个) 只是让我困惑的“开关只需要使用表达式一次”:表达式的值对Strings使用了两次,但只计算了一次...... @voo 很好...我已经将答案的实现换成了另一个(类似于switch 可以做的事情。

以上是关于切换运算符是原子的吗?的主要内容,如果未能解决你的问题,请参考以下文章

美团2020面试题:基于volatile变量的运算在并发下是安全的吗?

向量引用运算符 [] 是线程安全的吗?

+= 运算符在 Python 中是线程安全的吗?

隐式生成的赋值运算符应该是 & ref 限定的吗?

R:lts回归:$运算符对原子向量无效

“子集”和“$ 运算符对原子向量无效”