首先,它没有可靠地失败。我设法进行了一些没有发生异常的运行。但是,这并不意味着生成的地图是正确的。也有可能每个线程都见证了自己的值被成功放置,而生成的映射错过了几个映射。
但事实上,失败的发生率很高。我创建了以下调试代码来说明 的工作原理:NullPointerException
HashMap
static <K,V> void debugPut(HashMap<K,V> m, K k, V v) {
if(m.isEmpty()) debug(m);
m.put(k, v);
debug(m);
}
private static <K, V> void debug(HashMap<K, V> m) {
for(Field f: FIELDS) try {
System.out.println(f.getName()+": "+f.get(m));
} catch(ReflectiveOperationException ex) {
throw new AssertionError(ex);
}
System.out.println();
}
static final Field[] FIELDS;
static {
String[] name={ "table", "size", "threshold" };
Field[] f=new Field[name.length];
for (int ix = 0; ix < name.length; ix++) try {
f[ix]=HashMap.class.getDeclaredField(name[ix]);
}
catch (NoSuchFieldException ex) {
throw new ExceptionInInitializerError(ex);
}
AccessibleObject.setAccessible(f, true);
FIELDS=f;
}
使用这个与简单的顺序打印:for(int i=0; i<5; i++) debugPut(m, i, i);
table: null
size: 0
threshold: 1
table: [Ljava.util.HashMap$Node;@70dea4e
size: 1
threshold: 1
table: [Ljava.util.HashMap$Node;@5c647e05
size: 2
threshold: 3
table: [Ljava.util.HashMap$Node;@5c647e05
size: 3
threshold: 3
table: [Ljava.util.HashMap$Node;@33909752
size: 4
threshold: 6
table: [Ljava.util.HashMap$Node;@33909752
size: 5
threshold: 6
如您所见,由于 的初始容量,即使在顺序操作期间也创建了三个不同的后备阵列。每次增加容量时,不规则并发错过阵列更新并创建自己的阵列的可能性就越高。0
put
这对于空映射的初始状态和尝试放置其第一个键的多个线程尤其重要,因为所有线程都可能遇到表的初始状态并创建自己的表。此外,即使读取已完成的第一个的状态,也会为第二个数组创建一个新数组。null
put
put
但是,逐步调试揭示了更多的中断机会:
在 putVal 方法
内部,我们看到最后:
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
换句话说,在成功插入新键后,如果新大小超过 .因此,在第一个 ,在开始时调用,因为该表是,并且由于您指定的初始容量是,即太低而无法存储一个映射,因此新容量将是,而新容量将是,四舍五入为 。因此,在第一个操作结束时,新的操作被超越并触发。因此,初始容量为 ,第一个已经创建并填充了两个数组,如果多个线程同时执行此操作,则中断的机会要高得多,并且所有线程都遇到初始状态。threshold
put
resize()
null
0
1
threshold
1 * loadFactor == 1 * 0.75f == 0.75f
0
put
threshold
resize()
0
put
还有一点。查看 resize()
操作,我们看到以下行:
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
… (transfer old contents to new array)
换句话说,新数组引用在用旧条目填充之前会存储到堆中,因此即使没有对读取和写入进行重新排序,也有可能另一个线程读取该引用而不看到旧条目,包括它之前自己编写的条目。实际上,减少堆访问的优化可能会降低线程在紧随其后的查询中看不到自己的更新的可能性。
尽管如此,还必须指出,在这里解释所有内容的假设是没有根据的。由于 JRE 也在内部使用,甚至在应用程序启动之前,在使用 时也有可能遇到已编译的代码。HashMap
HashMap