一、为什么要是用ConcurrentHashMap
在并发编程中使用HashMap可能导致程序死循环(这是在JDK1.7中,在JDK1.8中导致的线程不安全主要是put方法可能会导致值被覆盖)。而使用线程安全的HashTable效率又非常低下(全表数据被synchronized修饰),基于以上两个原因,便有了ConcurrentHashMap的登场机会。
1、HashMap线程不安全
- JDK1.7
- 线程不安全主要体现在发生扩容的时候,重新哈希(rehash)的过程中出现了数据环路,导致的死循环。因为在1.7中,插入新的节点的操作是通过头插法实现的。A、B两个线程同时对HashMap进行扩容操作就容易形成链表的循环。
- 线程不安全主要体现在发生扩容的时候,重新哈希(rehash)的过程中出现了数据环路,导致的死循环。因为在1.7中,插入新的节点的操作是通过头插法实现的。A、B两个线程同时对HashMap进行扩容操作就容易形成链表的循环。
- JDK1.8
- 在JDK1.8的时候对环路问题进行了很好地解决,1.8采用的便是尾插法,而且,在JDK8中引入了红黑树的应用,当链表长度大于8并且数组长度大于等于64的时候的时候就换转化为红黑树。
- 上述死循环问题解决了,但是HashMap仍是线程不安全的。比如说在操作put()方法的时候,此时线程A和线程B同时进行添加操作
- 线程A执行完毕这行代码之后,因为时间片消耗完了,处于挂起状态,注意,此时A已经将数据插入到HashMap中;线程B执行,也进行了相同的操作,假如说他们两个的hashCode相同,便会将原有A插入的节点的值进行覆盖,从而导致线程不安全。
- 除此之外HashMap的size的数值也会受到影响(此时A、B插入不同的key),A线程执行到这里挂起,注意A进行了++(原来是10,现在变为了11)操作,B线程此前也拿到了原来的size值(10),也对他进行++操作(成为11),此时命名插入了两个节点,但是size的值只加了1,导致线程不安全。
2、Hashtable
Hashtable的线程是安全的,我们读他的源码就可以发现,Hashtable在几乎所有的方法上都加上了synchronized锁,从而避免线程不安全的情况,但是Hashtable正是由于这种全表加锁的情况,导致在并发操作的情况下错,导致性能特别低下,效率比较低。
二、ConcurrentHashMap的结构以及线程安全实现
基于以上两点的考虑,引入了ConcurrentHashMap,他作为Java并发的一种容器进行使用。ConcurrentHashMap在JDK1.7和JDK1.8里面也是不同的,我们分别来讲解。
-
JDK1.7
ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重 入锁(ReentrantLock),在ConcurrentHashMap里扮演锁的角色;HashEntry则用于存储键值对数 据。一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种 数组和链表结构。
- 使用分段锁的技术,提升了并发访问的效率。(启发于HashTable)
HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
- 使用分段锁的技术,提升了并发访问的效率。(启发于HashTable)
-
JDK1.8
- 在JDK1.8 ,ConcurrentHashMap主要通过Synchronized + CAS来实现线程安全。
- 其中initTable()用于里面table数组的初始化。table初始化是没有加锁的,当要初始化时会通过CAS操作将sizeCtl置为-1,而sizeCtl由volatile修饰,保证修改对后面线程可见。这之后如果再有线程执行到此方法时检测到sizeCtl为负数,说明已经有线程在给扩容了,这个线程就会调用Thread.yield()让出一次CPU执行时间。
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //正在初始化时将sizeCtl设为-1 try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); // 扩容的最大值为新容量的0.75倍 } } finally { sizeCtl = sc; } break; } } return tab; }
- put的流程解析
- 如果数组桶为空,初始化数组桶(自旋 + CAS)
- 如果桶内为空,CAS放入,不加锁,成功了就直接 break 跳出
- 如果桶内元素处于移动状态(扩容),就协助扩容
- 使用 synchronized 加锁加入节点
- 在JDK1.8 ,ConcurrentHashMap主要通过Synchronized + CAS来实现线程安全。