并发代码中赋值运算符的返回值

Posted

技术标签:

【中文标题】并发代码中赋值运算符的返回值【英文标题】:Return value of assignment operator in concurrent code 【发布时间】:2012-10-02 18:34:15 【问题描述】:

给定以下类:

class Foo 
  public volatile int number;

  public int method1() 
    int ret = number = 1;
    return ret;
  

  public int method2() 
    int ret = number = 2;
    return ret;
  

如果多个线程在同一个Foo 实例上同时调用method1()method2(),那么对method1() 的调用是否可以返回1 以外的任何值?

【问题讨论】:

【参考方案1】:

我认为答案取决于编译器。语言specifies:

在运行时,赋值表达式的结果是赋值发生后变量的值。

我想理论上这个值可以在第二个(最左边的)赋值发生之前改变。

但是,使用 Sun 的 javac 编译器,method1 将变成:

0:   aload_0
1:   iconst_1
2:   dup_x1
3:   putfield        #2; //Field number:I
6:   istore_1
7:   iload_1
8:   ireturn

这会复制堆栈上的常量1,并将其加载到number,然后再加载到ret,然后返回ret。在这种情况下,在分配给ret之前是否修改了存储在number中的值并不重要,因为分配的是1,而不是number

【讨论】:

该语言令人困惑,因为它似乎暗示使用了变量的“值”,似乎暗示可以进行第二次读取? 字节码确实很有趣,但不能证明规范要求是什么。 @BeeOnRope - 从表面上看,在我看来,编译器生成的代码并不严格符合语言规范。但是,“在分配发生之后”可能意味着“立即之后”,并且其他线程对变量的任何进一步更改都不会影响结果。如果这就是意思(我不清楚它是什么意思),那么 javac 正在生成正确的代码,将值与 number 的并发更改隔离开来。 @Ted Hopp 也许您应该从规范中添加以下句子。它可能会澄清一些事情。 好吧,即使由于重新读取数字而该语言确实允许返回值 2,但我认为您显示的代码并没有超出规范,因为在语句编号的生命周期必须是 1。如果值在语句范围内发生变化,则没有任何其他关于应该选择什么值的保证,选择 1 似乎很好。【参考方案2】:

JLS 15.26 规定:

有12个赋值运算符;所有在语法上都是右关联的(它们从右到左分组)。因此,a=b=c 表示 a=(b=c),将 c 的值赋给 b,然后将 b 的值赋给 a。

Ted Hopp 的回答表明 Sun 的 javac 没有遵循这种行为,可能是作为一种优化。

由于这里的线程,method1 的行为将是未定义的。如果 Sun 的编译器使行为保持不变,那么它不会中断未定义的行为。

【讨论】:

volatile fields 是否允许这种优化? 我很好奇,如果这是规范的意图。我认为他们可能已经在“然后将 b 的值分配给 a”中指定了 b,只是为了明确在 a、b 和 c 具有不同类型(例如原始整数/浮点类型)并且发生转换的情况下,赋值就像使用了转换后的值一样。例如,如果 a 和 c 是双精度数,而 b 是浮点数。 @BeeOnRope 这很有可能——JLS 页面似乎更关心类型转换而不是易失性字段。 我接受了你的回答,因为我不能凭良心接受最高投票的答案(对不起,Ted - 虽然我也支持你),因为“这个编译器的作用”没有告诉您该语言实际提供的保证。 FWIW,自从我问这个问题后,我就使用了这个成语,假设 not 读取了number,并且总是分配最右边的值(在本例中为 1 或 2)到最右边=左边的所有变量,因为这样写干净整洁,而且我从未见过一个编译器在读取时编译。 ...但我同意@Bringer128 的观点,即我在 JLS 中找不到严格支持这种行为的语言。【参考方案3】:

语句要么包含易失性读取,要么不包含易失性读取。这里不能有任何歧义,因为 volatile read 对于程序语义非常重要。

如果 javac 是可信的,我们可以得出结论,该语句不涉及对 number 的易失性读取。赋值表达式x=y 的值实际上就是y 的值(转换后)。

我们也可以推断出来

    System.out.println(number=1);

不涉及阅读number

    String s;

    (s="hello").length();

不涉及阅读s

    x_1=x_2=...x_n=v

不涉及阅读x_n, x_n-1, ...;相反,v 的值直接分配给x_i(通过x_n, ... x_i 的类型进行必要的转换后)

【讨论】:

我希望你是对的,但规范肯定不会对这种解释给予太多重视,是吗?我认为 volatile 是一个红鲱鱼。它可能包含非易失性读取,并且仍然使用线程做奇怪的事情(易失性意味着某些事情必须发生,但同样的事情可以通过正常读取发生)。 这个答案在我看来是正确的。 JLS 确实明确指出 a = b = c 等同于 b = c; a = b; 但他们似乎已将其实现为 b = c; a = c; 以防止需要缓慢的易失性字段检索。话虽如此,引用的 JLS 参考可能只是一个糟糕的例子,而不是编译器必须这样做参考。

以上是关于并发代码中赋值运算符的返回值的主要内容,如果未能解决你的问题,请参考以下文章

如何使用具有多个返回值的python赋值运算符

C++中赋值运算操作符和=重载有啥区别?

python自增自减?赋值语句返回值?逗号表达式?

赋值函数(运算符重载)

类和对象—4

简写赋值运算符,+=,真正的含义?