Java 8 ConcurrentHashMap

我发现ConcurrentHashMap已经在Java 8中完全重写为更“无锁”。 我浏览了get()方法的代码,看到没有明确的锁机制:

 public V get(Object key) { Node[] tab; Node e, p; int n, eh; K ek; int h = spread(key.hashCode()); if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } else if (eh < 0) return (p = e.find(h, key)) != null ? p.val : null; while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; } 

题:

如何从一个线程中看到从其他线程对此hashmap进行的修改,因为代码不在同步保护伞下(这会强制执行先前发生的关系)?

注意:整个ConcurrentHashMap是表的包装器: transient volatile Node[] table;

所以table是对数组的易变引用,而不是对volatile元素数组的引用! 这意味着如果有人正在更新此数组中的元素,则在其他线程中将看不到修改。

简短的回答

Node#valvolatile ,它在订购之前确定您的发生。

更长的答案

synchronized不是线程安全的必要条件,它是工具箱中的一个工具,可以使系统线程安全。 您将不得不考虑此ConcurrentHashMap上的一整套操作来推断线程安全性。

知道原始的ConcurrentHashMap也是非阻塞的,这很有用。 注意Java-CHM之前的版本

 V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // ignore this } e = e.next; } } return null; } 

在这种情况下,没有阻塞,那么它是如何工作的? HashEntry#valuevolatile 。 这是线程安全的同步点。

CHM-8的Node类是相同的。

 static class Node implements Map.Entry { final int hash; final K key; volatile V val; volatile Node next; 

因此,在这种情况下,非零的val应该确保在关于put之前的动作的关系之前发生。

该文档未声明同步发生。 例如它说明

[…]汇总操作(如putAllclear ,并发检索)可能反映仅插入或删除某些条目。

换句话说,允许并发使用和提供同步访问之间存在差异。

Java语言规范写道 :

如果我们有两个动作x和y,我们写hb(x,y)来表示x发生在y之前。

  • 如果x和y是同一个线程的动作,并且x在程序顺序中出现在y之前,那么hb(x,y)。

  • 从对象的构造函数的末尾到该对象的终结器(第12.6节)的开始有一个发生前的边缘。

  • 如果动作x与后续动作y同步,那么我们也有hb(x,y)。

  • 如果是hb(x,y)和hb(y,z),那么hb(x,z)。

并定义

同步动作引发与动作的同步关系,定义如下:

  • 监视器m上的解锁动作与m上的所有后续锁定动作同步(其中“后续”根据同步顺序定义)。

  • 对易失性变量v(第8.3.1.4节)的写入与任何线程对v的所有后续读取同步(其中“后续”根据同步顺序定义)。

  • 启动线程的操作与其启动的线程中的第一个操作同步。

  • 向每个变量写入默认值(零,false或null)与每个线程中的第一个操作同步。

    虽然在分配包含变量的对象之前将默认值写入变量似乎有点奇怪,但从概念上讲,每个对象都是在程序开始时使用其默认初始化值创建的。

  • 线程T1中的最终操作与另一个检测到T1已终止的线程T2中的任何操作同步。

    T2可以通过调用T1.isAlive()或T1.join()来完成此操作。

  • 如果线程T1中断线程T2,则T1的中断与任何其他线程(包括T2)确定T2已被中断的任何点同步(通过抛出InterruptedException或通过调用Thread.interrupted或Thread.isInterrupted)。

也就是说,读取易失性字段确定发生 – 就像显式锁定一样。