ConcurrentHashMap 是 Java 并发包 (java.util.concurrent) 中的一种线程安全的哈希表实现。HashMap 在多线程环境下扩容会出现 CPU 接近 100% 的情况因为 HashMap 并不是线程安全的我们可以通过 Collections 的Map synchronizedMap(Map m)将 HashMap 包装成一个线程安全的 map。比如 SynchronzedMap 的 put 方法源码就是加锁过的public V put(K key, V value) { synchronized (mutex) {return m.put(key, value);} }不过这并不是最优雅的方式。相对于 HashMapConcurrentHashMap 就是线程安全的 map其中利用了锁分段的思想大大提高了并发的效率。1.8 版本舍弃了 segment并且使用了大量的 synchronized以及 CAS 无锁操作以保证 ConcurrentHashMap 的线程安全性。为什么不用 ReentrantLock 而是 synchronzied 呢实际上synchronzied 做了很多的优化这个我们前面也讲过了包括偏向锁、轻量级锁、重量级锁可以依次向上升级锁状态因此synchronized 相较于 ReentrantLock 的性能其实差不多甚至在某些情况更优。ConcurrentHashMap 的变化ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中有一些区别。JDK 1.7ConcurrentHashMap 在 JDK 1.7 中提供了一种粒度更细的加锁机制这种机制叫分段锁「Lock Striping」。整个哈希表被分为多个段每个段都独立锁定。读取操作不需要锁写入操作仅锁定相关的段。这减小了锁冲突的几率从而提高了并发性能。这种机制的优点在并发环境下将实现更高的吞吐量而在单线程环境下只损失非常小的性能。可以这样理解分段锁就是将数据分段对每一段数据分配一把锁。当一个线程占用锁访问其中一个段数据的时候其他段的数据也能被其他线程访问。有些方法需要跨段比如size()、isEmpty()、containsValue()它们可能需要锁定整个表而不仅仅是某个段这需要按顺序锁定所有段操作完后再按顺序释放所有段的锁。如下图ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组构成的。Segment 是一种可重入的锁 ReentrantLockHashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组Segment 的结构和 HashMap 类似是一种数组和链表结构一个 Segment 里包含一个 HashEntry 数组每个 HashEntry 是一个链表结构的元素每个 Segment 守护着一个 HashEntry 数组里的元素当对 HashEntry 数组的数据进行修改时必须首先获得它对应的 Segment 锁。单一的 Segment 结构如下像这样的 Segment 对象在 ConcurrentHashMap 集合中有 2 的 N 次方个共同保存在一个名为 segments 的数组当中。 因此整个 ConcurrentHashMap 的结构如下可以说ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面有若干个子哈希表。Case1不同 Segment 的并发写入可以并发执行Case2同一 Segment 的一写一读可以并发执行Case3同一 Segment 的并发写入Segment 的写入是需要上锁的因此对同一 Segment 的并发写入会被阻塞。由此可见ConcurrentHashMap 中每个 Segment 各自持有一把锁。在保证线程安全的同时降低了锁的粒度让并发操作效率更高。ConcurrentHashMap 读写过程如下get 方法为输入的 Key 做 Hash 运算得到 hash 值。通过 hash 值定位到对应的 Segment 对象再次通过 hash 值定位到 Segment 当中数组的具体位置。put 方法为输入的 Key 做 Hash 运算得到 hash 值。通过 hash 值定位到对应的 Segment 对象获取可重入锁再次通过 hash 值定位到 Segment 当中数组的具体位置。插入或覆盖 HashEntry 对象。释放锁。JDK 1.8而在 JDK 1.8 中ConcurrentHashMap 主要做了两个优化同 HashMap 一样链表也会在长度达到 8 的时候转化为红黑树这样可以提升大量冲突时候的查询效率以某个位置的头结点链表的头结点或红黑树的 root 结点为锁配合自旋 CAS 避免不必要的锁开销进一步提升并发性能。相比 JDK1.7 中的 ConcurrentHashMapJDK1.8 中的 ConcurrentHashMap 取消了 Segment 分段锁采用 CAS synchronized 来保证并发安全性整个容器只分为一个 Segment即 table 数组。JDK1.8 中的 ConcurrentHashMap 对节点 Node 类中的共享变量和 JDK1.7 一样使用 volatile 关键字保证多线程操作时变量的可见性static class NodeK,V implements Map.EntryK,V { final int hash; final K key; volatile V val; volatile NodeK,V next; Node(int hash, K key, V val, NodeK,V next) { this.hash hash; this.key key; this.val val; this.next next; } ...... }ConcurrentHashMap 的字段1、tablevolatile Node[] table:装载 Node 的数组作为 ConcurrentHashMap 的底层容器采用懒加载的方式直到第一次插入数据的时候才会进行初始化操作数组的大小总是为 2 的幂次方。2、nextTablevolatile Node[] nextTable扩容时使用平时为 null只有在扩容的时候才为非 null3、sizeCtlvolatile int sizeCtl该属性用来控制 table 数组的大小根据是否初始化和是否正在扩容有几种情况当值为负数时如果为-1 表示正在初始化如果为 -N 则表示当前正有 N-1 个线程进行扩容操作当值为正数时如果当前数组为 null 的话表示 table 在初始化过程中sizeCtl 表示为需要新建数组的长度若已经初始化了表示当前数据容器table 数组可用容量也可以理解成临界值插入节点数超过了该临界值就需要扩容具体指为数组的长度 n 乘以 加载因子 loadFactor当值为 0 时即数组长度为默认初始值。4、sun.misc.Unsafe U在 ConcurrentHashMap 的实现中可以看到用了大量的U.compareAndSwapXXXX方法去修改 ConcurrentHashMap 的一些属性。这些方法实际上是利用了 CAS 算法用于保证线程安全性这是一种乐观策略假设每一次操作都不会产生冲突当且仅当冲突发生的时候再去尝试。我们前面也讲过了CAS 操作依赖于现代处理器指令集通过底层的CMPXCHG指令实现。CAS(V,O,N)核心思想为若当前变量实际值 V 与期望的旧值 O 相同则表明该变量没被其他线程进行修改因此可以安全的将新值 N 赋值给变量若当前变量实际值 V 与期望的旧值 O 不相同则表明该变量已经被其他线程做了处理此时将新值 N 赋给变量操作就是不安全的在进行重试。在并发容器中CAS 是通过sun.misc.Unsafe类实现的该类提供了一些可以直接操控内存和线程的底层操作可以理解为 Java 中的“指针”。该成员变量的获取是在静态代码块中static { try { U sun.misc.Unsafe.getUnsafe(); ....... } catch (Exception e) { throw new Error(e); } }以上是第一部分的主要内容。