为什么以下程序可以在JVM内存模型的JLS规范下工作

时间:2011-07-10 02:39:14

标签: java jvm

on JLS 3,17.5 Final Field Semantics的第二个讨论部分:http://java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5

据说myS可以等于“/ tmp”,我个人无法理解这一点。任何人都可以给出更多解释?另一点是这个例子告诉的,这是否意味着如果我们想在多线程之间共享Global.s,我们需要使它最终(如果是最终的,那么在构造之后就不能改变)或者需要在读取和写入时进行同步?或声明一个长度为1和final的String数组,以便可以更改和共享??

JLS中的原始内容:


考虑以下示例。一个线程(我们将称之为线程1)执行

Global.s = "/tmp/usr".substring(4);

而另一个线程(线程2)执行

String myS = Global.s;
if (myS.equals("/tmp"))System.out.println(myS);

JLS中的解释:

String对象旨在不可变,字符串操作不执行同步。虽然String实现没有任何数据争用,但其他代码可能涉及使用字符串的数据争用,而内存模型对具有数据争用的程序提供弱保证。特别是,如果String类的字段不是final,那么可能(尽管不太可能)Thread 2最初可以看到字符串对象偏移的默认值0,允许它比较为等于“ / tmp目录”。对String对象的后续操作可能会看到4的正确偏移量,因此String对象被视为“/ usr”。 Java编程语言的许多安全功能依赖于Strings被认为是真正不可变的,即使恶意代码使用数据竞争在线程之间传递String引用。

3 个答案:

答案 0 :(得分:2)

使用char [],offset和count实现字符串。 substring方法构造一个具有相同char []和新偏移量和计数的新String对象。基于执行语义,当线程2尝试访问它时,可以部分初始化新的String。根据{{​​3}},substring返回一个使用简单构造函数构造的新String对象:

644       // Package private constructor which shares value array for speed.
645       String(int offset, int count, char value[]) {
646           this.value = value;
647           this.offset = offset;
648           this.count = count;
649       }

因此,在String类定义中没有将char [],offset和count标记为final,则线程2在访问它们时可能会看到不一致的值。如果发生这种情况,则可以设置char [],但偏移和计数可能是错误的。如果offset仍显示为默认值0,则会看到整个字符串。当然,它需要惊人的时机,JIT指令的具体重新排序,以及实现这一目标的一大堆“运气”。

答案 1 :(得分:1)

"/tmp/usr".substring(4)执行时,它取决于

Global.s = "/tmp/usr".substring(4);

实际执行为:

Global.s = "/tmp/usr";
s = s.substring(4);

如果线程2在代码执行之前查找Global.s ,或者这两行之间(非常不可能),它将会看到s不是"/usr/",而是null"/tmp/usr",具体取决于时间。

字符串是不可变的,但对字符串的引用不是:

String str = "A"; // "A" is immutable
str = "B";        // variable "str" may be changed to refer to a different String

答案 2 :(得分:0)

您需要在上下文中阅读该示例和说明。上下文是它解释了为什么Java定义final字段具有与内存模型相关的特殊语义。

它的意思是没有特殊的final语义,一个线程可以看到另一个线程在不一致状态下创建的不可变对象。特殊的final语义阻止了这一点;见JLS 17.5.1。在这种情况下,String类的3个内部字段为final的事实意味着String构造函数的结尾与生成的String上的任何操作之间存在先前的关系。


  

但是JLS说它可能是“/ tmp”,我无法理解。

子字符串操作旨在创建状态为{offset: 4, count: 4, chars: "/tmp/usr".chars}对象。

但是内存模型并不能保证非同步线程会按照它们的生成顺序看到字段更新。特别是,它可能会看到状态{offset: 0, count: 4, chars: "/tmp/usr".chars}中的字符串...即“/ tmp”。

(事实上,由于final字段的特殊语义,这不会发生,如下面的示例所示。)