jdk1.7中hashmap源码分析

本文详细分析了JDK1.7中HashMap的源码,包括常量定义、构造方法、put操作、扩容机制、hash计算以及transfer方法。特别指出在多线程环境下,transfer方法可能导致链表循环,从而引发性能问题。HashMap使用头插法在扩容时可能造成链表顺序反转,导致并发问题。
摘要由CSDN通过智能技术生成

jdk1.7中hashmap源码分析

常量定义:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 默认初始长度


static final int MAXIMUM_CAPACITY = 1 << 30; //最大值

static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认加载因子


static final Entry<?,?>[] EMPTY_TABLE = {};


transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;//数组

transient int size;//当前hashmap的大小

int threshold;//扩容阈值


final float loadFactor;//扩容因子


transient int modCount;//代表对hashmap的操作次数  提供一种fast-fail快速失败机制

构造方法:

public HashMap() {
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

put

public V put(K key, V value) {//PUT时,如果key相同,会进行覆盖操作,返回的是被覆盖的value
    //第一次进来就是空的,进行初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    if (key == null)
        //从这一点可以看出hashmap是支持key为null的,如果不支持,这里可能会直接抛出一个异常
        return putForNullKey(value);
    int hash = hash(key);//计算key的hash值
    int i = indexFor(hash, table.length);//用hash值和数组长度算出一个数组下标

    //遍历第i个位置上的链表
    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);//hashmap中没用到这个方法
            return oldValue;
        }
    }

    modCount++;
    //添加
    addEntry(hash, key, value, i);
    return null;
}

inflateTable

private void inflateTable(int toSize) {
    // Find a power of 2 >= toSize
    //对toSize做了处理后才新建Entry
    //找到一个>=toSize的最近的2的幂次方数
    int capacity = roundUpToPowerOf2(toSize);
    //
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    table = new Entry[capacity];//初始化
    initHashSeedAsNeeded(capacity);
}
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    //如何取>=number的最小的2次幂呢
    //规律:2的幂运算的二进制数,都只含一个1,例如0000 0001
    //Integer.highestOneBit这里返回的是<=number的最近一个2的幂运算,原理是通过>>和|运算,一步一步把二进制最高位为1的后面的数,都替换为0
    //例如0010 1100 替换结果就是0010 0000 如果在以后代码中遇到类似场景,可以直接用这个方法
    //计算最小2的幂之后,左移一位
    int rounded = number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (rounded = Integer.highestOneBit(number)) != 0
                ? (Integer.bitCount(number) > 1) ? rounded << 1 : rounded
                : 1;

    return rounded;
}

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();//异或 相同为1 不相同为0

    // 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);
}

indexFor

static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    //&操作 都为1 则为1  这里指两个数的二进制位对比
    //例如
    //h:0101 0101 length-1:0000 1111
    //&之后的结果就是: 0000 0101
    //位运算效率高
    return h & (length-1);
}

addEntry

void addEntry(int hash, K key, V value, int bucketIndex) {
    //扩容
    //threshold=数组长度*0.75
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //2倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }

    createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
    //将当前数组元素取出来
    Entry<K,V> e = table[bucketIndex];
    //新new一个entry,把当前数组作为新数组的next
    //再把新entry赋值给当前索引位置,这就是jdk1.7的头插法
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
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));
    table = newTable;
    //赋值新的扩容阈值
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {//rehash先默认为false,大多数情况他都是false,jdk8当中都没有这个rehash了
    int newCapacity = newTable.length;
    //双重循环,把老数组的内容分配到新数组中
    for (Entry<K,V> e : table) {
        while(null != e) {
            //如果是多线程环境,线程A线程B,A和B会各自创建一个新数组
            //A会正常执行如下代码 B走到这一步就会卡住,假设指针指向第一个元素1
            //A循环后所有的数据都转移到了新数组中,元素由1,2,3变为3,2,1  这个时候线程B的next指向的是链表最后端的1
            //此时线程B继续执行
            //B执行完之后,会覆盖A循环出来的新数组,此时B循环出来的数组是有大问题的,
            // 后果是后续在put元素时,如果恰巧循环到了这个位置的链表,就会出现无限循环的问题

            //归根结底还是因为头插法把链表顺序搞反了,导致第二个线程循环分配链表元素时,乱套了,形成一个无限循环的链表,这是bug
            Entry<K,V> next = e.next;
            if (rehash) {
                //rehash大多数情况都是false,因为很少有人去配置这个启动参数,不配置的话,默认要比整数最大值大才有可能为true
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //分配到新数组中的位置规律:和就数组一样或者就数组的索引位置加上新数组的长度
            int i = indexFor(e.hash, newCapacity);
            //此处仍然是用头插法将链表的数据移动到新数组中
            //存在问题:例如原链表中数据从上到下是1,2,3
            //因为遍历是按1,2,3的顺序遍历,那么到了新链表中,由于头插法特性,链表数据正好相反了,变为3,2,1
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
final boolean initHashSeedAsNeeded(int capacity) {
    boolean currentAltHashing = hashSeed != 0;//哈希种子为0的情况下,currentAltHashing为false
    boolean useAltHashing = sun.misc.VM.isBooted() &&
            (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);//这里提供一个可配置的参数
    boolean switching = currentAltHashing ^ useAltHashing;//异或,不同则为true
    if (switching) {
        //更新哈希种子
        hashSeed = useAltHashing
            ? sun.misc.Hashing.randomHashSeed(this)
            : 0;
    }
    return switching;
}

多线程情况下transfer:
线程A正常执行,线程B会卡住,线程A运行前线程B的指向效果是:
在这里插入图片描述

线程A正常执行完,分配旧元素到新数组中了,此时线程B的指向情况为:
在这里插入图片描述
线程B执行第一次:
在这里插入图片描述
线程B执行第二次:
在这里插入图片描述
执行到e.next为null时,不再继续执行,此时会形成一个循环链表:
新数组会再赋值给table,问题就大了.
形成了一个无限循环的链表.
在这里插入图片描述
源码看起来会乱乱的,第一次看建议下载jdk源码对照注释进行逻辑分析.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值