HashMap、TreeMap、Hashtable、LinkedHashMap、ConcurrentHashMap原理和差异

相互关系

  • Map接口有三个比较重要的实现类,分别是HashMap、TreeMap和HashTable。在实际使用中,我们还经常使用LinkedHashMap、ConcurrentHashMap。
Map 有序性 线程安全性 备注
HashMap 无序 不安全
TreeMap 有序 不安全 根据键排序,可自定义Comparator
Hashtable 无序 安全(锁全表) 不允许null
LinkedHashMap 有序 不安全 根据插入/访问顺序排序,有序的HashMap
ConcurrentHashMap 无序 安全(锁一个桶) 线程安全的HashMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, Serializable
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, Serializable
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V> implements ConcurrentMap<K,V>, Serializable
  • 类的继承关系
AbstractMap
    -> HashMap
        -> LinkedHashMap
    -> TreeMap
    -> ConcurrentHashMap
Dictionary
    -> Hashtable
  • 接口的继承关系
Map // size, isEmpty,containsKey, containsValue, get, put, remove, clear, keySet, entrySet等方法
    -> SortedMap // comparator, subMap, headMap, tailMap, firstKey, lastKey
        -> NavigableMap // 添加了搜索选项到接口
    -> ConcurrentMap

实现原理和线程安全性

HashMap(jdk1.8)

  • HashMap 使用一个Node数组来存储数据,这个Node可能是链表结构,也可能是红黑树结构。
  • 如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。 如果同一个格子里的key不超过8个,使用链表结构存储;如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。

  • 成员变量

transient Node[] table; // 存储元素的数组,第一次使用时初始化,需要时扩容,大小一定是2的指数
transient Set> entrySet; // 存放具体元素的集
transient int size; // 存放元素的大小,HashMap的大小,HashMap保存的键值对的数量,不等于table长度
transient int modCount; // 每次扩容和更改map结构的计数器
int threshold;  // 临界值,当size达到threshold时,将HashMap的容量加倍。threshold的值="容量*加载因子"
final float loadFactor; // 加载因子, 默认0.75
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node next;
        // more code
}
  • 构造函数
public HashMap()
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)
public HashMap(Map m) // 此时table大小初始化为m.size()/loadFactor+1
  • 扩容
    • 每次插入结束时,会判断size与threshold大小,如果size>threshold,就会调用resize()函数进行扩容。
    • 扩容时会生成一个newTab,将oldTab中的元素重新hash插入到newTab,比较耗时。
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

TreeMap

  • TreeMap是利用红黑树来实现的,实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行。

  • 成员变量

private final Comparatorsuper K> comparator;
private transient Entry root;
private transient int size = 0;
private transient int modCount = 0;
 static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry left;
        Entry right;
        Entry parent;
        boolean color = BLACK;
        // more code
}

Hashtable

  • Hashtable的实现原理与HashMap基本一致。区别在于Hashtable的方法是同步的,Hashtable是线程安全的;HashMap的方法不是同步的,HashMap不是线程安全的。
  • 除构造函数外,Hashtable的所有 public 方法声明中都有 synchronized关键字,效率较低,考虑线程安全性的话,建议使用其他方案,比如ConcurrentHashMap。

  • 成员变量,与HashMap基本一致

private transient Entry[] table;
private transient int count;
private int threshold;
private float loadFactor;
private transient int modCount = 0;

LinkedHashMap

  • 用一个双向链表存储。
  • 成员变量
transient LinkedHashMap.Entry head;
transient LinkedHashMap.Entry tail;
final boolean accessOrder; // true 按照访问顺序排序(每次get后调整指针), false 按照插入顺序排序。
  • Entry结构在HashMap.Node的基础上添加了前后指针。
static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry before, after;
        Entry(int hash, K key, V value, Node next) {
            super(hash, key, value, next);
        }
}

ConcurrentHashMap

  • 线程同步的HashMap,比Hashtable效率高。原因是Hashtable中采用的锁机制是一次锁住整个hash表,同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
  • 成员变量(volatile修饰的变量具有可见性与有序性,每次都会去读内存里的值)
transient volatile Node[] table; // 存储元素的数组
private transient volatile Node[] nextTable; // 下一个存储元素的数组,只在resize时非空
private transient volatile long baseCount;
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;
private transient KeySetView keySet;
private transient ValuesView values;
private transient EntrySetView entrySet;
  • 插入(put)算法:
    1. 计算key的hash值
    2. 循环执行3-6步:
    3. 如果table为空,初始化table
    4. 如果所插入的bin为空,用cas算法新建Node插入(确保插入时bin是空的),成功跳出循环
    5. 如果正在扩容,协助扩容
    6. 锁住当前Node,插入数据,成功则跳出循环
    7. 扩容(需要时)
  • 利用sun.misc.Unsafe实现3种原子操作
    static final  Node tabAt(Node[] tab, int i) {
        return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }
    static final  boolean casTabAt(Node[] tab, int i, Node c, Node v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }
    static final  void setTabAt(Node[] tab, int i, Node v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

其他线程安全的HashMap替代方案Collections.synchronizedMap

  • SynchronizedMap类是定义在Collections中的一个静态内部类。它实现了Map接口,并对其中的每一个方法实现,通过synchronized关键字进行了同步控制。它的锁力度也是Map,效率比ConcurrentHashMap差,但可以接收任意Map实例,实现Map的同步。

  • 使用示例

Map<String, Object> map1 = Collections.synchronizedMap(new HashMap<String, Object>());  
Map<String, Object> map2 = Collections.synchronizedMap(new TreeMap<String, Object>());  

选择

  • HashMap:适用于在Map中插入、删除和定位元素,线程不安全。
  • Treemap:适用于按自然顺序或自定义顺序遍历键(key),线程不安全,通常比HashMap慢。
  • Hashtable:线程安全,性能较差,不建议使用。
  • LinkedHashMap:能按插入/访问顺序遍历的HashMap,线程不安全。
  • ConcurrentHashMap:线程安全,锁力度细,性能较好。
  • Collections.synchronizedMap: 线程安全,可以接收任意Map实例。

你可能感兴趣的:(Java)