• 深入浅出学Java——HashMap


    深入浅出学Java——HashMap

    关于哈希表百度百科给出的定义。

    散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表

    给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。

    简单的概述一下:

    HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

    继承于AbstractMap

    public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {}

    基本属性

        /**
         * The default initial capacity - MUST be a power of two.
       * 默认初始容量-16.
         */
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        /**
         * The load factor used when none specified in constructor.
         * 负载因子默认为0.75.
         */
        static final float DEFAULT_LOAD_FACTOR = 0.75f; 
        /**
         * Holds cached entrySet(). Note that AbstractMap fields are used
         * for keySet() and values().
         * 初始化的默认数组
         */
        transient Set<Map.Entry<K,V>> entrySet;
    
        /**
         * The number of key-value mappings contained in this map.
         * HashMap中元素的数量
         */
        transient int size;    
        /**
         * The load factor for the hash table.
         *  判断是否需要调整HashMap的容量
         * @serial
         */
        final float loadFactor;

    主要的就是以上几个属性,简单的介绍了一下。

    注意:HashMap的扩容操作是一项很耗时的任务,所以如果能估算Map的容量,最好给它一个默认初始值,避免进行多次扩容。HashMap的线程是不安全的,多线程环境中推荐是ConcurrentHashMap。(默认的大小:16)

    首先需要强调一点,HashMap的线程不安全体现在会造成死循环、数据丢失、数据覆盖这些问题。其中死循环和数据丢失是在JDK1.7中出现的问题,在JDK1.8中已经得到解决,然而1.8中仍会有数据覆盖这样的问题。

    线程不安全

     1     /**
     2      * Implements Map.put and related methods.
     3      *
     4      * @param hash hash for key
     5      * @param key the key
     6      * @param value the value to put
     7      * @param onlyIfAbsent if true, don't change existing value
     8      * @param evict if false, the table is in creation mode.
     9      * @return previous value, or null if none
    10      */
    11     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
    12                    boolean evict) {
    13         Node<K,V>[] tab; Node<K,V> p; int n, i;
    14         if ((tab = table) == null || (n = tab.length) == 0)
    15             n = (tab = resize()).length;
    16         if ((p = tab[i = (n - 1) & hash]) == null)
    17             tab[i] = newNode(hash, key, value, null);
    18         else {
    19             Node<K,V> e; K k;
    20             if (p.hash == hash &&
    21                 ((k = p.key) == key || (key != null && key.equals(k))))
    22                 e = p;
    23             else if (p instanceof TreeNode)
    24                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
    25             else {
    26                 for (int binCount = 0; ; ++binCount) {
    27                     if ((e = p.next) == null) {
    28                         p.next = newNode(hash, key, value, null);
    29                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
    30                             treeifyBin(tab, hash);
    31                         break;
    32                     }
    33                     if (e.hash == hash &&
    34                         ((k = e.key) == key || (key != null && key.equals(k))))
    35                         break;
    36                     p = e;
    37                 }
    38             }
    39             if (e != null) { // existing mapping for key
    40                 V oldValue = e.value;
    41                 if (!onlyIfAbsent || oldValue == null)
    42                     e.value = value;
    43                 afterNodeAccess(e);
    44                 return oldValue;
    45             }
    46         }
    47         ++modCount;
    48         if (++size > threshold)
    49             resize();
    50         afterNodeInsertion(evict);
    51         return null;
    52     }

    其中第十四行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

    除此之前,还有就是代码的第48行处有个++size,我们这样想,还是线程A、B,这两个线程同时进行put操作时,假设当前HashMap的zise大小为10,当线程A执行到第48行代码时,从主内存中获得size的值为10后准备进行+1操作,但是由于时间片耗尽只好让出CPU,线程B快乐的拿到CPU还是从主内存中拿到size的值10进行+1操作,完成了put操作并将size=11写回主内存,然后线程A再次拿到CPU并继续执行(此时size的值仍为10),当执行完put操作后,还是将size=11写回内存,此时,线程A、B都执行了一次put操作,但是size的值只增加了1,所有说还是由于数据覆盖又导致了线程不安全。

    HashMap的初始容量为16,填充因子默认都是0.75,HashMap扩容时是当前容量翻倍即:capacity*2,HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸。

    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
     
    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);
     
  • 相关阅读:
    5.22 css和基本选择器
    5.21http网页基础
    ArrayList类源码浅析(二)
    ArrayList类源码浅析(一)
    Long类源码浅析
    Integer类源码浅析
    JDK中String类的源码分析(二)
    JDK中String类的源码分析(一)
    Struts2漏洞修复总结
    [LeetCode]-011-Longest Common Prefix
  • 原文地址:https://www.cnblogs.com/changyuyao/p/13677958.html
Copyright © 2020-2023  润新知