HashMap

作者: 米豆同学 | 来源:发表于2019-04-28 23:32 被阅读0次

HashMap

一个数组,每个存储了链表的头节点,put时候根据key得到hashcode ,然后计算在散列表中的位置,插入到数组对应的链表头下一个元素,get时候根据key查找,若hash code 相同,存在冲突,遍历链表,否则直接返回

static class Node<K,V> implements Map.Entry<K,V> {
     final int hash;
     final K key;
     V value;
     Node<K,V> next;

transient Node<K,V>[] table;

Object hashcode 其值就是对象的内存地址,String hashcode 计算:
String.java

    public int hashCode() {
        int h = hash;
        final int len = length();
        if (h == 0 && len > 0) {
            for (int i = 0; i < len; i++) {
                h = 31 * h + charAt(i);
            }
            hash = h;
        }
        return h;
    }

HashMap特性

hashcode & equals

hashcode 和 equals 同时复写保持一致
比如hashcode 相同,但是equals为false,那么会产生大量冲突,
如果hashcode 不同,equals 为true,相同key存在了不同的index中,不能保证key值唯一性,如果是hashset 那么无法保证数据不重复

冲突查找

冲突时候默认是链表存储,当链表长度大于8 ,则使用红黑树TreeNode查找,速度从O(n)提升为O(lgn);当红黑树中个数小于6时候,再恢复成链表查找

保证key值唯一

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;
            //注释1:如果key 的hashcode 相同并且key.equals为true 那么覆盖原来的值
            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;
    }

线程不安全

HashMap 线程不安全会出现哪些问题?

(1) 如果一个线程put ,刚好赶上扩容,这时候另外一个线程需要get可能使用的还是老的index 这样就不能得到正确的值

(2)如果两个线程同时put,并且key值产生冲突,可能会导致只有一个线程写进去了,另外一个值丢失

(3)最糟糕的还可能引起死循环,两个线程同时put,并引发resize 时候,因为resize需要把老的数据考到新的数组里面,在线程1执行到注释一处,线程二完成数据拷贝,形成环路,可以参考 https://coolshell.cn/articles/9606.html

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        ... ...

        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;
                    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
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                          //注释1
                            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);
                        
                    }
                }
            }
        }
        return newTab;
    }

扩容机制

默认长度16,个数超过16*0.75时候会扩大2倍,所以如果知道个数提前定义N/0.75 大小的hashmap 可以避免扩容,提高性能

参考文档

http://coding-geek.com/how-does-a-hashmap-work-in-java/

  • HashTable

HashTable源码中是使用synchronized来保证线程安全的

public synchronized V get(Object key) {
       // 省略实现
    }
public synchronized V put(K key, V value) {
    // 省略实现
    }
  • ConcurrentHashMap

jdk 1.7

使用了一个包含16个锁的数组,每个锁保护散列表的1/16,轻重第N个散列桶由N mod 16 个锁来保护,如果hashcode 均匀分布,大约能把对锁的请求减少到原来的1/16

缺点:

与单个锁实现独占访问相比,如果再需要获得所有锁的操作,如resize或者clear 时,开销变大。但一般情况只需要获取一个锁,如get获取数据

jdk17.png

jdk 1.8

采用CAS和synchronized来保证并发安全,给每个hash 桶的头结点加锁,put的时候如果:

(1)不存在改key对应的头结点:

则通过CAS方法加入头结点,如果当前节点是空则加入,如果不为空说明其他线程此刻已经加入,则进入下一次循环

(2) 存在头结点,对表头加锁,插入链表或者红黑树中

get 的时候没有加锁,直接查

jdk18.png

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        ...


    /**
     * The array of bins. Lazily initialized upon first insertion.
     * Size is always a power of two. Accessed directly by iterators.
     */
     //使得对table的任何更新对其它线程也都是立即可见的
    transient volatile Node<K,V>[] table;

        /**
     * Stripped-down version of helper class used in previous version,
     * declared for the sake of serialization compatibility.
     */
    static class Segment<K,V> extends ReentrantLock implements Serializable {
        private static final long serialVersionUID = 2249069246763182397L;
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

CAS (Compare And Set)解决加锁造成性能损耗的一种机制,如果内存中的值与期望的值相同则设置新值

下面例子中如果工作线程值与内存中一致,才执行++ 操作。

    private AtomicInteger ai = new AtomicInteger();  
    private int i = 0;  
   /** 使用CAS实现线程安全计数器 */  
    private void safeCount() {  
        for (;;) {  
            int i = ai.get();  
            // 如果当前值 == 预期值,则以原子方式将该值设置为给定的更新值  
            boolean suc = ai.compareAndSet(i, ++i);  
            if (suc) {  
                break;  
            }  
        }  
    }  
  
    /** 非线程安全计数器 */  
    private void count() {  
        i++;  
    }  
} 
  • Collections.synchronizedMap

HashSet

如何保证数据唯一?

内部维护了一个HashMap,add 时候以元素的值作为key,因为hashmap的key是唯一的,所以HashSet的值唯一

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }
    ...
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

相关文章

网友评论

      本文标题:HashMap

      本文链接:https://www.haomeiwen.com/subject/uvjngqtx.html