HashMap相关

一、HashMap 和 Hashtable的区别

  1. 两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全
    Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合(Collections.synchronizedMap()实现原理是Collections定义了一个SynchronizedMap的内部类,这个类实现了Map接口,在调用方法时使用synchronized来保证线程同步,当然了实际上操作的还是我们传入的HashMap实例,简单的说就是Collections.synchronizedMap()方法帮我们在操作HashMap时自动添加了synchronized来实现线程同步,类似的其它Collections.synchronizedXX方法也是类似原理
  2. HashMap可以使用null作为key,而Hashtable则不允许null作为key
    虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事
    HashMap以null作为key时,总是存储在table数组的第一个节点上
  3. HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类
  4. HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75
    HashMap扩容时是当前容量翻倍即:capacity*2,Hashtable扩容时是容量翻倍+1即:capacity*2+1
  5. 两者计算hash的方法不同
    Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length; hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;

    HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸

    static int hash(int h) {
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    
     static int indexFor(int h, int length) {
            return h & (length-1);
        } int hash(int h) {
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }
    
     static int indexFor(int h, int length) {
            return h & (length-1);
        }
  6. HashMap和Hashtable的底层实现都是数组+链表结构实现

 

二、HashSet 和 HashMap、Hashtable的区别

 

        除开HashMap和Hashtable外,还有一个hash集合HashSet,有所区别的是HashSet不是key value结构,仅仅是存储不重复的元素,相当于简化版的HashMap,只是包含HashMap中的key而已

        通过查看源码也证实了这一点,HashSet内部就是使用HashMap实现,只不过HashSet里面的HashMap所有的value都是同一个Object而已,因此HashSet也是非线程安全的,至于HashSet和Hashtable的区别,HashSet就是个简化的HashMap的,所以你懂的
下面是HashSet几个主要方法的实现

  private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
  
  public HashSet() {
    map = new HashMap<E,Object>();
  }
  public boolean contains(Object o) {
    return map.containsKey(o);
  }
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }
  public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
  }

  public void clear() {
    map.clear();
  }private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();
  
  public HashSet() {
    map = new HashMap<E,Object>();
  }
  public boolean contains(Object o) {
    return map.containsKey(o);
  }
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }
  public boolean add(E e) {
    return map.put(e, PRESENT)==null;
  }
  public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
  }

  public void clear() {
    map.clear();
  }

 

三、HashMap 和 Hashtable的实现原理

 

    HashMap和Hashtable的底层实现都是数组+链表结构实现的,这点上完全一致

    添加、删除、获取元素时都是先计算hash,根据hash和table.length计算index也就是table数组的下标,然后进行相应操作。

参考资料:  https://blog.csdn.net/u011202334/article/details/51496381

我们可以使用CocurrentHashMap来代替HashTable吗?这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道HashTable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。

 

 

 

四、ConcurrentHashMap的实现

1. ConcurrentHashMap是如何实现线程安全的?

 

   1.1、JDK1.7中是用Segment(extends ReentrantLock)来实现。

    /**
     * The segments, each of which is a specialized hash table.
     */
    final Segment<K,V>[] segments;//ConcurrentHashMap 有一个Segment数组,也就是说他里面有很多锁

来看看Segment是何方圣神

    //原来就是一把锁
    static final class Segment<K,V> extends ReentrantLock implements Serializable {


        /**
         * The per-segment table. Elements are accessed via
         * entryAt/setEntryAt providing volatile semantics.
         */
        transient volatile HashEntry<K,V>[] table;//这里的结构就和HashMap 差不多了

来看看怎么put的

    public V put(K key, V value) {
        Segment<K,V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        return s.put(key, hash, value, false);//哈哈,原来是根据hash获取到元素要放在哪个Segment中,然后调用了Segment的put方法
    }

那看看Segment的put是什么鬼

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null ://重点 先tryLock获取锁
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();//重点 释放锁
            }
            return oldValue;
        }

好了,总结一下

JDK1.7中ConcurrentHashMap是用一个二维数组链表来实现的。其实就是一个Segment的数组,每个Segment又是一个数组链表,而且是一个锁,每次对同一个Segment中元素进行写操作的时候,会锁住整个Segment。
那和HashTable比较一下,有什么好处呢?
好处就是,ConcurrentHashMap写操作只会锁一段(锁住Segment中所有元素),对不同Segment元素的操作不会互相阻塞,而HashTable用的是synchronized,会锁住整个对象,相当于一个HashTable上的操作都是并行的,连get方法都会阻塞其他操作。
换个说法吧,一个HashTable只有一把锁,最多只有一个线程获取到锁。
ConcurrentHashMap有很多把锁(比如16),那么此时最多支持16个并发(一个并发一把锁,人人有份,不用抢),当然了,最理想的场景是16个并发操作的Segment都不一样。

 

 

 

   1.2、JDK1.8如何实现线程安全。

    改进一:取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

    改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

为了说明以上2个改动,看一下put操作是如何实现的。

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;
        // 如果table为空,初始化;否则,根据hash值计算得到数组索引i,如果tab[i]为空,直接新建节点Node即可。注:tab[i]实质为链表或者红黑树的首节点。
        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
        }
        // 如果tab[i]不为空并且hash值为MOVED,说明该链表正在进行transfer操作,返回扩容完成后的table。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 针对首个节点进行加锁操作,而不是segment,进一步减少线程冲突
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 如果在链表中找到值为key的节点e,直接设置e.val = value即可。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            // 如果没有找到值为key的节点,直接新建Node并加入链表即可。
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 如果首节点为TreeBin类型,说明为红黑树结构,执行putTreeVal操作。
                    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) {
                // 如果节点数>=8,那么转换链表结构为红黑树结构。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 计数增加1,有可能触发transfer操作(扩容)。
    addCount(1L, binCount);
    return null;
}

时间过得真快,不写这个博客,还不知道1.8改动这么大,逝者如斯夫,不舍昼夜,吾将上下而求索。

 

 

五、Java 8系列之重新认识HashMap

    JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化等。

参考资料:  https://tech.meituan.com/java-hashmap.html

 

 

六、HashMap多线程死循环问题

 

参考资料: https://blog.csdn.net/xuefeng0707/article/details/40797085

                 https://coolshell.cn/articles/9606.html

 

七、补充

   hashMap为啥初始化容量为2的次幂

   hashMap源码获取元素的位置:
   static int indexFor(int h, int length) {
      // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
      return h & (length-1);
   }
  解释:
   h:为插入元素的hashcode
   length:为map的容量大小
   &:与操作 比如 1101 & 1011=1001
   如果length为2的次幂  则length-1 转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,
   而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,
   最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值