【HashMap源码一】Java7-HashMap源码学习笔记(JDK1.7--put方法-get方法-出现死循环CPU100%原因等问题)

Java7–HashMap

常量

// 默认容量  16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 默认加载因子 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;


/**表未膨胀时要共享的空表实例。
*/
static final Entry<?,?>[] EMPTY_TABLE = {};

/**表,根据需要调整大小。长度必须始终是 2 的幂。
*/
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;


/**此映射中包含的键值映射的数量。
*/
transient int size;


/**
* 要调整大小的下一个大小值(容量负载因子)。 阈值, 是通过加载因子算出来的
*/
// If table == EMPTY_TABLE 那么这是初始容量
// 膨胀时将创建table。
int threshold;

/**加载因子
*/
final float loadFactor;

构造方法

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 *	构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
 */
public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

调用无参构造会去调用有参构造, 传入: 默认容量16, 默认加载因子 0.75

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    this.loadFactor = loadFactor; //默认0.75
    threshold = initialCapacity;//默认16
    init();
}


/** 子类linkedlist中有用
子类的初始化挂钩。在初始化 HashMap 之后但在插入任何条目之前,在所有构造函数和伪构造函数(clone、readObject)中调用此方法。 (在没有这个方法的情况下,readObject 需要明确的子类知识。)
     */
void init() {
}
  • 进行一系列判断赋值

put方法

public V put(K key, V value) {
    if (table == EMPTY_TABLE) { //判断当前的table是否为空
        inflateTable(threshold); //1,为空就初始化, -----------------------------------------详细方法在下面1-------------------
    }
    if (key == null) // HashMap的key可以为null
        return putForNullKey(value);//2,相当于key为null 放到数组的下标0的位置-----------------详细方法在下面2-------------------
    int hash = hash(key); //4,计算hash值----------------------------------------------------详细方法在下面4-------------------
    int i = indexFor(hash, table.length); //3,拿hash值和数组长度获得一个数组下标--------------详细方法在下面3-------------------
    //Entry<K,V> e = table[i];-------------循环从table[i]开始,链表头节点
    // e = e.next------------取下一个节点
    //e != null; ------------ 说明取到最后一个节点, 遍历所有元素
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //判断 hash值是否相等    &&            key是否相等
        //hash值相等, key不一定相等, hash值不相等, key一定不相等 
        //key不相等, hash值一定不相等 , 所以先比较hash ,为false可以直接跳过,节省
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            //如果有key相同的就覆盖
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);//HashMap没有使用这个方法,是为了子类使用
            //赋值完成后返回旧的value
            return oldValue; //找到之后就return, 好处:不用每次都遍历到尾部
        }
    }

    modCount++;//9代表修改次数,跟异常有关-----------------------------------------------------详细解释在下面9--------------------
    addEntry(hash, key, value, i);//5,添加元素------------------------------------------------详细方法在下面5-------------------
    return null; //put方法如果不重复返回null
}

1,初始化方法

//1, 初始化方法
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize  找出>=2的幂次方的数,  2,  4, 8, 16 ...
    //			比如传进来10 , 返回16, 16返回16
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
        //计算
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        int capacity = roundUpToPowerOf2(toSize);

        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

}

如何获得一个大于等于2的tosize的 2的幂次方数
//找规律
1 --------  0000 0001
2 --------  0000 0010
4 --------  0000 0100
8 --------  0000 1000
16 -------  0001 0000
//二进制中, 只有一个bit点为1 的数
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

public static int highestOneBit(int i) {   //传进来一个17
    // HD, Figure 3-1   |运算  有1填1
    i |= (i >>  1);		/*    17 0001 0001	 
    					  >>  1  0000 1000
                          |  	 0001 1001
    					*/ 
    i |= (i >>  2);   //  >>  2  0000 0110
    				  //  |      0001 1111
    i |= (i >>  4);   //  >>  4  0000 0001
    				  //  |      0001 1111
    i |= (i >>  8);   //  >>  8  0000 0000
    				  //  |      0001 1111  不会变了
    i |= (i >> 16);		
    return i - (i >>> 1);  //>>1 0000 1111
    					   //i-  0001 0000 ----16
}
        System.out.println(Integer.highestOneBit(6));  //4
        System.out.println(Integer.highestOneBit(7));  //4
        System.out.println(Integer.highestOneBit(8));  //8
        System.out.println(Integer.highestOneBit(10));  //8
        System.out.println(Integer.highestOneBit(16));  //16
传入的数字  001* ****
    >>1    0001 ****
    |      0011 ****
    >>2    0000 11**
    |      0011 11**
    >>4    0000 0011
    |      0011 1111  最后都会把高位1后面的*全部变成1
           0010 0000  最后执行的i - (i >>> 1);
相当于是获取传入的数子高位后面全是0的数
    001* ****
    0010 0000  所以就获得了2 的幂次方的数
为什么要右移 8  16 这么多?
    因为int 4字节 32个bit位
再回到roundUpToPowerOf2方法
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
        ? MAXIMUM_CAPACITY
        : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
//17--highestOneBit--16
//我们想要的结果是 17----32
//加了一个表达式(number - 1) << 1)
//比如17<<1 = 17*2  = 34--highestOneBit-- 32 
//    15<<1 = 30--highestOneBit--16
//这样就能达到目的了

再回到初始化方法来
//1, 初始化方法
private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize  找出>=2的幂次方的数,  2,  4, 8, 16 ...
    //			比如传进来10 , 返回16, 16返回16
    int capacity = roundUpToPowerOf2(toSize);

    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];
    initHashSeedAsNeeded(capacity);
        //计算
    private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize
        //现在我们已经获取到了一个>= tosize 的 2的幂次方数
        int capacity = roundUpToPowerOf2(toSize);
		
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);	
        table = new Entry[capacity]; //已16作为数组的大小创建
        initHashSeedAsNeeded(capacity);
    }

}

2, key==null调用putForNullKey

private V putForNullKey(V value) {
    //key = null ,相同的话覆盖
    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);// 相当于key为null 放到数组的下标0的位置
    return null;
}

3,计算下标

//a , indexfor
static int indexFor(int h, int length) {
    return h & (length-1);
}
h:   0101 0101
15:  0000 1111
&    0000 0101   取值范围是  0000-1111  0-15
为什么数组的最大值要是2的幂次方数

好处,算下标的时候 取& 比取% 位运算效率高一点

4,通过key计算hash值

final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }
    h ^= k.hashCode();
    //此函数确保在每个位位置仅相差恒定倍数的 hashCode 具有有限数量的冲突(在默认负载因子下约为 8)。
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}
h:   0101 0101
15:  0000 1111
&    0000 0101   取值范围是  0000-1111  0-15
//由上面看出, 高位的不影响结果, 所以 需要进行右移, 异或, 让高位参与进算法中来
//为了让链表不那么长
//为了让hashcode更散列一点

5,添加元素addEntry(hash, key, value, i);

//添加元素
void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断数组大小是否大于阈值 , 扩容    threshold = (table.length)16 * 0.75
    //null != table[bucketIndex 表示 数组当前索引的值不为空
    if ((size >= threshold) && (null != table[bucketIndex])) {
        resize(2 * table.length); //6扩容方法-翻倍----------------------------方法在下面6-----------------------
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}	
	//创建entry放入数组
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //  头插法                                     e这里是next
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++; //添加之后大小++
    }

6,扩容resize

void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
	
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));//7转移元素-----------------------详细方法在下面7--------------------------
    table = newTable;//再把原来的数组重新赋值成扩容后的数组
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);//再重新计算阈值
}
可以看到上面第3步的计算下标的公式
//a , indexfor
static int indexFor(int h, int length) {
    return h & (length-1);
}
h:   0101 0101
15:  0000 1111
&    0000 0101   取值范围是  0000-1111  0-15
通过下面位运算可以看出,扩容后存放新数组的下标会出现两种情况(不rehash情况)
  1. 和老数组的下标一致, index
  2. 老数组下标+原数组长度length(默认16)= old-index + old.length

image-20220320125718999

7,扩容后转移元素(transfer)

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) { //1.8没有这个判断了---------------------详细方法在下面8-----------------------------
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];//把新元素插到链表头部
            newTable[i] = e; //再把这个元素作为新头部, 实现下移
            e = next;//上面完成了1个元素转移, 指向下一个继续循环转移
        }
    }
}

1.7多线程扩容可能出现循环链表, 最终导致put/set 进入死循环(CPU100%)

两个线程同时进入转移方法时transfer

都会进入循环遍历老数组的元素

image-20220320131719694

发现第一个元素已经把老数组的元素转移到了新数组里, 而且链表顺序反了过来

image-20220320131600571

第二个线程的变量, 就会指向第一个线程转移后的地址, 接着指向后面的代码

if (rehash) { //1.8没有这个判断了
    e.hash = null == e.key ? 0 : hash(e.key);
}
//转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];//把新元素插到链表头部
newTable[i] = e; //再把这个元素作为新头部, 实现下移
e = next;//上面完成了1个元素转移, 指向下一个继续循环转移

第二次循环后

image-20220320132247364

e2 , 和 next2 重新指向

image-20220320132432492

走完之后

image-20220320135317870

出现了循环链表

第二个线程再把数组赋值到这个新数组table = newTable;

下次再指向put/get时, 进入循环链表的话就会死循环

原因:

  • 第二个线程的e2 和next2 位置翻了过来, 主要是因为使用的头插法导致了链表顺序反了过来

怎么避免扩容出现的问题

  • 1防止他进行扩容, 控制他的阈值, threshold,确定数据大小, 设置好阈值
  • 2多线程操作时做一些操作, 如加锁

8, rehash(转移元素transfer里调用)

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) { //1.8没有这个判断了
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //转移后有两种情况, 一种是原来的下标, 一种是原来的下标+扩容大小
            int i = indexFor(e.hash, newCapacity);
            e.next = newTable[i];//把新元素插到链表头部
            newTable[i] = e; //再把这个元素作为新头部, 实现下移
            e = next;//上面完成了1个元素转移, 指向下一个继续循环转移
        }
    }
}
//是由调用transfer 传进来的
transfer(newTable, initHashSeedAsNeeded(newCapacity));

    final boolean initHashSeedAsNeeded(int capacity) {
        boolean currentAltHashing = hashSeed != 0;
        boolean useAltHashing = sun.misc.VM.isBooted() &&
                (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean switching = currentAltHashing ^ useAltHashing;
        if (switching) {
            hashSeed = useAltHashing
                ? sun.misc.Hashing.randomHashSeed(this)
                : 0;
        }
        return switching;
    }
//是有jdk.map.althashing.threshold这个属性决定的
    private static class Holder {
        static final int ALTERNATIVE_HASHING_THRESHOLD;

        static {
            String altThreshold = java.security.AccessController.doPrivileged(
                new sun.security.action.GetPropertyAction(
                    "jdk.map.althashing.threshold"));
            

  • 如果数组容量 capacity >= jdk.map.althashing.threshold , hash种子就会被赋值
  • jdk.map.althashing.threshold作用, 就是可以觉得hash算法不够, 可以加一个系统变量来让hash算法更加复杂
  • 添加vm参数-Djdk.map.althashing.threshold=3

image-20220320141039263

生成hash种子

image-20220320132551269

9,modCount++; 代表修改次数

image-20220320135427942

删除key =2 时会出现异常

反编译后, 发现是使用迭代器

image-20220320141147787

父类迭代器,

image-20220320141309229

image-20220320141841422

第二次循环modcount 已经++ = 3, 而expectedModCount还是2

image-20220320142001345

再次判断不相等之后就会抛出异常

怎么解决这个问题呢

使用迭代器的remove方法

image-20220320142113104

迭代器的remove方法会重新给modecount进行赋值, 就不会出现异常了

image-20220320142220456

为什么要使用modecount来抛出异常呢
  • 快速失败, 发现错误让程序快速停止
  • 比如两个线程同时操作时, 一个线程正在读取, 一个线程在修改, 就会出现问题 ,所以就会抛出异常

为什么用头插法

  • 都需要遍历链表,所以头插尾插都差不多

数组和链表插入效率

image-20220320123538701

  • 实际情况数组和链表的插入效率不一定谁快谁慢
  • 还是要根据实际情况
  • 数组插入时会把元素往后移动, 而链表需要定位到指定位置 再执行插入, 数组也需要定位, 但是由于自身特性有索引, 比较快

get方法

    public V get(Object key) {
        if (key == null)
            return getForNullKey();
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }

getEntry

    final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
		//通过hash算法计算出hash值
        int hash = (key == null) ? 0 : hash(key);
        //再通过hash值计算出数组下标, 再进行链表遍历比较key的值
        for (Entry<K,V> e = table[indexFor(hash, table.length)];
             e != null;
             e = e.next) {
            Object k;
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;// 找到就返回
        }
        return null;
    }

HashMap的get方法是怎么获取到数据的

get(key);

  • 1, 通过key.hash获取到hashcode , 在进行一系列操作右移异或 &等操作获得数组下标
  • 2, 比对数组下标的key是否相等, 不相等在比较是否有下一个元素(next) 找到为止
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值