相互关系
- 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<K,V>[] table; // 存储元素的数组,第一次使用时初始化,需要时扩容,大小一定是2的指数
transient Set<Map.Entry<K,V>> 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<K,V> next;
// more code
}
- 构造函数
public HashMap()
public HashMap(int initialCapacity)
public HashMap(int initialCapacity, float loadFactor)
public HashMap(Map<? extends K, ? extends V> 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 Comparator<? super K> comparator;
private transient Entry<K,V> 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<K,V> left;
Entry<K,V> right;
Entry<K,V> 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<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder; // true 按照访问顺序排序(每次get后调整指针), false 按照插入顺序排序。
- Entry结构在HashMap.Node的基础上添加了前后指针。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
ConcurrentHashMap
- 线程同步的HashMap,比Hashtable效率高。原因是Hashtable中采用的锁机制是一次锁住整个hash表,同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。
- 成员变量(volatile修饰的变量具有可见性与有序性,每次都会去读内存里的值)
transient volatile Node<K,V>[] table; // 存储元素的数组
private transient volatile Node<K,V>[] 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<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;
- 插入(put)算法:
- 计算key的hash值
- 循环执行3-6步:
- 如果table为空,初始化table
- 如果所插入的bin为空,用cas算法新建Node插入(确保插入时bin是空的),成功跳出循环
- 如果正在扩容,协助扩容
- 锁住当前Node,插入数据,成功则跳出循环
- 扩容(需要时)
- 利用sun.misc.Unsafe实现3种原子操作
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> 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实例。