hashmap自己解读(外加红黑树+ConcurrentHashMap)

开头

借用网友的图片,这是hashmap的整体结构,数组+链表(jdk>1.8时加入红黑树)
请添加图片描述
本文使用的jdk版本为1.8.0_301,内容较jdk1.7版本改变很大
看文章的时候不建议完全看我的注释,建议先自己分析,遇到卡壳的地方来看一下(我也是这么学的)。
看的话,建议从头到尾看,因为开头写的比较详细
关于HashMap的基础部分,看看这个挺好的
https://zhuanlan.zhihu.com/p/127147909

HashMap类

hash方法

	//根据key,算出hash值,后续将hash%n得到key在数组中的位置
    static final int hash(Object key) {
        int h;
        //key.hashCode(),每个基本类型实现了自己的hashCode方法,比如String.hashCode(),可以自己去看一看
        //java中>>>代表无符号右移,忽略符号位,空位都以0补齐
        //h^(h>>>16)是h的低16位和高16位相与。如果容量不大很少用到hash的高位,这里均衡用了一下
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

总结:
1.设置的key需要实现hashCode方法
2.hashmap中最终的hash是高16位和低16位相与的结果

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)
            n = (tab = resize()).length;
        //(n-1)&hash = hash%n 其中n是数组的长度,也就是说通过hash定位到数组中的位置
        //对计算机来说,&操作相比%时间更短
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//数组中没有值就插入一个新的Node
            tab[i] = newNode(hash, key, value, null);
        //发生hash碰撞,解决
        else {
            Node<K,V> e; K k;
            //p为原数组中的结点,与新加进来的结点进行比较:
            //hash相同,同时key也完全相同,则把当前结点负值给临时变量e,后续修改value
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果原结点p,已经是红黑树的结点,那么就将当前结点加入到p所在的红黑树中,e作为返回值
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //否则既不是原结点修改value也不是红黑树,则为添加到链表中,同时进行计数
            //如果加入后节点值>=8则转换为红黑树
            //如果在链表中找到了key相同的结点,进行value替换
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                    	//添加到空结点的后面
                        p.next = newNode(hash, key, value, null);
                        //计数超过8-1,则链表转化成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //遍历链表途中是否找到相同的key,找到就返回
                    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;
            }
        }
        //命中为空,则为修改hashmap的结构,修改计数加一
        ++modCount;
        //修改结构后判断大小,是否进行resize
        if (++size > threshold)
            resize();
        //Callbacks to allow LinkedHashMap post-actions
        afterNodeInsertion(evict);
        return null;
    }

总结
put分为两种:
1.命中hashmap中的某个key,则修改value
2.未命中,添加结点。添加结点时如果总结点数大于8,并且数组长度大于64会将链表转化成红黑树
如果节点数大于8,但是数组长度小于64,会先考虑扩容。因为hash在小于64的时候效率最高

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) {
            //数组中存在hash相同
            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;
    }

总结
1.数组中结点是否命中
2.判断是否是红黑树,并查找结点
3.链表中查找结点

resize方法

    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //threshold表示当HashMap的size大于threshold时会执行resize操作。
		//threshold=capacity*loadFactor
		//在HashMap初始化的时候如果填写初始化容量initialCapacity,则会初始化threshold,值为this.threshold = tableSizeFor(initialCapacity);否则threshold的值为0
		//后文称之为扩容阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
        //旧容量大于0
        if (oldCap > 0) {
        	//旧数组容量已经大于最大容量了,阈值设置到更大, 
        	//HashMap.MAXIMUM_CAPACITY = 1 << 30 值为0x40000000
        	//Integer.MAX_VALUE = 0x7fffffff
            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
        }
        //表示旧容量是0 但是旧阙值却大于零,就扩容新的容量为旧的阈值
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        //旧容量为0 旧阈值为0 则初始化
        else {               // zero initial threshold signifies using defaults
        	//初始容量  1<<4
            newCap = DEFAULT_INITIAL_CAPACITY;
            //初始阈值  1<<4 * 0.75
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        //新阈值为0时,设置新阈值为新容量 * 新加载因子
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            //考虑到新容量或者新阈值是否比最大容量大,如果是的话,设置最大容量为0x7fffffff
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        //扩容结束
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        //重新建立hash数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //e没有下一个结点,就老哥一个,直接重新散列
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果在旧哈希表中,这个位置是树形的结果,就要把新hash表中也变成树形结构
                    //通过split函数进行拆分
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //链表的情况赋值给new tab
                    else { // preserve order
                    	//下面算法很微妙
                    	//定义两个链表lo和hi,分别设置头指针和尾指针
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //将原链表拆分成两个链表
                            //根据(e.hash & oldCap) == 0来区分是加入lo链表还是hi链表
                            //扩容2倍后Node要么添加在j上,要么添加在j+oldCap上具体原理,参考
                            //https://segmentfault.com/a/1190000015812438
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        //如果lo链表非空, 我们就把整个lo链表放到新table的j位置上
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //如果hi链表非空, 我们就把整个hi链表放到新table的j+oldCap位置上
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结
resize发生在table初始化, 或者table中的节点数超过threshold值的时候, threshold的值一般为负载因子乘以容量大小.
每次扩容都会新建一个table, 新建的table的大小为原大小的2倍.
扩容时,会将原table中的节点re-hash到新的table中, 但节点在新旧table中的位置存在一定联系: 要么下标相同, 要么相差一个oldCap(原table的大小).
这也是cap为2的倍数的好处之一

remove方法

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }
    
	final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {
            Node<K,V> node = null, e; K k; V v;
            //这块跟get方法一样,先找结点
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;
            else if ((e = p.next) != null) {
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            //找到之后要删除
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p)
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

总结:先找结点,然后按照类型执行对应的方法,进行删除

TreeNode类

基本结构

	//继承LinkedHashMap.Entry,LinkedHashMap底层结构是 HashMap + 双向链表
    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);
        }
   }

treeifyBin方法(用链表构建红黑树)

	final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //数组的长度小于64先扩容,而不是只要长度大于8就转化为树
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        //否则再转化
        //找到数组中的结点
        else if ((e = tab[index = (n - 1) & hash]) != null) {
        	//hd是头结点,tl是临时结点
            TreeNode<K,V> hd = null, tl = null;
            do {
            	//将Node结点替换成TreeNode结点
                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);
        }
    }

//树的构建
        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) {
                    x.parent = null;
                    x.red = false;
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)
                            dir = -1;
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null &&
                                  (kc = comparableClassFor(k)) == null) ||
                                 (dir = compareComparables(kc, k, pk)) == 0)
                            dir = tieBreakOrder(k, pk);

                        TreeNode<K,V> xp = p;
                        if ((p = (dir <= 0) ? p.left : p.right) == null) {
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x);
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

总结:转化会先判断tab长度,小于64先进行扩容

split(拆分红黑树)

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
            TreeNode<K,V> b = this;
            // Relink into lo and hi lists, preserving order
            TreeNode<K,V> loHead = null, loTail = null;
            TreeNode<K,V> hiHead = null, hiTail = null;
            int lc = 0, hc = 0;
            for (TreeNode<K,V> e = b, next; e != null; e = next) {
                next = (TreeNode<K,V>)e.next;
                e.next = null;
                if ((e.hash & bit) == 0) {
                    if ((e.prev = loTail) == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                    ++lc;
                }
                else {
                    if ((e.prev = hiTail) == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                    ++hc;
                }
            }
            //在这之前的逻辑是将红黑树每个节点的hash和一个bit进行&运算,
            //根据运算结果将树划分为两棵红黑树,lc表示其中一棵树的节点数
            if (loHead != null) {
            	//树的结点数<=6重新构建红黑树(前提是先调用resize方法)
                if (lc <= UNTREEIFY_THRESHOLD)
                    tab[index] = loHead.untreeify(map);
                else {
                    tab[index] = loHead;
                    if (hiHead != null) // (else is already treeified)
                        loHead.treeify(tab);
                }
            }
            if (hiHead != null) {
                if (hc <= UNTREEIFY_THRESHOLD)
                    tab[index + bit] = hiHead.untreeify(map);
                else {
                    tab[index + bit] = hiHead;
                    if (loHead != null)
                        hiHead.treeify(tab);
                }
            }
        }

总结:resize的时候会执行红黑树的拆分,拆分成两部分,拆分后小于等于6的才会转化成链表

关于hashCode方法贴出几个常见类的

String.hashCode

    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

Integer.hashCode

    public static int hashCode(int value) {
        return value;
    }

ArrayList.hashCode(继承AbstractList)

HashMap中的key最好不要使用可变类,比如ArrayList。
因为HashMap中的映射是基于key.hashCode(),如果key改变则key.hashCode()也会改变,导致最终映射错误

    public int hashCode() {
        int hashCode = 1;
        for (E e : this)
            hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
        return hashCode;
    }

线程安全的ConcurrentHashMap

hashmap不是线程安全的。
提到线程安全不得不提到Hashtable,Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

CAS + synchronized

在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受Segment的个数限制。因此,在JDK1.8中,ConcurrentHashMap的实现原理摒弃了这种设计,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CASsynchronized实现。

cas是什么呢,其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS
这里引用一下别人的话

CAS机制是一种数据更新的方式。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。

悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable华的时间可能比你的更新操作的时间还要长。

乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。

使用

import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ConcurrentHashMapDemo {
      private final ConcurrentHashMap<Integer,String> conHashMap = new ConcurrentHashMap<Integer,String>();
	  public static void main(String[] args) {
		  ExecutorService  service = Executors.newFixedThreadPool(3);
		  ConcurrentHashMapDemo ob = new ConcurrentHashMapDemo();
		  service.execute(ob.new WriteThreasOne());
		  service.execute(ob.new WriteThreasTwo());
		  service.execute(ob.new ReadThread());
		  service.shutdownNow();
	  }
	  class WriteThreasOne implements Runnable {
		@Override
		public void run() {
			for(int i= 1; i<=10; i++) {
				conHashMap.putIfAbsent(i, "A"+ i);
			}			
		}
	  }
	  class WriteThreasTwo implements Runnable {
		@Override
		public void run() {
			for(int i= 1; i<=5; i++) {
				conHashMap.put(i, "B"+ i);
			}
		}
	  }  
	  class ReadThread implements Runnable {
		@Override
		public void run() {
		   Iterator<Integer> ite = conHashMap.keySet().iterator();
	  	   while(ite.hasNext()){
	  		   Integer key = ite.next();
	  		   System.out.println(key+" : " + conHashMap.get(key));
		  }
		}
	  }	  
}

下篇文章接着写并发吧,太长了

参考
https://zhuanlan.zhihu.com/p/127147909
https://segmentfault.com/a/1190000015812438
https://tech.meituan.com/2016/06/24/java-hashmap.html
https://www.cnblogs.com/aaabbbcccddd/p/14849064.html
https://www.cnblogs.com/54chensongxia/p/12160085.html
https://pdai.tech/md/java/thread/java-thread-x-juc-collection-ConcurrentHashMap.html
红黑树没有基础
https://www.zhihu.com/question/312327402
https://www.jianshu.com/p/e136ec79235c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值