我查看了源代码java.util.hashmap并看到了以下代码:
public Set<K> keySet() {
Set<K> ks;
return (ks = keySet) == null ? (keySet = new KeySet()) : ks;
}
(Windows,Java版本" 1.8.0_111")
在我的MacBook上看起来像这样:
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
(MacOS X Sierra,Java版本" 1.8.0_121")
为什么两个变体都声明局部变量KS?为什么不是这样写的:
public Set<K> keySet() {
if (keySet == null) {
keySet = new KeySet();
}
return keySet;
}
或
public Set<K> keySet() {
return keySet == null ? (keySet = new KeySet()) : keySet;
}
javadoc有答案:
/**
* Since there is no synchronization performed while accessing these fields,
* it is expected that java.util.Map view classes using these fields have
* no non-final fields (or any fields at all except for outer-this). Adhering
* to this rule would make the races on these fields benign.
*
* It is also imperative that implementations read the field only once,
* as in:
*
* public Set<K> keySet() {
* Set<K> ks = keySet; // single racy read
* if (ks == null) {
* ks = new KeySet();
* keySet = ks;
* }
* return ks;
* }
*}
*/
transient Set<K> keySet;
据我所知,这是一个非常整洁的优化。
以前是这样写的:
if (keySet == null) { // volatile read
keySet = new AbstractSet<K>() { // volatile write
....
return keySet; // volatile read
这些操作无法重新排序,因为这里有一些内存障碍。因此看起来像这样:
[StoreLoad]
// volatile read
[LoadLoad]
[LoadStore]
[StoreStore]
[LoadStore]
// volatile write
[StoreLoad]
[StoreLoad] // there's probably just one barrier here instead of two
// volatile read
[LoadLoad]
[LoadStore]
这里有很多障碍,最昂贵的是在x86
上排放的StoreLoad
。
假设我们在此处删除volatile
。由于没有插入这些操作的障碍,因此可以以任何方式重新排序,并且在keySet
变量的这里有两次读取。
我们可以将一个简短的读数读取并将变量存储到本地字段(因为它们是本地的,它们是线程安全的 - 没有人可以更改本地声明的参考),据我所知,唯一的问题是,多个线程可能会同时看到零引用,并使用空的KeySet
初始化它,并可能进行太多的工作;但这很可能比障碍便宜。
另一方面,如果某些线程看到一个非编号引用,它将100%看到一个完全初始化的对象,这是有关final
字段的评论。如果所有对象都是最终的,则JMM保证在构造函数之后进行"冻结"动作;或用更简单的单词(IMO),如果所有字段均为最终并且在构造函数中初始化,则在其之后插入了两个障碍:LoadStore
和LoadLoad
;从而达到相同的效果。