并发代码中赋值运算符的返回值
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 参考可能只是一个糟糕的例子,而不是编译器必须这样做参考。以上是关于并发代码中赋值运算符的返回值的主要内容,如果未能解决你的问题,请参考以下文章