HashMap是我们很常用的一个集合类。但在并发编程中使用HashMap可能会导致程序死循环(1.8不会),而使用HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会。
温馨提示:源代码来自JDK1.8
一、ConcurrentHashMap的常量
//最大容量 private static final int MAXIMUM_CAPACITY = 1 << 30; //默认容量 private static final int DEFAULT_CAPACITY = 16; //最大可能需要的数组大小,用于toCharArray等相关方法 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //默认并发数 private static final int DEFAULT_CONCURRENCY_LEVEL = 16; //负载因子 private static final float LOAD_FACTOR = 0.75f; //哈希冲突的链表转换为红黑树的阈值 static final int TREEIFY_THRESHOLD = 8; //哈希冲突的红黑树转换为链表的阈值 static final int UNTREEIFY_THRESHOLD = 6; // static final int MIN_TREEIFY_CAPACITY = 64; // private static final int MIN_TRANSFER_STRIDE = 16; // private static int RESIZE_STAMP_BITS = 16; //帮助Map扩容的最大线程数 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; //记录size大小的偏移量 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //节点哈希字段的编码 static final int MOVED = -1; // hash for forwarding nodes static final int TREEBIN = -2; // hash for roots of trees static final int RESERVED = -3; // hash for transient reservations static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash // static final int NCPU = Runtime.getRuntime().availableProcessors();
二、ConcurrentHashMap中的属性变量
//存放键值对的哈希表 transient volatile Node<K,V>[] table; //扩容时生成的哈希表,容量时原容量的2倍 private transient volatile Node<K,V>[] nextTable; // 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;
其中sizeCtl在不同的地方有不同的用途,其值也不同,所代表的的含义也不同
1、负数代表正在进行初始化或扩容操作
2、-1代表初始化
3、-N表示有N-1个线程正在进行扩容操作
4、整数或0代表hash表还没有初始化,这个数值表示初始化或下一次进行扩容的阈值
三、哈希节点
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; } /*代码省略大法*/ }
注意value和next使用volatie修饰。
四、主要方法
get方法:
public V get(Object key) { //哈希表 Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek; //计算key的hash值 int h = spread(key.hashCode()); //如果哈希表不为空,哈希表长度大于0,并且根据key计算的hash值能找到对应的节点 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { //如果这个节点的hash值等于key的hash值 if ((eh = e.hash) == h) { //如果这个节点的key和传入的key相等,则返回该值 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; //遍历链表,如果找到hash值和key值和传入的key值和计算出的哈希值相同则返回这个节点的值 while ((e = e.next) != null) { if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) return e.val; } } return null; }
大致步骤为:
1、计算传入key的hash值
2、根据hash值使用hash算法映射相应的哈希桶
3、若桶无节点,返回null
4、如果桶有节点,若节点的key值和hash值与传入的key值和计算出的hash值相同,则返回节点的value
5、若当前节点时树节点,则去红黑树中查找
6、若有链表,则遍历链表查找
put方法:
public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { //如果key或value为空,抛出异常 if (key == null || value == null) throw new NullPointerException(); //计算hash值 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(); //通过hash算法计算出要插入的桶,若桶为空,则使用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 } //如果这个节点的hash值是-1,则说明Map正在进行扩容操作,当前线程协助扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); //出现hash冲突或者要更新值 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; //如果传入的key和计算出的hash值与该节点相同,则覆盖旧值返回 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; //说明出现了hash冲突 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; }
put方法的大致步骤为:
1、判断key值和value值,任何一个为空都抛出异常,也就是ConcurrentHashMap不能存入null值
2、计算key的hash值
3、判断哈希表是否为空,为空则初始化,不为空进行下一步
4、通过hash算法计算出要插入的桶,若桶为空,则使用CAS操作将这个键值对放入桶中,桶不为空进行下一步
5、如果这个节点的hash值是-1,则说明Map正在进行扩容操作,当前线程协助扩容
6、进行到这里需要加锁,接下来继续判断,遍历链表
7、如果传入的key和计算出的hash值与该节点相同,则覆盖旧值返回,如果不相同则出现了hash冲突,将新节点插入链表尾部
8、如果这个节点是树节点,则按照红黑树的方式去操作
9、在最后需要判断链表长度是否超过转换为红黑树的阈值,若超过则转换,不然返回旧值(正常插入旧值为null)
initTable方法
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { //说明有其他线程正在初始化哈希表 if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin //当前线程获取了初始化哈希表的权利,使用CAS操作将SIZECTL设置为-1,表示当前线程在初始化哈希表 else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { //初始化容量 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") //初始化哈希表 Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; //下次扩容的大小 相当于0.75*n sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; }
这个方法不难,我觉得看我的注释应该就能理解了。
remove方法
public V remove(Object key) { return replaceNode(key, null, null); } final V replaceNode(Object key, V value, Object cv) { //计算hash值 int hash = spread(key.hashCode()); //遍历哈希表 for (Node<K,V>[] tab = table;;) { Node<K,V> f; int n, i, fh; //如果哈希表为空,或者根据key计算出对应的哈希桶内没有元素就返回 if (tab == null || (n = tab.length) == 0 || (f = tabAt(tab, i = (n - 1) & hash)) == null) break; //Map正在扩容,帮助扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); else { V oldVal = null; boolean validated = false; //加锁 synchronized (f) { if (tabAt(tab, i) == f) { //如果是Node节点 if (fh >= 0) { validated = true; for (Node<K,V> e = f, pred = null;;) { K ek; //如果传入的key和计算出的hash值与该节点相同 if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { //得到旧值 V ev = e.val; //如果cv为空,说明是移除操作,cv等于ev,说明是替换操作 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; } } //是TreeBin节点,按照TreeBin的操作去完成 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; }
remove方法使用replaceNode方法实现,传入两个null值
remove方法的步骤大致是:
1、计算key的hash值
2、死循环遍历哈希表
3、如果哈希表为空,或者根据key计算出对应的哈希桶内没有元素就返回
4、如果Map正在扩容,则当前线程协助扩容
5、如果3,4都不满足,这个时候需要加锁后操作
6、如果是Node节点,遍历链表,如果节点的key值和hash值与传入的key值和计算出的hash值相同,进行下一步操作
7、因为这是remove方法,所以将这个节点的value置null,然后删除这个节点(单节点置为null,多节点则使用链表删除)
8、若是树节点,则按照红黑树的方式去操作
9、最后返回旧值(没有找到返回null)
扩容方法我有点看不懂,等我看懂之后再来补上