HashMap和ConcurrentHashMap

邮差的信 提交于 2019-12-11 18:31:57

HashMap和ConcurrentHashMap

JDK1.7-HashMap

实现的数据结构:数组和链表 map[] + Entry<K,V>;
map[] 为数组:结构为数组;
Entry<K,V>为链表:结构为 
{
	String key;  //key值
	String value; //value 值
	Entry<K,V> next; //下一个Entry
	int hash; //key的hashcode值
}
数组默认初始化大小为16,最大的数组长度是1<<30,加载因子是0.75
线程不安全
/**
  * map的put方法
  */
public V put(K key, V value) {
    
    /**
      * 这段代码执行,inflateTable(threshold) 会将table初始化为一个长度为16的Entry数组。
      */
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    
    /**
      * key为null时,能够进行插入,所以HashMap支持为null的key
      */
    if (key == null)
        return putForNullKey(value);
    /**
      * 计算key值得hash值
      */
    int hash = hash(key);
    /**
      * 通过key值得hash值,计算出map中数组得索引值
      */
    int i = indexFor(hash, table.length);
     /**
      * 插入之前判断该key是否插入过,如果插入过,就将原value值覆盖,同时返回旧value值
      */
    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++;
    /**
      * 插入新的entry值
      */
    addEntry(hash, key, value, i);
    return null;
}
int hash = hash(key); //计算出hash值
int i = indexFor(hash, table.length); //通过key值得hash值,计算出map中数组得索引值

final int hash(Object k) {
	int h = hashSeed;
	if (0 != h && k instanceof String) {
		return sun.misc.Hashing.stringHash32((String) k);
	}
	h ^= k.hashCode();
	h ^= (h >>> 20) ^ (h >>> 12);
	return h ^ (h >>> 7) ^ (h >>> 4);
}

static int indexFor(int h, int length) {  
        return h & (length-1);  
}  

两段代码详解:
1. hash(Object k):计算hash值,但是hash函数之后存在一系列得亦或运算,他的根本作用是增强散列性,使数据能够均匀得散列在map中,减少hash碰撞。

2.indexFor(int h, int length)原理:
hash值 & length - 1:
eg:
95=00000000000000000000000001011111
length: 16 
15=00000000000000000000000000001111

&:当值都为1时,结果为1
95=00000000000000000000000001011111
&
15=00000000000000000000000000001111
=
15=00000000000000000000000000001111

所以结果由此可见只能通过最后四位来决定,因此结果一定会在[0-15]之间(长度为4,8,32的同理可得结果都在[0-3],[0-7],[0-31]):
同时需要满足这个条件的必然需求时map的数组容量必须是2的n方数;所以HashMap的size永远是2的n方数,代码实现如下:
static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
private V putForNullKey(V value) {
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;
    addEntry(0, null, value, 0);
    return null;
}

putForNullKey详解:
当key为null时,由代码可以得出,该entry值存放位置为数组得第一个位置,而且有且只有一个key为null值得entry,当key为null值得时候,会现遍历数组第一个位置得链表,如果有key为null得entry,就会进行更新,同时会返回旧值,当然未遍历到就进行插入。
void addEntry(int hash, K key, V value, int bucketIndex) {
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}

addEntry详解:
加新的Entry会先判断当前插入是否会导致使用map的容量值 > size*0.75f(也就是扩容阈值),如果大于阈值,就是将map扩容至 2*oldSize,之后在进行插入操作;
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++;
}

createEntry详解:
获取计算的位置的链表,将entry.next指向链表首个entry,然后移位将该entry置为链表的首个entry:
table[bucketIndex] = new Entry<>(hash, key, value, e);
hashMap resize在多线程情况下会出现链表死锁的问题:
因为map在链表的插入时插入到第一个entry,当进行resize时,会导致链表倒排,当一个线程倒排时不会出现问题的,如果出现多个线程同时对同一个链表倒排,此时会出现链表死锁的情况。

JDK_1.7-ConcurrentHashMap

ConcurrentHashMap的出现主要是为了提供一个线程安全的Map,相对于HashMap,他的效率肯定会更低,但是他的优势时线程安全的;他的实现原理就是对map内的数组元素进行加锁(Segment分段锁),以此来保证线程安全性;
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock);
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问

在这里插入图片描述

JDK_1.8-HashMap

JDK_1.8相比JDK_1.7有所优化:
①. hash()方法改变
②. 当链表长度大于8时,链表转换为红黑树,提高查询效率
③. 链表插入在链表尾部,jdk_1.7 插入在链表头部
hash()方法改变:
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

解析:相较与JDK_1.7, 异或运算变少,散列性较低,如此做的原因是JDK_1.8加入了红黑树,提高了查询效率,所以把散列性也降低了。

红黑树
在这里插入图片描述

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;
        if ((p = tab[i = (n - 1) & hash]) == null)
            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) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        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;
                    p = e;
                }
            }
            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);
        }
}
链表插入在链表尾部:
if ((e = p.next) == null) { 
       p.next = newNode(hash, key, value, null);
       if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
            break;
}

解析: 当链表的next为空,说明这是链表末端。
链表插入在链表尾部解决了JDK_1.7的链表死锁问题;
为什么JDK_1.7链表需要在头端插入而不是末端?
解析:JDK_1.7从头部插入时因为插入速度快,而且不需要遍历整个链表。

插入时会遍历链表,当发现链表有key时会进行更新,那这样也相当于遍历过链表?
解析:插入时遍历可能会在遍历到第一个位置时就找到结果,之后就不会进行遍历,这样更新操作的效率会变快。

为什么JDK_1.8链表需要在末端插入?
解析:因为JDK_1.8本来就需要遍历链表来判断是否将链表转换为红黑树,而且最关键的是解决了链表死锁的问题。
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!