Java容器:HashTable, synchronizedMap与ConcurrentHashMap

首先须要明确的是,无论使用那种Map,都不能保证公共混合调用的线程安全,只能保证单条操做的线程安全,在这一点上各Map不存在优劣。html

前文中简单说过HashTable和synchronizedMap,其实这两个类不须要说太多,把代码贴一下相信看过Java多线程的就能很容易理解了。java

HashTable

HashTable的话,实现这个样子的。能够看到的是,对于Hash表的全部操做,HashTable都加了锁,但也只能保证单条操做的线程安全。数组

public synchronized V get(Object key) {
       // 省略实现
}
public synchronized V put(K key, V value) {
    // 省略实现
}

synchronizedMap

synchronizedMap的实现以下,没直接在方法上加,尽管其实质与HashTable是等效的,也一样有HashTable的缺陷,但synchronizedMap给用户留下了选择的空间:用户能够在不须要加锁时直接操做原始Map,在实际编码时就能够基于这点进行优化。安全

// synchronizedMap方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
       return new SynchronizedMap<>(m);
   }
// SynchronizedMap类
private static class SynchronizedMap<K,V>
       implements Map<K,V>, Serializable {
       private static final long serialVersionUID = 1978198479659022715L;

       private final Map<K,V> m;     // Backing Map
       final Object      mutex;        // Object on which to synchronize

       SynchronizedMap(Map<K,V> m) {
           this.m = Objects.requireNonNull(m);
           mutex = this;
       }

       SynchronizedMap(Map<K,V> m, Object mutex) {
           this.m = m;
           this.mutex = mutex;
       }

       public int size() {
           synchronized (mutex) {return m.size();}
       }
       public boolean isEmpty() {
           synchronized (mutex) {return m.isEmpty();}
       }
       public boolean containsKey(Object key) {
           synchronized (mutex) {return m.containsKey(key);}
       }
       public boolean containsValue(Object value) {
           synchronized (mutex) {return m.containsValue(value);}
       }
       public V get(Object key) {
           synchronized (mutex) {return m.get(key);}
       }

       public V put(K key, V value) {
           synchronized (mutex) {return m.put(key, value);}
       }
       public V remove(Object key) {
           synchronized (mutex) {return m.remove(key);}
       }
       // 省略其余方法
}

ConcurrentHashMap

提升安全HashMap的并发性的方法,能够经过减少锁粒度的方式,不对整个Hash表加锁,而是对每一个bucket加锁,甚至用锁池,每一个锁维护几个bucket,让Map的不一样部分能够被多个线程访问,不过这样的方式会让对总体集合操做的方法的实现更加困难。Java7中的ConcurrentHashMap就经过Segment引入了这个分段加锁概念,但Java8因为上述困难更改了机制,引入了红黑树结构,去掉了Segment。数据结构

JDK1.8的改进后,ConcurrentHashMap的写性能有10%左右的下降,但读性能有了很大提高。主要是将过于集中的hash节点的效率从O(N)提升到了O(LOGN)。多线程

ConcurrentHashMap利用了CAS进行实现,从而以乐观锁的方式实现了线程安全的HashMap,concurrentHashMap的源码很复杂,一些方法的实现思路以下:并发

Java8的ConcurrentHashMap的数据结构实现思路大概为,对于Hash表中每个节点,其数据结构能够为单节点,链表数组或红黑树,随着节点中元素增长而改变。(改变方法见treeifyBin)。性能

put()方法

  • hash数组是否为空,为空则先调用initTable()方法进行初始化
  • 若是hash数组已经初始化了,则根据hash值找到对应的数组下标,若是对应节点为空,经过cas方式直接插入
  • 若是数组已经扩容,则进行数据迁移
  • 若是数组该位置已经有值了,则须要对该节点加锁并进行数据插入操做,仅对一个节点加锁,其锁粒度实际上比Java7中Segment实现更小。此时若是该节点是链表结构,则遍历链表,插入数据;若是若是该节点是红黑树结构,则调用红黑树的插值方法插入新值
  • 针对链表结构,若是插入新元素后,hash数组长度超过阈值,则须要调用treeifyBin()方法进行扩容或者是将链表转换为红黑树

initTable()方法

  • 当table不存在,开始自旋。
  • 利用CAS操做将sizeCtl属性设置为-1,表示本线程正对数组初始化,阻止其余线程的初始化。
  • 进行常规的初始化操做,扩容阈值为数组容量的75%。
  • 将sizeCtl设置成扩容阈值,结束初始化。

treeifyBin()方法

该方法用于对数组链表扩容,或将链表结构转化为红黑树,一个节点的元素个数大于链表阈值(默认8)时,若是数组链表长度小于红黑树阈值(默认64),则对数组链表扩容,不然将该节点转换为红黑树。优化

transfer(),helpTransfer(),tryPresize()方法

这些方法负责hash表扩容,因为要经过CAS实现线程安全,代码十分复杂。大概思路为,原数组长度为n,则产生n个迁移任务,让每个线程负责一个小任务,以后监测是否有其余没作完的任务,帮助迁移。ui

get()方法

get方法不涉及CAS操做,实现较为简单,计算hash值,找到对应节点进行判断:

  • 该位置为null返回null。
  • 该位置节点为所求值,返回值。
  • 该位置节点hash值小于0,说明在扩容,或者为红黑树,使用find方法。
  • 以上都不知足,该位置为链表,遍历搜索。

性能

目前多线程环境下ConcurrentMap的性能有很高的优越性,一般状况下,若是你的Map处于多读少写的场景,优先考虑ConcurrentMap,但在多写少读的情境中,因为资源竞争激烈,CAS自旋可能致使ConcurrentMap性能不如synchronizedMap。

参考文献

Collections.synchronizedMap()、ConcurrentHashMap、Hashtable之间的区别
Java8 ConcurrentHashMap详解
浅谈Java8中的ConcurrentHashMap
SynchronizedMap

相关文章
相关标签/搜索