这真是我所知道的Map吗?
开门先来几个问题,这样有目的研究会更清楚
1. 除了HashMap你还用过其他的Map吗?
2. HashMap底层是如何实现的?
3. Map是无序的吗?
4. ConcurrentHashMap 是如何实现线程安全的?
除了HashMap你还用过其他的Map吗?
interesting HashMap在实际中非常常用,甚至我们常常将HashMap和Map等价了。
我所知道的是 LinkedHashMap,HashTable 但是我还真没用过这2个。
那么我整理了一下Map的实现类,大约如下
graph TD
A[ Map ] --> B[AbstractMap]
A --> SortedMap
SortedMap --> NavigableMap
NavigableMap --> TreeMap
A --> HashTable
B --> TreeMap
B --> HashMap
HashMap --> LinkedHashMap
B --> EnumMap
B --> IdentityHashMap
B --> WeakHashMap
WeakHashMap 还有个内部类的实现(ClassValue中的ClassValueMap)
我一直觉得自己整理出来这个,已经可以了!直到第5个问题的发生~~~~
我TM干了什么,我竟然把concurrent包忘记了,忘记了,忘记了。我仿佛听到了一个响亮的耳光!!
o(≧口≦)o
ヾ(。ꏿ﹏ꏿ)ノ゙
好吧,加上concurrentMap及它的实现ConcurrentHashMap,ConcurrentSkipListMap
graph TD
A[ Map ] --> B[AbstractMap]
A --> C[ConcurrentMap]
B --> HashMap
HashMap --> LinkedHashMap
B --> EnumMap
B --> IdentityHashMap
B --> WeakHashMap
B --> TreeMap
C --> ConcurrentHashMap
C --> ConcurrentNavigableMap
ConcurrentNavigableMap --> ConcurrentSkipListMap
B --> ConcurrentHashMap
B --> ConcurrentSkipListMap
A --> HashTable
A --> SortedMap
SortedMap --> NavigableMap
NavigableMap --> TreeMap
天啊,好乱!!!为了没有其他的耳光,我决定打开我珍藏的api,看看Map还有没其他实现。于是发现
Interface Map
所有已知实现类:
AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap
Attributes
Properties –> Provider –> AuthProvider
把Attributes,Properties 丢到爪哇国的我现在只想撞墙。
PrinterStateReasons、RenderingHints、SimpleBindings、TabularDataSupport、UIDefaults是javax中的内容(rt.jar)就先不考虑了。
如上分析:最起码应该说出来的LinkedHashMap、TreeMap、Attributes、Properties 、ConcurrentHashMap、Hashtable
HashMap底层是如何实现的?
分析一下,其实这个问题包含好多个问题,HashMap的坑是一个接一个的。
1. HashMap是如何存储值的。
2. put的时候是如何确定值的位置,而且get的时候要能找得到。
3. 会不会出现2个值同时存在一个位置。
4. HashMap扩容是如何实现的。
其实如果深挖,估计还能挖出来更多,所以我决定推翻我对HashMap的理解,去看源码。
源码中实现过程如下:
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//此处省略
}
上述源码中的意思比较明显,就是定义了一个Node数组。Node中需要关注的是Next,它是定义了一个链表结构。这段代码直接表述出了一个基本的拉链法结构。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里就是HashMap中传说已久的hash算法,看得出来,其中借助了Object的HashCode。
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) {
if (first.hash == hash && // always check first node
((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;
}
get方法没啥,主要是getNode实现的多,getNode逻辑中竟然还包括了TreeNode的一部分实现。按道理父类不应该包含子类逻辑的(TreeNode的确是HashMap的实现,但是继承了LinkedHashMap.Entry,LinkedHashMap.Entry继承了Node。他们都是Map.Entry的实现类),请注意这里有个大坑,稍后详细分析,HashMap的链表有2种实现,一种是Node实现的单向链表,一种是TreeNode实现的树(双向链表)。
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
getNode主要就是从链表中迭代寻找key值一致的Node。hash(key)的作用比较偏向于定位数组的下标。
定位数组下标主要靠2句代码
n = tab.length
(n - 1) & hash
这里结合hash(Object key)方法中的算法可以得到(tab.length-1) & (h = key.hashCode()) ^ (h >>> 16) 这个计算数组下标的核心算法。
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)
n = (tab = resize()).length;
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);//treeMap中的实现
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);//treeMap中的实现
return null;
}
这个put的逻辑好麻烦,忽略掉TreeNode的实现。我们一样可以发现n = tab.length、tab[i = (n - 1) & hash]这样的语句,也就说明取数组下标的算法和get方法是一致的。
这里有几个坑,tab = resize() 、++size > threshold 、treeifyBin(tab, hash)。
这里就必须去看resize、treeifyBin这2个方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
//此处省略
}
}
return newTab;
}
说实话,这个resize方法看了我一个下午,加上n多试验。然后不得不承认我的孤陋寡闻,之前一直以为resize挺简单的。在不得不吐槽这个代码的变量命名简直惨不忍睹(Cap是表示数组的长度,Thr表示的是map的预警值),而且十分苦逼的是这些注释有啥用。
这里得到一些有用的信息,就是new HashMap的时候数组是没有初始化的,只有放入第一个值才会去初始化数组。数组在MAXIMUM_CAPACITY(1<<30)的时候都是直接翻倍的(newCap = oldCap << 1)。大于MAXIMUM_CAPACITY时是警戒值加到Integer.MAX_VALUE,数组不发生改变。
tab = resize() 、++size > threshold告诉我们了初始化数组的时间(第一次put)和数组扩张的时间(已有元素大于警戒值)。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
treeifyBin(tab, hash);的作用非常奇怪,这里代码的作用是当单个链表长度>=TREEIFY_THRESHOLD(8) - 1时将原本的Node替换为TreeNode。Node和TreeNode最大的差距在于Node只有Next是单向链表,而TreeNode存在prev和left,right是树结构,因为TreeNode是继承的Node所以也是存在next的。
此处是非常混乱了,既是一个双向链表,也是一棵树。
这里整理下要点:
1. HashMap默认初始化数组的大小为16(DEFAULT_INITIAL_CAPACITY = 1<<4)
2. HashMap会以2^n 的方式去扩容而且它的容量只能是2^n。也就是说你指定一个初始值,HashMap也会使用大于等于这个值的2的n次幂去实例化数组(最大值是是2^30)。
3. HashMap计算元素位置会使用 (tab.length-1) & (h = key.hashCode()) ^ (h >>> 16) 的方式去计算,而不是网上流传的取模法。这里有个有意思的地方(null是没有hashCode的,HashMap将null的hash值直接固定的0,也就null值一定会在数组的下标0中)
4. HashMap中的元素的下标是可以重复的,重复的下标会使用链表的方式存储(node.next)。这种方式是典型的拉链法(数组中存放链表)。这个链表初始是单向链表,长度大于等于7是会转换为树(双向链表)。
5. 数组的扩容时机(并不是HashMap的大小,链表理论上是可以无限存储的,而数组的容量是有限的)是HashMap的已有元素>= 警戒值( 最大容量*警戒阀值或者是原警戒值双倍) 。警戒阀值默认为0.75f,可以在构造函数中自定义。这种扩容的方式可以有效的减少链表的长度,也就是使查询效率变高。迭代速度变慢。我没有说插入和删除速度变慢,因为我也不知道(数组和链表的优缺点远远没有网上说的那么简单,这和内存管理有极大的关系,整理完内存管理后继续深挖!)
6. HashMap的扩容方式,新建一个数组,然后重新移动元素。也就是说,数组和链表会重新建立。这里的大问题是慢,慢就意味着在扩容的时间中去插入元素是非常危险的。
7. HashMap 数组的极限是1073741824(1<<30) ,源码中没有发现限制链表长度的代码。也就是理论上HashMap的存储并没有极限。所以,之前理解的1073741824最大值是有问题的。(我电脑内存12G给了JVM4个G,可以把map弄成1<<28。我要换电脑才能试出来Map的极限吗?)
Map是无序的吗?
这里既然问了Map,其实在第一个问题中已经有明显的回答了,LinkedHashMap和TreeMap明显不是没有结构的命名。这里就引申出一个问题,他们是如何保证自己的结构的!
此处将总结提前
1. LinkedHashMap在基本的数据存储中依旧是使用的拉链法。只是为了保证有序性实现了一个双向链表的结构。也就是说,在平时使用的时候依旧是用的HashMap的方法(这样保证了速度不会被影响太多)。只是在迭代器中使用了双向链表结构,使它可以被迭代了(其实不止迭代器)。
2. HashMap是一个数组+单向链表或树的结构,单向链表和树是在同一个数组下标上只有一种结构,所以HashMap也不是绝对无序(只要想实现还是可以进行有序迭代的)。LinkedHashMap就非常奇怪,它的链表实现是完全独立于数组的,可以理解为在HashMap的基础上增加了链表实现用来保存顺序,其顺序与保存顺序完全一致。
3. TreeMap的实现是通过红黑树实现的。在TreeMap的保存时会默认进行排序(key必须是java.lang.Comparable的实现),也就是说TreeMap的顺序和保存顺序无关(put,1、2、3、4、5、6和put,6、5、4、3、2、1得到的结果是一样的)。
LinkedHashMap
上面的源码已经把脸快打成猪头了,很多理解发现都和源码对不上,所以干脆重新理理算了。
经过,查看LinkedHashMap的源码,发现一个奇怪的事情:
LinkedHashMap本身没有实现put方法,也就是说,它是用的父类HashMap的put方法。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
get方法变化并不大,一样是使用了getNode(),而且这个getNode也没有被重写,依旧是HashMap中的方法。所以它的存储结构是和HashMap是一致的,那么LinkedHashMap是如何保证它的顺序的呢?
所以我们的目标应该转向get,put方法出现的3个方法中。当然还有迭代器的实现也是需要注意的。上面引用中出现了afterNodeAccess,这个方法莫名眼熟。这个方法在HashMap中的put方法出现过,同时出现的还有afterNodeInsertion,在HashMap中的removeNode中还出现了afterNodeRemoval方法。
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
注释就说明了是给LinkedHashMap回调的~
其中重头戏是afterNodeAccess,按照重头戏压轴的规则,所以这个最后说~
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
先来看看打酱油的插入afterNodeInsertion,看名字挺重要的,实际上就是个打酱油的。因为即使在if中前2个判断都是true,最后一个removeEldestEntry已经恒定为false。为这个酱油默哀三分钟,有人知道它为啥酱油请告诉我!!!
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
afterNodeRemoval没有打酱油,其实主要就是将入参Node从链表中摘掉了。但是这里有个非常重要的信息,那就是它只修改了before和after,所以可以知道这里是个双向链表。
这里还有个引申那就是它敢非常肯定的转型称为LinkedHashMap.Entry而不怕报错,那么我推测LinkedHashMap中的元素只可能为2种(Entry,TreeMap)。看到此处,前面HashMap中的一个混乱点就清晰了,TreeNode除了需要保证树结构来保证查询效率,还需要保存链表结构使LinkedHashMap有序。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
来到了我期待已久的重头戏,然后,我发现他是个假的重头戏,这个根本就是个骗子方法。这个注释move node to last简直在嘲讽我的智商。为啥这个方法叫Access?谁能告诉我这个方法为何也是个酱油,将元素放到链表最后一个有啥意义吗?
那么跳过了3个假重点后,我收获了一些草泥马草原的空气,还有一个问题。它为何干如此胆大的敢直接转型成Entry。没有重写put方法让我很放心的将问题定位在newNode和newTreeNode头上了。于是我找到了这2个方法。
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
到这里LinkedHashMap的存储问题就已经搞定了,就是用的双向链表来保证数据的顺序的。为了防止我的智商再次被戏耍,所以我还是看看迭代器去。
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {return next != null;}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
public final void remove() {
//此处省略
}
}
看到了迭代器中的next = e.after;,在看迭代器的三个实现并没有重写这个方法我觉得世界清静了。
TreeMap
恩,上面HashMap对我认知的颠覆最大,我一直以为HashMap是数组+双向链表,谁知道竟然是单向链表和树。
不扯了,继续看TreeMap。根据上面的经验,先看三个地方Map.Entry的实现,put,get方法。
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;
//此处省略
}
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
这里可以获取到一个信息,就是TreeMap的key不能是随意的,必须是java.lang.Comparable的子类。没有compareTo方法貌似TreeMap就转不动了。
根据Entry我们基本可以判断这是一棵红黑树了,在put方法最后面调用了一次fixAfterInsertion,这个方法实现了平衡红黑树的功能。不出所料,我在remove方法看到调用了fixAfterDeletion。
到这里就没有啥研究欲望了,毕竟红黑树结构,和红黑树的平衡都有了。最后看下迭代器避免翻车。
abstract class PrivateEntryIterator<T> implements Iterator<T> {
Entry<K,V> next;
//此处省略
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;
}
//此处省略
}
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
从上面看到下个节点的读取顺序是左中右,是中序。也就是是说第一个节点是在树的最左侧的子节点。于是我找到了这么一段。
final Entry<K,V> getFirstEntry() {
Entry<K,V> p = root;
if (p != null)
while (p.left != null)
p = p.left;
return p;
}
ConcurrentMap是如何实现线程安全的
ConcurrentMap有2个实现,一个是ConcurrentHashMap,这个是要对比HashMap去看看的。另一个是ConcurrentSkipListMap,这个就没啥参照物了,慢慢看咯。
ConcurrentHashMap的线程安全
读HashMap源码给了很多经验,比如我们需要关注的点已经非常清晰了数组的定义、Map.Entry的实现、put、get、remove。
先看看数组
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final float LOAD_FACTOR = 0.75f;
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
一开始的初始定义和HashMap并没有什么区别,都是默认初始16,最大1<<30。但是有个Integer.MAX_VALUE - 8的疑惑在,回头再看看这个定义应用在那里。
好了,重点来了,table的定义中比HashMap多了个volatile关键字,而且还多了个nextTable。volatile的作用在这个地方应该是保证table在内存中的可见性。nextTable的作用就不清楚了,继续往下看了。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return val; }
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
}
Node定义中有2个点和HashMap区别较大,一个是val和next都是用了volatile关键字,另一个是setValue直接出异常:不支持此操作。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get方法貌似没啥特点,虽然说和HashMap完全不一样,但是也没啥可以关注并得到信息的地方(可能是我水平不够)。
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
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();
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
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
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;
}
public V remove(Object key) {
return replaceNode(key, null, null);
}
/**
* Implementation for the four public remove/replace methods:
* Replaces node value with v, conditional upon match of cv if
* non-null. If resulting value is null, delete.
*/
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;
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) {
//此处省略
}
else if (f instanceof TreeBin) {
//此处省略
}
}
}
//此处省略
}
}
return null;
}
(@ο@) 哇~,这2个方法看的我头好大。整理整理
1. 和HashMap一样,链表中存在2中结构,一种是单向链表一种是树。不同的是这里用的不是TreeNode而是TreeBin
2. synchronized (f) 加锁的对象是数组的节点,而不是整个数组。
3. e.val = value; 说明了不是不支持setValue操作,而是直接修改了值。为啥?
大约就这些,多线程中需要注意的事情原子性,可见性,有序性。对Node节点加锁的方法保证对单个节点的操作是原子的,volatile保证了可见性(有一定程度上的有序)。
多线程这块一直是弱项,这里代码读的好累。特别是锁那块,一直以为remove只锁一个节点有问题,就是如果e.next被删除了会出现一些奇奇怪怪的问题。后来反应过来有volatile,修改了会及时变化的。
ConcurrentSkipListMap 如何实现线程安全
说实话,这个代码看疯我了,因为整个代码中根本没有出现过synchronized关键字。为何如此神奇着要归功于一个神奇的数据结构Skip List(跳表)。所以呢!它关键在于这代码的注释与数据结构。
* Head nodes Index nodes
* +-+ right +-+ +-+
* |2|---------------->| |--------------------->| |->null
* +-+ +-+ +-+
* | down | |
* v v v
* +-+ +-+ +-+ +-+ +-+ +-+
* |1|----------->| |->| |------>| |----------->| |------>| |->null
* +-+ +-+ +-+ +-+ +-+ +-+
* v | | | | |
* Nodes next v v v v v
* +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
* | |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
* +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+ +-+
static final class Node<K,V> {
final K key;
volatile Object value;
volatile Node<K,V> next;
//此处省略
}
static class Index<K,V> {
final Node<K,V> node;
final Index<K,V> down;
volatile Index<K,V> right;
//此处省略
}
好了,上面代码和注释只需要表示ConcurrentSkipListMap使用跳表实现就可以了,跳表+volatile为何能保证线程安全,后续在整理一下。(我那可怜的数据结构已经完全还给老师了,我需要十全大补汤~~~~)