Jdk1.7下的HashMap

一. 概述
首先我们都知道java1.7中hashmap使用的数据结构是数组加链表;hashmap也不是线程安全的;它支持key和value都是null,但是这样的值只允许有一个。
hashMap的底层是用了一个Entry的数组,然后数组中的一个每一个节点对应着一个链表。

二. 源码解析

  1. 源码中重要变量
//默认初始化容量,1向左移4位,就是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 
//最大容量;也就是2的30次方,这是因为int类型可以表示的最大数是2的32次方
static final int MAXIMUM_CAPACITY = 1 << 30;
//threshold的最大值
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
//默认加载因子0.75,
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//table用来存储Entry数组
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;  
 //已经使用的数组的位置个数,用来判断是否需要扩容
transient int size;
//阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了
//也就是为table分配内存空间后,threshold一般为 capacity*loadFactory。
//HashMap在进行扩容时需要参考threshold
int threshold;
//负载因子,表示table的填充度,默认0.75
final float loadFactor;

  1. put()方法
public V put(K key, V value) {
 		
        if (table == EMPTY_TABLE) {
       		 // 真正的初始化方法
        	// 代码比较简单 
        	// threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1); 
        	// threshold 初始时为12
            // table = new Entry[capacity];
            //  最后单独说initHashSeedAsNeeded(capacity);
            inflateTable(threshold); // 这里传的是初始时 的 16 
        }
        // 为null值 指定位置
        // table[0]的链表  并返回旧值或null
        if (key == null)
            return putForNullKey(value);
            // 计算hash值
        int hash = hash(key);
        // 为利用hash找到 key应该放到table的某个数组的链下
        int i = indexFor(hash, table.length);
        // 遍历 table[i]链表 找到与当前key相同的替换并返回旧值
        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);
                return oldValue;
            }
        }
		// 若table[i]下没有相同的key 则 modCount+1,并执行addEntry将key,value放到map中
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

执行addEntry方法将key,value存入map中。
此方法先计算是否需要扩容后真正放值。

void addEntry(int hash, K key, V value, int bucketIndex) {
 		// add之前会判断当前map中的数量是否大于等于阈值
 		// 扩容条件 当前size超过阈值 并且 table[bucketIndex] 不为空
        if ((size >= threshold) && (null != table[bucketIndex])) {
        	// 扩容原先的两倍 
            resize(2 * table.length);
            // 重新计算hash值
            hash = (null != key) ? hash(key) : 0;
            // 计算当前key所属的 table数组位置
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

jdk中数据的插入是先通过计算得到key的hashcode值,然后通过hashcode值与当前的桶值进行与运算(桶值就是现在的数组长度,然后与运算是这个hashcode的值与这个数组长度的值-1进行与运算,),然后插入到对应的桶中,一个桶对应的是一个链表,然后插入链表的时候是采用头节点插入,这样插入的速度更快。然后在桶中存放的是对应的链表的引用,然后在你插入之后将新的头节点的引用给桶就可以了
在put()数据的时候key是可以相同的,但是它是会覆盖key相同节点的value,并且返回
因为你在插入的时候都需要去遍历查看是否有相同的key,那为什么插入的时候要用头插法而不用尾插法?
因为你在使用尾插法的时候每次都要遍历到最后,但是使用头插法的时候不用每次遍历到最后。
3. get()方法

 // 获取key对应的value 
 public V get(Object key) {
        if (key == null)
            //如果key为null,调用getForNullKey()
            return getForNullKey();
        //key不为null,调用getEntry(key);
        Entry<K,V> entry = getEntry(key);
        return null == entry ? null : entry.getValue();
}
 
 //当key为null时,获取value
 private V getForNullKey() {
        if (size == 0) {
            return null;//链表为空,返回null
        }
        //链表不为空,将“key为null”的元素存储在table[0]位置,但不一定是该链表的第一个位置!
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null)
                return e.value;
        }
        return null;
 }
 
//key不为null,获取value
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {//判断链表中是否有值
         //链表中没值,也就是没有value
            return null;
        }
       //链表中有值,获取key的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;
            //判断key是否相同
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;//key相等,返回相应的value
             }
        return null;//链表中没有相应的key
}

  1. resize()方法,扩容
    扩容方法是创建一个新的Entry数组(容量为原先的两倍)
    并将原先数组中的值转移到新数组中
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        // 如果当前table容量已经等于最大容量值则将阈值设定为整数最大值并返回
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;//2<sup>31</sup>-1
            return;
        }
		
        Entry[] newTable = new Entry[newCapacity];
        // 转移值 initHashSeedAsNeeded用于返回是否需要重新计算hash
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        // 转移值之后 更改table的引用 
        table = newTable;
        // 更新阈值
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

转移数组方法 transfer()方法
遍历table 并 循环遍历每个链表 为链表中每个元素计算隶属与数组哪个位置然后转移

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) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

扩容:扩容的时候是生成一个新的数组,然后通过遍历数组,在每个数组节点中对应的链表中取每一个节点元素的hash值,然后通过这个新的hash值和数组长度进行与运算,来算出它的数组下标。
在扩容的时候也是用的头插法
扩容的目的:其实扩容是为了将每个数组节点下面的链表变短,然后这样可以提高查询的效率。
它是线程不安全的,在多个线程使用同一个hashmap的时候可能会造成一个循环链表。

三. 为什么hashMap线程不安全
1. 扩容造成死循环
造成死循环的主要源头就是transfer()方法中,因为它使用的是头插法,所以说链表的顺序是会翻转的,这也是造成死循环的关键。

int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }

假如这个时候有两个线程对同一个hashmap进行操作,然后线程A先开始执行,执行到newTable[i] =
e这里的时候停止。然后线程B执行,线程B全部执行完毕,然后执行的结果是正确的一个过程;这个时候线程A开始执行,因为这个时候线程B执行完毕,所以newTable和table中的Entry都是主存中的最新值,所以此时线程A都是用的是内存中的值。
此时如果有一个 next是执行线程A中的e,然后此时它在继续循环,到最后就会出现死循环,因为这个时候会出现e.next = e,next.next = e的情况,并且e =null。

  1. 扩容造成数据丢失
    在执行put()操作的时候,这个也是同时有两个线程;
    线程A先执行,在执行到newTable[i] = e的时候挂起,同时这个时候线程B获得时间片,然后完成resize()操作,同时支持完毕,这个时候newtable和table都是最新的值,再切换到线程A的时候,next指向的就会为null,执行 newtable[i] = e操作的时候就将本来是另一个元素存放的位置改成了另一个元素,再循环一次就会导致有元素丢失,并且线程循环链表,并造成死循环。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值