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

Java 为什么以下程序可以在JVM内存模型的JLS规范下工作,java,jvm,Java,Jvm,关于JLS 3,17.5最终字段语义的第二部分讨论: 据说myS可以等同于“/tmp”,我个人无法理解这一点。有人能解释得更清楚吗?另一点是这个例子告诉我们的,这是否意味着如果我们想在多线程之间共享Global.s,我们需要将其设置为final(如果是final,则在构造后不能更改),或者在读写时需要同步?或者声明一个长度为1和final的字符串数组,以便可以更改和共享 JLS中的原始内容: 考虑下面的例子。一个线程(我们称之为线程1)执行 另一个线程(线程2)执行时 JLS中的说明: 字符

关于JLS 3,17.5最终字段语义的第二部分讨论:


据说myS可以等同于“/tmp”,我个人无法理解这一点。有人能解释得更清楚吗?另一点是这个例子告诉我们的,这是否意味着如果我们想在多线程之间共享Global.s,我们需要将其设置为final(如果是final,则在构造后不能更改),或者在读写时需要同步?或者声明一个长度为1和final的字符串数组,以便可以更改和共享

JLS中的原始内容:


考虑下面的例子。一个线程(我们称之为线程1)执行

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


JLS中的说明:

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

这取决于
“/tmp/usr”的时间。子字符串(4)
执行

Global.s = "/tmp/usr".substring(4);
实际执行为:

Global.s = "/tmp/usr";
s = s.substring(4);
如果线程2在执行该代码之前或(不太可能)在这两行之间查找
Global.s
,它将看到
s
不是
“/usr/”
,而是
null
”/tmp/usr“
,具体取决于时间

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

这取决于何时执行子字符串(4)“/tmp/usr”

Global.s = "/tmp/usr".substring(4);
实际执行为:

Global.s = "/tmp/usr";
s = s.substring(4);
如果线程2在执行该代码之前或(不太可能)在这两行之间查找
Global.s
,它将看到
s
不是
“/usr/”
,而是
null
”/tmp/usr“
,具体取决于时间

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


字符串使用字符[]、偏移量和计数实现。substring方法使用相同的char[]和新的偏移量和计数构造一个新的String对象。根据执行语义,当线程2尝试访问新字符串时,新字符串可能会被部分初始化。根据,substring返回一个使用简单构造函数构造的新字符串对象:

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       }

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

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

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       }

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

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

它的要点是,如果没有特殊的
final
语义,一个线程可能会看到另一个线程在不一致状态下创建的不可变对象。特殊的
final
语义阻止了这一点;看见在这种情况下,String类的3个内部字段是final,这意味着String构造函数的结尾和对结果字符串的任何操作之间存在“先到先”关系


但联合联络小组说可以是“tmp”,我不明白

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

但是内存模型不能保证未同步的线程会按照字段更新的顺序看到字段更新。特别是,它可能会看到状态为
{offset:0,count:4,chars:“/tmp/usr.chars}
”的字符串。。。i、 e.“/tmp”


(事实上,这并没有发生,因为
final
字段的特殊语义,如下面的示例所述。)

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

它的要点是,如果没有特殊的
final
语义,一个线程可能会看到另一个线程在不一致状态下创建的不可变对象。特殊的
final
语义阻止了这一点;看见在