HashMap源码理解与ConcurrentHashMap了解

HashMap源码分析
HashMap底层基于红黑树+hash表;extends AbstractMap,实现了clonable和序列化接口
有两个参数影响性能:负载因子和初始容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;
负载因子,默认为0.75;经过科学计算的值;如果大于0.75会增加哈希表的利用效率,但是会增大哈希冲突的概率;小于0.75会减少哈希冲突,但是会降低hash表的利用效率

static final int TREEIFY_THRESHOLD = 8;
树化阈值,当某个桶节点下的链表长度超过8就将链表树化

static final int UNTREEIFY_THRESHOLD = 6;
解树化阈值,当红黑树在进行了扩容或者删除操作后个数<=6的时候,在下一次resize操作的时候将红黑树退化为链表,节省空间。
size是当前集合元素个数
默认容量:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; =16
int threshold=cap*加载因子;(达到这个值就开始扩容,第一次为12)

构造方法四个:
1)创建一个hashMap默认初始化容量为16,加载因子为0.75,其他属性默认
2)传入一个int型的初始化容量
3)传入一个float型的加载因子和int型初始化容量
4)(这个比较难,我自己是简单化理解了,具体实现还是要结合源码)传入一个任意的Map对象,将它变为HashMap

内部结构:
1.transient Node<K,V>[] table; Node是单向链表的一个节点,是HashMap中的一个静态内部类,实现了MapEntry接口,单向链表的一个节点,Node节点的hashcode值是key和value分别的hashCode值的异或。
2.TreeNode<K,V> 继承LinkedHashMap.Entry,红黑树是一种特殊的二叉查找树,红黑树的每个节点上都有存储位表示节点的颜色,可以是红或黑。

红黑树的特征:
1)根节点是黑节点;
2)每个节点不是黑节点就是红节点
3)如果一个节点是红节点,那么他的子节点必须为黑节点
4)每个为(null或NIL)的叶子节点是黑色。
5)是平衡二叉树。
6)左子树上所有结点的值都小于或者等于他的根节点的值;右子树上所有的值都大于或等于他根节点的值。所以左右子树都为二叉搜索(排序)树。
7)从任意节点到其每个叶子的所有路径都包含相同数目的黑色节点。
主要用红黑树来存储有序数据,时间复杂度为O(lgn)效率很高。

内部hash实现:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

插入数据(主要实现在putVal方法中)
传进一个key值,若key值为null就把他放在第一个位置,否则保留高16位,将他的高十六位与他的hashCode值进行亦或运算,因为hashCode值比较大(值越大,越高位就是有效位),异或操作是让高低十六位打乱,减少hash冲突。hash计算是为了计算找到对应的桶下标。不直接用Object类的hashCode是因为计算出来的hashcode值太大了,需要开辟大量空间并且很多都是无用的。

hash表通过key的hashcode%n取余就是桶数组中所在的位置

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //定义临时Node节点数组,p是任意节点变量,n是节点数组长度,i是索引位置
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //HashMap也是采用懒加载模式,此时还没有初始化,进行初始化操作
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//第一次扩容时桶的数量为默认容量16
            //此时hash表对应的下标还没有存储元素,i=(n-1)&hash)真正数组下标的计算;相当于取模操作
            //n一定是2的次方,n-1的二进制码就一定是全1,这样保证hash表中的所有索引都有可能被访问到
         if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            //此时冲突位置的key和要保存元素的key值相等,把已经存在的值赋给临时变量e
            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);
                        //链表长度>=7在下一次存储前树化;因为是++binCount
                        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;
                        //遍历桶中的链表,与前面的e=p.next进行组合,作为遍历链表的条件
                    p = e;
                }
            }
            /**如果在桶中找到了key、hash值都相等的结点,新值替换旧值
            **/
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;//表结构被修改的次数增加
        //当前存放的元素个数大于容量,进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

树化操作

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);
        }
    }

扩容操作:
resize() 初始化或者对桶的大小进行扩容,为null就按threshold=16来进行分配,每次扩容为原来的2倍
在hashMap中的键值对大于阈值或者初始化时,调用resize方法进行扩容,都是扩展2倍,扩展后Node对象的位置要么在原位置要么移动到偏移量两倍的位置。
1)hashMap初始化采用懒加载模式,在第一次添加元素的时候进行初始化,第一次调用put方法时才会真正地为数组分配空间。
2)桶数组中每个元素都是一个链表或红黑树,数组初始化长度是16,把数组中每一格称为一个桶,当数组中已经被使用的桶的数量超过了threshold时,就要进行扩容。
3)每个桶中都是一个链表,当链表长度超过树化阈值就要进行树化,红黑树长度小于解树化阈值就退化为链表,节省空间。
获取数据get(Object key)
主要由getNode()实现

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;
    }

通过key值计算出当初存放的位置,然后从第一项开始找,如果没有找到,则通过f((e=first.next)!=null)查询下一个节点,如果是红黑树节点,就在红黑树中找,否则就在链表中查找。

HashMap根据value怎么找到key?
方法1:自定义一个getKey方法,传进来一个map和一个value;定义一个list(因为HashMap值不唯一;重复的key放在一个集合中)然后for循环遍历keySet,使用equals方法判断如果key.get(key)和value相等,就将当前key值放入list中。
方法二:将map中的每个键值对通过keySet变成set集合的对象;使用迭代器遍历;如果entry.getvalue().equals(value)则加入list中。

JDK1.7与JDK1.8中ConcurrentHashMap设计的区别与如何高效的实现线程安全
1.JDK1.7使用Lock体系中的ReentrantLock来保证线程安全,将hashtable中一把锁锁整张表优化为Segement(分段锁,16把锁,每把锁锁的是桶对象)
2.JDK1.8将锁的粒度进一步细化,每个桶的头结点加一把锁,底层基于红黑树和hash表,结构类似于HashMap,锁的数量会随着hash表的增加而增加,支持并发线程数进一步提高;使用Synchronized+CAS操作来保证线程安全。数据存储使用volatile保证可见性。内部虽然还有Segment的定义,但是仅仅是为了保证序列化时的兼容性,不再有任何结构上的用处,因为不使用Segment,初始化操作大大监护,修改为lazy-load形式,避免初始开销。
不同的segment是同步还是异步?大的Segement是不能扩容的,下面的每个子Segement可以扩容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值