HashMap 与 ConcurrentHashMap - 20181207

HashMap浅析

java7中的hashmap,与数组中元素相同hashcode位置的元素,是以链表的方式存在,链接在元素的头节点处,而java8则是链接在尾节点。(第一个不同点)
首先,会将新加入的元素链到旧元素的头部,然后将这个头部赋值到旧元素的位置,完成新元素加节点的操作。(java7的添加原理)

public class MyHashMap<K,V> {

    //自定义一个节点
    class Entry<K,V>{
        private K k;
        private V v;
        private Entry<K,V> next;

        public Entry() {
        }
        public Entry(K k, V v, Entry<K, V> next) {
            this.k = k;
            this.v = v;
            this.next = next;
        }

        public K getK() {
            return k;
        }
        public void setK(K k) {
            this.k = k;
        }
        public V getV() {
            return v;
        }
        public void setV(V v) {
            this.v = v;
        }
        public Entry<K, V> getNext() {
            return next;
        }
        public void setNext(Entry<K, V> next) {
            this.next = next;
        }
    }

    private Entry[] table;  //自定义存储数组
    private static Integer CAPACITY=8;  //自定义数组长度
    private int size=0; //集合长度,在addEntry中++

    public MyHashMap() {
        this.table = new Entry[CAPACITY];   //构造方法初始化数组
    }

    //增加节点
    private void addEntry(K key, V value, int i) {
        Entry entry = new Entry(key, value, table[i]);
        table[i]=entry;
        size++;
    }

    //put方法
    public Object put(K key,V value){

        //元素基于hashcode在数组中找寻下标
        int hash=key.hashCode();
        int i=hash%8;

        //更新链接的链表 数组这个位置元素;有下个元素;链表中的下一个节点
        for(Entry<K,V> entry=table[i];entry!=null;entry=entry.next){
            if(entry.k.equals(key)){    //原来的值与传进来的值,key相等
                V oldValue=entry.v;
                entry.v=value;
                return oldValue;
            }
        }

        addEntry(key, value, i);

        return null;
    }

    //get方法
    public Object get(Object key){
        int hash=key.hashCode();
        int i=hash%8;   //找到下标

        //循环链表
        for(Entry<K,V> entry=table[i];entry!=null;entry=entry.next){
            if(entry.k.equals(key)){    //判断key
                return entry.v;
            }
        }
        return null;
    }

    //size方法:统计有多少元素
    public int size(){
        //不之间length数组,也不以循环数组和链表的形式
        return size;
    }
}
  • jdk7与jdk8的区别
  1. jdk8中会将链表转会转变为红黑树
  2. 新节点插入链表的顺序不同(7是插头节点,8插尾节点)
  3. hash算法的简化
  4. resize逻辑的修改(7扩容会死锁需指定阀值,8不会)
  • jdk1.7
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始化容量 1向左移4位 即初始化容量16
//【初始化桶大小,因为底层是数组,所以这是数组默认的大小。】
static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量
//【桶最大值】
static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子,用于扩容
//【默认的负载因子】
static final Entry<?,?>[] EMPTY_TABLE = {}; //定义个Entry数组,类似上边手写的,增加了一个hash值

transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE; //初始化成上边的table
//【table 真正存放数据的数组】
transient int size;	//逻辑长度
//【Map 存放数量的大小】
int threshold; //根据加载因子,计算出阀值
//【桶大小,可在初始化时显式指定】
final float loadFactor; //可修改加载因子
//【负载因子,可在初始化时显式指定】
transient int modCount; //和线程并发有关 修改次数?

 //273行
 public HashMap(int initialCapacity) {	//有参构造,主要是赋值用
        this(initialCapacity, DEFAULT_LOAD_FACTOR); //初始容量,加载因子
    }

 //311行   初始化table数组,按照容量大小来算
 private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize 找到一个2的次方数字
        int capacity = roundUpToPowerOf2(toSize);
	   //修改阀值 容量*加载因子
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity); //计算hash值
  }

  //486行
  public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); //阈值扩容 roundUpToPowerOf2 判断填充容量因子
        }
        if (key == null) //当key为null时,调用putForNullKey方法,讲value放置在数组第一个位置
            return putForNullKey(value);
        int hash = hash(key); //传k进入,返回哈希值/哈希码
        int i = indexFor(hash, table.length); //确认位置,得到存储在数组中的位置
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue; //进行交换,出来新值
            }
        }
        modCount++;
        addEntry(hash, key, value, i); //往数组添加的方法
        return null;
    }

  //877行
  void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) { //大过阈值 且 数组放满(1.8取消这一特性)不为空
            resize(2 * table.length); //数组扩容 + transfer //数组拷贝
            hash = (null != key) ? hash(key) : 0; //哈希计算
            bucketIndex = indexFor(hash, table.length); //重新算出位置下标
        }
        createEntry(hash, key, value, bucketIndex); 
    }

  //895行
  void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
  }

归纳起来,简单的说HashMap在底层讲k-v当成一个整体Entry对象。HashMap底层采用Entry[]数组来保存k-v对,当需要存储一个Entry对象时,会根据hash算法来决定其在数组中的存储位置,再根据equals方法决定其在链表中的位置;取出时同理。

  • jdk1.8
  1. 思考: 为何1.8使用了红黑树?
    树结构相比链表插入效率会低些,但查询效率则高许多;红黑树相比完全平衡二叉树插入效率高,但查询效率低于完全平衡二叉树
    其实,红黑树链表完全平衡二叉树之间,取了折中。
    因为hashmap不单仅是插入,也不单仅是查询,是需要平衡的,所以选用了红黑树。
//改动1:新增两个关键属性
static final int TREEIFY_THRESHOLD = 8;	//当链表达到8,会转成红黑树,提升查询效率
//【链表 --> 红黑树】
static final int UNTREEIFY_THRESHOLD = 6; //红黑树节点变成6个,会还原成链表
//【红黑树 --> 链表】
//336行 改动2:因为选择红黑树,可以使用较差的散列性,所以hash算法没1.7繁琐
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

//624行 put    
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i; //new数组,节点,下标
        if ((tab = table) == null || (n = tab.length) == 0) //空数组
            n = (tab = resize()).length; //resize 新增初始化功能
        if ((p = tab[i = (n - 1) & hash]) == null) //算出数组下标i
            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) {
//改动3:加尾节点原因?要对链表判断,是不是需要树化,需要遍历;反正需要遍历到最后元素,就将新节点加到链表的尾节点
                    if ((e = p.next) == null) { //表示循环到最后一个节点
                        p.next = newNode(hash, key, value, null); //正常生成新的
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //统计个数,到8-1变成树
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { //新值放到数组
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold) //改动4.resize的改动,jdk1.7存在扩容改动位置的问题,移动位置会产生死锁问题;1.8顺序固定。
            resize();
        afterNodeInsertion(evict);
        return null;
    }    

HashMap 与 ConcurrentHashMap 在7,8中变化

1. java7 HashMap

在这里插入图片描述

这个仅仅是示意图,因为没有考虑到数组要扩容的情况,具体的后面再说。

大方向上,HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。

上图中,每个绿色的实体是嵌套类 Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。

capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。

loadFactor:负载因子,默认为 0.75。

threshold:扩容的阈值,等于 capacity * loadFactor

2. java7 ConcurrentHashMap

在这里插入图片描述

ConcurrentHashMap 和 HashMap 思路是差不多的,但是因为它支持并发操作,所以要复杂一些。

整个 ConcurrentHashMap 由一个个 Segment 组成,Segment 代表”部分“或”一段“的意思,所以很多地方都会将其描述为分段锁。注意,行文中,我很多地方用了 “槽” 来代表一个 segment。

简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。

concurrencyLevel: 并行级别、并发数、Segment 数,怎么翻译不重要,理解它。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以理论上,这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的。

再具体到每个 Segment 内部,其实每个 Segment 很像之前介绍的 HashMap,不过它要保证线程安全,所以处理起来要麻烦些。

3. java8 HashMap

在这里插入图片描述

Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。

根据 Java7 HashMap 的介绍,我们知道,查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。

为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

Java7 中使用 Entry 来代表每个 HashMap 中的数据节点,Java8 中使用 Node,基本没有区别,都是 key,value,hash 和 next 这四个属性,不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。

4. java8 ConcurrentHashMap

在这里插入图片描述

Java7 中实现的 ConcurrentHashMap 说实话还是比较复杂的,Java8 对 ConcurrentHashMap 进行了比较大的改动。建议读者可以参考 Java8 中 HashMap 相对于 Java7 HashMap 的改动,对于 ConcurrentHashMap,Java8 也引入了红黑树。

说实话,Java8 ConcurrentHashMap 源码真心不简单,最难的在于扩容,数据迁移操作不容易看懂。

https://mp.weixin.qq.com/s/thHjDzkymzb4X76dQCSLNg##

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值