不应该myS.equals("/usr")
在JLS 的解释中吗?
最终字段旨在允许必要的安全保证。考虑以下示例。一个线程(我们称之为线程 1)执行
Global.s = "/tmp/usr".substring(4);
而另一个线程(线程 2)执行
String myS = Global.s; if (myS.equals("/tmp"))System.out.println(myS);
字符串对象旨在是不可变的,并且字符串操作不执行同步。
他们正在描述一种假设情况,在这种情况下,由于竞争条件,返回的子字符串可能看起来是 /tmp 或 /usr。因此,使用哪个字符串进行比较并不重要。该示例的要点是,如果此示例中描述的条件成立,则任何一个都可能是正确的。
实际上不,这可能是您乍一看的想法,但这是故意的。进一步引用文本(强调我的):
[...] 如果 String 类的字段不是最终的,那么线程 2 有可能(尽管不太可能)最初看到字符串对象的偏移量的默认值 0,从而允许它比较相等到“/tmp”
我看不出有什么不对。两个代码片段都指向不同的线程,并举例说明了Java中String类的真正不变性。之前,线程2执行时字符串的值为“/THREADS....”,然后被线程1改成“/usr”,描述中解释的很清楚。
不,不应该。
这个例子是一个很好的例子,因为它提到了一些 JVM 实现中存在的错误(不幸的是我不记得是哪个)。String.substring
没有创建新字符串,而是指向旧字符串,其中非最终字符串offset
和length
指向正确位置的字段。但是,由于字段不是最终的(并且没有其他同步),可能发生的正是示例代码后面的段落中提到的场景:
特别是,如果 String 类的字段不是最终的,那么线程 2 有可能(尽管不太可能)最初看到字符串对象的偏移量的默认值 0,允许它比较等于 " /tmp”。稍后对 String 对象的操作可能会看到正确的偏移量 4,因此 String 对象被视为“/usr”。
所以事实上,虽然字符串被认为是不可变的,但它们并不是因为对象在其构造函数完全运行之前对其他线程可见。这个例子说明了这一点。
String
s 内部是一个带有偏移量和长度的字符数组。这个字符数组曾经在多个字符串之间重用。作为内存使用优化,substring()
将返回String
由与原始字符串相同的字符数组支持的新字符串。这个方法的作用是确定结果字符串在这个字符数组中的新偏移量和长度是多少,然后调用一个私有构造函数来创建这个结果对象并返回它。
(正如 Joachim 指出的那样,这种方式不再String
有效,但 JLS 中的示例是基于旧的内部结构。)
现在,该私有构造函数的实现、JIT 或 CPU 的指令重新排序,或者只是线程之间共享内存工作的一般古怪方式可能导致这个新String
对象首先将其长度设置为4
. 偏移量将保持在0
,从而形成 this 的值String
"/tmp"
。只有一瞬间,它的偏移量才会被设置为4
,并使其值正确地变为"/usr"
。
换句话说:线程 2将能够在其构造函数执行过程中观察到该字符串。这似乎违反直觉,因为人们直观地将线程 1的代码理解为首先完全执行赋值的右侧,然后才更改Global.s
. 不幸的是,如果没有适当的内存同步,其他线程能够观察到不同的事件序列。使用final
字段是使 JVM 正确处理此问题的一种方法。(我相信声明Global.s
asvolatile
也可以,但我不是 100% 确定。)