java map 实现原理

本文源码基于jdk1.8

java 中常见的集合 list map set 其中list 和set都是collection的之类,map不是,是键值对应容器

常见的map 集合有 hashmap hashTable linkedHashMap ConcurrentHashMap

值是key -value 键值对的形式

比较:

key-value是否可为null线程安全是否有序实现方式继承类实现类效率
hashMap不安全无序数组加链表,1.8以后增加红黑树
AbstractMap
Map
Cloneable
Serializable
hashTable安全无序       和hashMap 一样也是散列表
Dictionary
Map

Cloneable
Serializable
linkedHashMap不安全有序在hashMap基础上加了双向的链表
HashMap
Map
Serializable
ConcurrentHashMap安全无序分多个桶,减少开销
AbstractMap
ConcurrentMap
Serializable

hashMap

特点: key 唯一不允许重复,允许key,value 为null 的情况 线程不安全,存值无序

原理:数组加链表(1.8以前),1.8之后添加了红黑树,基于hash表的map接口实现,

阈值(边界值)> 8 并且桶位数(数组长度)大于 64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询;

引入红黑树的原因:无论hash如何取值,也无法保证百分百的均匀分布元素,当 HashMap 中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候 HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。

针对这种情况,jdk1.8 中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。当链表长度很小的时候,即使遍历,速度也非常快,但是当链表长度不断变长,肯定会对查询性能有一定的影响,所以才需要转成树。

初始化数组大小16 最大值1<<30 初始负载因子 0.75

  • 容量:容量为数组的长度,亦即桶的个数,默认为16 ,最大为2的30次方,当容量达到64时才可以树化。
  • 装载因子:装载因子用来计算容量达到多少时才进行扩容,默认装载因子为0.75。当 HashMap 里面容纳的元素已经达到 HashMap 数组长度的 75% 时,表示 HashMap
    太挤了,需要扩容,而扩容这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数,可以通过创建HashMap 集合对象时指定初始容量来尽量避免。原因:当负载因子越大,接近于1,数组中存放的数据就越多,越挤,会导致查询速度变慢,相反如果太小是0.4或者更低,会导致过小就开始扩容,导致利用率太低。所以为了兼顾两方面,同时从最终的实验数据得出最佳的数值0.75;
  • 树化:树化,当容量达到64且链表的长度达到8时进行树化,当链表的长度小于6时反树化。
  • 当链表长度低于6会从红黑树转化成链表

第一次当我们put数据的时候,先重写hashcode 的值,得到hash值,然后将数据存在数组的对应下标位置。当再次put数据的时候,也是先进行上面的操作,找到对应位置,如果该桶的位置已经存在值了,则比较hash值,如果不相同,则下面生成一个链表,用来存放,如果相同,

则比较数值,

值数据也相同:直接覆盖,

数据不同:从该桶位的链表开始,一直往下比,直到出现不同的时候,便存在不同的地方的下一个位置,如果这个时候链表长度超过了8,那么链表就会转化成红黑树

扩容,在不断的添加数据的时候,如果刚好达到阀值,同时我们要添加的数据位置不为空,则需要扩容,每次扩容是原来的大小的2倍。

继承体系:

  • HashMap 实现了Cloneable接口,可以被克隆。
  • HashMap 实现了Serializable接口,属于标记性接口,HashMap 对象可以被序列化和反序列化。
  • HashMap 继承了AbstractMap,父类提供了 Map 实现接口,具有Map的所有功能,以最大限度地减少实现此接口所需的工作。

关于红黑树:

红黑树是一个平衡的二叉搜索树,同时他只要求部分地达到平衡,任何不平衡都会在三次旋转之内解决。故而期增删的效率相对AVL高,查询效率相对较低。
红黑树每个节点都有存储位,来表示节点的颜色颜色是红或者黑。
特点:每个节点是红色或者黑色
根节点是黑色
每个叶子节点都是黑色 叶子节点是只为空的节点(NIL或者null)
如果一个节点是红色,则他的子节点必须是黑色
从一个节点到其左右子树叶子节点的所有路径上 包含相同数目的黑节点,确保没有一条路径会比其他路径长两倍,因而,红黑树是相对接近平衡的二叉树。
下面是右旋操作 ,左旋则刚好相反。

 当长度大于8 的时候,红黑树的效率比链表要高,

小于8的时候,效率要低,多以才用红黑树这个数据结构来优化性能。

扩容机制: 上源码

/**
     * Initializes or doubles table size.  If null, allocates in
     * accord with initial capacity target held in field threshold.
     * Otherwise, because we are using power-of-two expansion, the
     * elements from each bin must either stay at same index, or move
     * with a power of two offset in the new table.
     *
     * @return the table
     */
    final Node<K,V>[] resize() {
        //把旧的table 赋值个一个变量
        Node<K,V>[] oldTab = table;
        //获取旧的tabel的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        // 旧的阈值
        int oldThr = threshold;
        int newCap, newThr = 0;
    
        if (oldCap > 0) {
            //判断数组的长度是否大约等于最大值
            if (oldCap >= MAXIMUM_CAPACITY) {
                //如果数组的长度达到了最大值,那么就不在进行扩容,直接返回,不管了任由hash冲突
                threshold = Integer.MAX_VALUE;
                return oldTab;
            //把旧的数组长度左移一位(也就是乘以2),然后判断是否小于最大值,并且判断旧的数组长度是否大于等于默认的长度16
            }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //如果条件成立就把旧的阈值左移一位复制给新的阈值
                newThr = oldThr << 1; // double threshold
        }//如果就的数组长度小于0并且旧的阈值大于0
        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);
        }
        //如果新的阈值等于0
        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) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    //如果没有子元素那么说明是下面不是一个链表,直接通过 hash&(新的数组长度-1)计算出新的位置,把就的数据放入新的位置
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    //如果该节点为红黑树结构,就进行树的操作
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        //有多个数据并且不是树那么该节点上放的是链表
                        //这里是java1.8很精妙的地方,如果 e.hash& 旧的数组长度 如果等于0
                        那么该数据的位置没有发生变化,还在原来的索引位置上,如果不等于0 那么就在该值就在 (原来的索引位置+旧的数组长度)的位置上,
                        这里重新创建了两个节点,在原来位置上的放入loHead中,在新的位置上的放入
hiHead 中,最后把这两组数据放入新的数组中即可。(这里的精妙之处是不用重新计算每一个数据的hash,就可以把旧的数据放入新的数组中去)
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            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);
                        if (loTail != null) {
                            loTail.next = null;
                            //这里把在原来索引位置上的放入新的数组中去
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                             //这里把在原来的索引位置+旧的数组长度)的位置上,放入新的数组中去
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
 

hashTable 

HashTable 与 HashMap 一样,也是链表散列,存储键值对,但 HashTable 继承了 Dictionary 类,实现了 Map、Clonable、Serializable 接口

初始化大小 11 负载因子0.75

public Hashtable() {
    this(11, 0.75f);
}

方法加了synchronized 线程安全 但是效率较低

put 过程:

HashTable的put过程
1、判断value不能为null,若为null抛出异常-》hashtable中value不能为null
2、通过key进行hash获取到key该存储的索引位置
3、该索引位置的链表进行遍历,获取key是否存在(key存在条件 hash相等且通过key.equals判断相等)
4、在存在该key的情况下,将value值进行更新且直接返回
5、key不存在则进行新节点插入逻辑
5.1、扩容考虑:entry节点个数大于阈值 (count>threshold)进行扩容
5.2、新容量大小为:2*table.length+1
5.3、将原哈希表中的数据全部进行重新hash到新的hash表中
5.4、更新插入的key的新的位置
5.5、找到新节点位置,创建entry实体通过头插入将元素插入
和hashMap 的区别:
相同点
1、底层数据结构都为数组+链表
2、key都不能重复
3、插入元素有不能保证插入有序
4、哈希过程通过key进行hash
不同点:
1、安全性问题:
HashMap不能保证线程安全
HashTable能保证线程
2、继承关系:
HashMap继承自AbstractMap
HashTable继承自Dictionary
3、null值问题
HashMap的key和value都可以为null
HashTable的key和value都不能为null
4、扩容方式
HashMap按照2table.length
HashTable按照2table.length+1
5、默认值
HashMap默认数组大小为16
HashTable默认数组大小为11
6、hash算法不同
7、效率不同
HashMap在单线程小效率高
HashTable在单线程小效率低

linkedHashMap

hashMap的子类,所以hashMap的属性他基本都有,

区别在于,他是有序的;只是加了指针,把元素串联了起来 

原理:双向链表 加hash

ConcurrentHashMap

主要分为1.7版本和1.8版本以后:

1.7版本 采用的的分段锁的机制

存储结构如下图:

ConcurrnetHashMap 由很多个 Segment 组合,而每一个 Segment 是一个类似于 HashMap 的结构,所以每一个 HashMap 的内部可以进行扩容。但是 Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,你也可以认为 ConcurrentHashMap 默认支持最多 16 个线程并发。

初始化逻辑。

必要参数校验。
校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无惨构造默认值是 16.
寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。
记录 segmentShift 偏移量,这个值为【容量 = 2 的N次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
初始化 segments[0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。

由于 Segment 继承了 ReentrantLock,所以 Segment 内部可以很方便的获取锁,put 流程就用到了这个功能。

tryLock() 获取锁,获取不到使用 scanAndLockForPut 方法继续获取。

计算 put 的数据要放入的 index 位置,然后获取这个位置上的 HashEntry 。

遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。

如果这个位置上的 HashEntry 不存在:

如果当前容量大于扩容阀值,小于最大容量,进行扩容。
直接头插法插入。
如果这个位置上的 HashEntry 存在:

判断链表当前元素 Key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值
不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。
如果当前容量大于扩容阀值,小于最大容量,进行扩容。
直接链表头插法插入。
如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null.
1.8的结构

放弃了segment,引入了红黑树:

不再是之前的 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。当冲突链表达到一定长度时,链表会转换成红黑树。

/**
 * Initializes table, using the size recorded in sizeCtl.
 */
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
        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;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

从源码中可以发现 ConcurrentHashMap 的初始化是通过自旋和 CAS 操作完成的。里面需要注意的是变量 sizeCtl ,它的值决定着当前的初始化状态。

-1 说明正在初始化
-N 说明有N-1个线程正在进行扩容
表示 table 初始化大小,如果 table 没有初始化
表示 table 容量,如果 table 已经初始化。
 

put 方法

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

根据 key 计算出 hashcode 。

判断是否需要进行初始化。

即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。

如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。

如果都不满足,则利用 synchronized 锁写入数据。

如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
 

总结:

Java7 中 ConcruuentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。

Java8 中的 ConcruuentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又退回链表。

有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的锁升级。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值