引言
前两篇文章我们介绍了集合中的列表和队列,接下来要介绍的也是一个使用非常广泛的类——Map
。
Map
保存了一对对的键值映射关系,每一个键在Map
中都是唯一的。Map
默认使用Object.equals
来判断是否包含某个键,所以我们要尽量避免使用equals
方法会随对象发生变化而变化的对象作为键。
使用Map
的时候,有两个关键参数我们是需要注意的。
capacity Map的初始容量
loadFactor 负载因子,决定了Map需要扩容时,元素数量和容量的比例
下面是Map
的类图。
从类图中我们能够发现,Map
的分化主要分为两个维度,第一个维度是是否有序,这里的有序是指键是否有序,键有序的对象都会继承SortedMap
接口。第二个维度则是是否良好地支持并发,这个则由是否实现了ConcurrentMap
接口确定。
具体实现
这里的具体实现主要会针对几个比较常用的实现进行描述,分别是HashTable
,HashMap
,ConcurrentHashMap
,TreeMap
,ConcurrentSkipListMap
。
HashTable
HashTable
是一个线程安全的Map
,因为其所有方法都被关键字synchronized
修饰。但是,其迭代器并没有设计成为支持多线程访问的,若使用迭代器访问的同时对HashTable
进行了修改,就会引发ConcurrentModificationException
。
还是了解一下其成员变量
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
private transient Entry<?,?>[] table;//使用一个一维数组进行元素保存
private transient int count;//记录元素数量
private int threshold; //需要resize的阈值,元素数量超过阈值后会引发扩容和重哈希,值为初始化大小*负载因子
private float loadFactor;//负载因子
private transient int modCount = 0;//版本号,版本号的存在表明了HashTable不能很好地支持多并发(不能在迭代器遍历的同时进行修改)
接下来了解一下一些具体的方法。
put
方法
public synchronized V put(K key, V value) {
//HashTable不保存null元素
if (value == null) {
throw new NullPointerException();
}
//首先需要检查key是否已经存在
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length; //这里要和0x7FFFFFFF进行与操作,是为了保证int的首位(符号为)为0,也就是index一定是一个正整数
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
再来看一下addEntry
方法
private void addEntry(int hash, K key, V value, int index) {
Entry<?,?> tab[] = table;
if (count >= threshold) {
// 如果当前元素数量已经大于阈值,那么首先要进行重哈希。重哈希会做两件事情,第一个是将数组容量扩大一倍,第二个是将原来的元素按哈希结果重新分布。
rehash();
tab = table;
hash = key.hashCode();
//通过取余的方式确认bucket位置
index = (hash & 0x7FFFFFFF) % tab.length;
}
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
tab[index] = new Entry<>(hash, key, value, e);//这里采用链式哈希法对新建元素进行保存
count++;
modCount++;
}
再了解一下get
方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//需要两个条件,哈希值和equals方法都相等
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
综上可以知道,HashTable
通过synchronized
关键字来保证其线程安全。但是又因为其迭代器未对多线程访问进行优化,因此使用迭代器的同时如果进行了修改操作,还是会导致ConcurrentModfiyException
的抛出。除此之外,HashTable
通过一维数组对键值对进行保存,使用链式哈希法解决冲突。
HashMap
和HashTable
不一样,HashMap
中的方法并为使用sychronized
关键字进行修饰,另外HashMap
也支持保存Null的key和value。
依然看一下其成员变量
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
transient Node<K,V>[] table;//仍然使用一维数组对元素进行保存
transient Set<Map.Entry<K,V>> entrySet;//保存了键值对的集合
transient int size;//集合大小
transient int modCount;//版本号
int threshold;//扩容阈值
final float loadFactor;//负载因子
}
然后看一下其put
方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//若Map为空,首先需要进行初始化
n = (tab = resize()).length;
//这里通过与的方式来确定Bucket的位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果已经存在节点,那么进行冲突解决
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果这里已经是树节点了,那么将新节点添加到红黑树上
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//首先通过链式哈希法解决冲突
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链式哈希法中元素数量已经超过阈值,那么将其转化成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
//版本号+1
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
从上面的方法我们能够看到,HashMap
解决冲突的方式比HashTable
要复杂一下。首先通过链式哈希解决,但若这个链达到一定长度之后,就会将链转化成树,以确保其元素获取时间。
所以接下来的get
方法,也需要对这个场景进行特殊处理。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//首先检查对应bucket有无元素
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//遍历哈希树
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
//遍历链式哈希,获取元素值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
这就是HashMap,通过更复杂一些的数据结构,保证了性能。
TreeMap
TreeMap
,顾名思义,这是一个基于红黑树的Map。红黑树本身就是一种二叉查找树,因此通过中序遍历就能够得到关键字有序的序列,这也是TreeMap
实现了SortedMap
的原因。
对于其添加、删除、查询节点的方式,实际上就是红黑树的删除、添加、查找的实现,在这里就不展开叙述了,有兴趣的可以通过这篇博客来了解红黑树。
这里主要讲的是其遍历元素的EntrySet
相关的方法及类。
public Set<Map.Entry<K,V>> entrySet() {
EntrySet es = entrySet;
return (es != null) ? es : (entrySet = new EntrySet());
}
class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
//getFirstEntry就是通过中序遍历获取最小的元素
return new EntryIterator(getFirstEntry());
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
Object value = entry.getValue();
Entry<K,V> p = getEntry(entry.getKey());
return p != null && valEquals(p.getValue(), value);
}
public boolean remove(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
Object value = entry.getValue();
Entry<K,V> p = getEntry(entry.getKey());
if (p != null && valEquals(p.getValue(), value)) {
deleteEntry(p);
return true;
}
return false;
}
public int size() {
return TreeMap.this.size();
}
public void clear() {
TreeMap.this.clear();
}
public Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<K,V>(TreeMap.this, null, null, 0, -1, 0);
}
}
//Entry的迭代器,直接继承PrivateEntryIterator(TreeMap迭代器的默认实现)
final class EntryIterator extends PrivateEntryIterator<Map.Entry<K,V>> {
EntryIterator(Entry<K,V> first) {
super(first);
}
public Map.Entry<K,V> next() {
return nextEntry();
}
}
abstract class PrivateEntryIterator<T> implements Iterator<T> {
Entry<K,V> next;
Entry<K,V> lastReturned;
int expectedModCount;
PrivateEntryIterator(Entry<K,V> first) {
expectedModCount = modCount;
lastReturned = null;
next = first;
}
public final boolean hasNext() {
return next != null;
}
final Entry<K,V> nextEntry() {
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
next = successor(e);
lastReturned = e;
return e;
}
final Entry<K,V> prevEntry() {
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
next = predecessor(e);
lastReturned = e;
return e;
}
public void remove() {
if (lastReturned == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
// 进行节点删除时,若该节点有两个子节点,则会把其中一个子节点的值上移,因此这里将next改为lastReturned
if (lastReturned.left != null && lastReturned.right != null)
next = lastReturned;
//lastReturned即为迭代器当前调用nextEntry返回的值,将这个节点进行删除
deleteEntry(lastReturned);
expectedModCount = modCount;
lastReturned = null;
}
}
从上面能够看到,对于TreeMap
来说,实际上还是通过树的遍历方式来进行entrySet的遍历。而PrivateEntryIterator
的存在,实际上就是为从任意一个节点进行遍历提供了可能(实际上,TreeMap还提供了DescendingKeyIterator
这种倒叙遍历的类)。
ConcurrentHashMap
ConcurrentHashMap
是支持并发访问的Map
,它也是通过数组来存储元素,然后在遇到冲突的时候,首先通过链表其次通过红黑树来解决哈希冲突同时保证访问的速度。它的并发控制也是通过Synchronized
来实现的,但与HashTable
一次性会锁住整个对象不一样,遇到并发写操作的时候,ConcurrentHashMap
选择锁住数组中的某个元素,分段锁的策略保证了其高并发的修改操作。
在扩容的时候,ConcurrentHashMap
首先会新建一个容量为原来两倍的数组,然后通过复制节点以及重哈希的方式将原有节点移动至新的数组中。扩容也是分段进行的,若A线程在扩容的时候了遇到了B线程进行元素添加/删除/修改的操作,那么B线程也会参与到扩容中,加快扩容的速度。待扩容完成后继续进行原操作。
接下来看一下putVal
的具体实现
final V putVal(K key, V value, boolean onlyIfAbsent) {
//不允许添加key为null或者是value为null的对象
if (key == null || value == null) throw new NullPointerException();
//计算哈希值
int hash = spread(key.hashCode());
//这里的binCount用于记录哈希链中的元素个数,若超过一定值,需要将哈希链转化为哈希树
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//这里是判断对应的bucket上是否有元素了,如果没有元素的话,直接通过cas完成添加
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//在扩容的时候,旧的数组上所有的节点都会被替换成ForwardingNode,而这个Node的哈希值就是MOVED(-1),因此遇到了这种节点的时候,就意味着Map正在做扩容操作,这个时候此线程可以参与到扩容中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//首先,将bucket中的第一个元素加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
//fh >= 0,链式哈希法处理冲突
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//如果是红黑树的话,那么按照红黑树的逻辑进行元素修改
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//判断是否需要转化成为哈希树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
其次再看一下replaceNode
方法,实际上这个方法与putVal
方法大同小异
final V replaceNode(Object key, V value, Object cv) {
//首先,计算哈希值
int hash = spread(key.hashCode());
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//其次,判断Map是否为空,若Map已经为空,则没有必要进行处理了
if (tab == null || (n = tab.length) == 0 ||
(f = tabAt(tab, i = (n - 1) & hash)) == null)
break;
//再次,判断是否处于扩容状态,若处于扩容状态则主动参与到扩容中
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
boolean validated = false;
//同样,对第一个元素进行加锁
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
validated = true;
for (Node<K,V> e = f, pred = null;;) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
V ev = e.val;
if (cv == null || cv == ev ||
(ev != null && cv.equals(ev))) {
oldVal = ev;
if (value != null)
e.val = value;
else if (pred != null)
pred.next = e.next;
else
setTabAt(tab, i, e.next);
}
break;
}
pred = e;
if ((e = e.next) == null)
break;
}
}
//如果是红黑树,则进行树的元素移除操作
else if (f instanceof TreeBin) {
validated = true;
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> r, p;
if ((r = t.root) != null &&
(p = r.findTreeNode(hash, key, null)) != null) {
V pv = p.val;
if (cv == null || cv == pv ||
(pv != null && cv.equals(pv))) {
oldVal = pv;
if (value != null)
p.val = value;
else if (t.removeTreeNode(p))
setTabAt(tab, i, untreeify(t.first));
}
}
}
}
}
if (validated) {
if (oldVal != null) {
if (value == null)
addCount(-1L, -1);
return oldVal;
}
break;
}
}
}
return null;
}
以上就是ConcurrentHasMap
的两个主要操作。实际上ConcurrentHashMap
在Java7和Java8中有两个完全不同的实现,Java7的实现是为数组划分了Segment
,然后针对Segment
进行加锁。这样做的话实际上就限制了ConcurrentHashMap
的最大并发数了,而且也有可能在某个分区内的元素较为集中,进而导致并发量进一步下降。
关于Java7中的实现,可以参考这里。
ConcurrentSkipListMap
ConcurrentSkipListMap
,基于跳跃列表实现的Map
。跳跃表的原理实际上是建立了一个多层的链表,然后在多层的链表中,又会随机地跳过某些元素(也因此得名跳跃列表)。这样的多层链表控制,有两个好处。第一查询元素的时间不再是O(n)了,可以提高效率。第二个就是扩容时的效率提高,相比于树插入/删除时需要进行扩容或者是树的调整/重哈希,跳跃表示不需要进行这些步骤的。在维基百科上是这样描述跳跃列表的。
跳跃列表不像某些传统平衡树数据结构那样提供绝对的最坏情况性能保证,因为用来建造跳跃列表的扔硬币方法总有可能(尽管概率很小)生成一个糟糕的不平衡结构。但是在实际中它工作的很好,随机化平衡方案比在平衡二叉查找树中用的确定性平衡方案容易实现。跳跃列表在并行计算中也很有用,这里的插入可以在跳跃列表不同的部分并行的进行,而不用全局的数据结构重新平衡。
那么,我们还是了解一下其内部重要的成员变量
public class ConcurrentSkipListMap<K,V> extends AbstractMap<K,V>
implements ConcurrentNavigableMap<K,V>, Cloneable, Serializable {
//头部指针,跳跃列表实际上还是多层的链表结构,这里的head指的是level最大的链表的首元素
private transient volatile HeadIndex<K,V> head;
//比较器,如果跳跃列表中的元素没有实现Comparable接口,也可以通过设置Comparator来确认排序
final Comparator<? super K> comparator;
}
接下来就是添加元素的doPut
方法,这个方法主要分为两个步骤,第一个步骤首先是找到元素在底层node列表中的顺序,并将元素插入。第二个则是建立高层的跳跃表链接关系。
private V doPut(K key, V value, boolean onlyIfAbsent) {
Node<K,V> z;
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
//首先需要找到其前置元素,
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
if (n != null) {
Object v; int c;
Node<K,V> f = n.next;
if (n != b.next)
break;
//如果前置元素处理被删除的过程中,这里需要继续进行删除(状态机中的某个状态)
if ((v = n.value) == null) {
n.helpDelete(b, f);
break;
}
if (b.value == null || v == n)
break;
if ((c = cpr(cmp, key, n.key)) > 0) {
b = n;
n = f;
continue;
}
//原来的key在跳跃列表已经存在,使用cas替换值
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
break;
}
}
//原来的key在跳跃列表中不存在,通过cas进行元素添加
z = new Node<K,V>(key, value, n);
if (!b.casNext(n, z))
break; // restart if lost race to append to b
break outer;
}
}
//开始进行列表跳跃层节点的添加
int rnd = ThreadLocalRandom.nextSecondarySeed();
//随机决定是否需要将跳跃列表层数+1,也决定了当前这个节点需要建立几层链表
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
while (((rnd >>>= 1) & 1) != 0)
++level;
Index<K,V> idx = null;
HeadIndex<K,V> h = head;
if (level <= (max = h.level)) {
//初始化这个节点的上层链表节点
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);
}
else {
//这里是整个列表的层数+1
level = max + 1;
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);
for (;;) {
h = head;
int oldLevel = h.level;
if (level <= oldLevel) // lost race to add level
break;
HeadIndex<K,V> newh = h;
Node<K,V> oldbase = h.node;
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
if (casHead(h, newh)) {
h = newh;
idx = idxs[level = oldLevel];
break;
}
}
}
// 将刚刚插入的跳跃层节点与其他节点链接起来
splice: for (int insertionLevel = level;;) {
int j = h.level;
for (Index<K,V> q = h, r = q.right, t = idx;;) {
if (q == null || t == null)
break splice;
if (r != null) {
Node<K,V> n = r.node;
int c = cpr(cmp, key, n.key);
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
if (j == insertionLevel) {
//插入层进行链接
if (!q.link(r, t))
break; // restart
if (t.node.value == null) {
findNode(key);
break splice;
}
//同时对insertionLevel-1,也就是下面的层级也需要进行链接
if (--insertionLevel == 0)
break splice;
}
if (--j >= insertionLevel && j < level)
t = t.down;
q = q.down;
r = q.right;
}
}
}
return null;
}
然后再具体看一下,其获取前置元素的方法,这个方法在get
和remove
方法中也被调用了,是跳跃列表的关键查找算法。
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
if (key == null)
throw new NullPointerException();
for (;;) {
//首先获取头部节点(最上层链表的第一个节点),然后向右进行查找
for (Index<K,V> q = head, r = q.right, d;;) {
if (r != null) {
Node<K,V> n = r.node;
K k = n.key;
//n.value = null,带白哦节点处于删除过程中,此时需要将节点删除,然后继续进行查找
if (n.value == null) {
if (!q.unlink(r))
break;
r = q.right;
continue;
}
//如果搜索的key比当前链表的下一个key要大,则继续向右查找
if (cpr(cmp, key, k) > 0) {
q = r;
r = r.right;
continue;
}
}
//如果搜索的key比当前链表的下一个key小,则代表此层链表跳的太远了,通过下层链表继续搜索
if ((d = q.down) == null)
return q.node;
q = d;
r = d.right;
}
}
}
了解完上述的方法,就对跳跃列表的具体实现有一个大致的了解了。
小结
这篇文章主要讲了集合框架中的Map
,以及具体的几个实现类。包括最初线程安全的HashTable
(但是其迭代器也不能和多线程并发同时使用),还有最常用的HashMap
,以及在并发下的ConcurrentHashMap
(通过对哈希表中的首元素进行加锁解决多并发读写问题,遇到扩容时,则使用sizeCtl进行全局锁),ConcurrentSkipListMap
(通过跳跃列表实现的Map
,这里所有的并发读写都是通过cas来进行控制的,属于无锁的实现)。