如何存储数据 (put、get)算法
put数据数组
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); //计算key的hash值 int hash = hash(key); //根据hash和数组的长度计算索引,即数组存放的位置 int i = indexFor(hash, table.length); 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; } } modCount++; //将算出的key的hash值,key,value ,以及索引存放在entry对象中,并加入table数组中 addEntry(hash, key, value, i); return null; }
get数据多线程
public V get(Object key) { //若是key为null,则返回null对于的value if (key == null) return getForNullKey(); //根据key获取entry Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); } final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } //计算出key的hash值 int hash = (key == null) ? 0 : hash(key); //根据hash值获取索引,遍历索引下的全部的entry值 for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; //若是hash值相同而且key也相同则返回value if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
默认容量为何是16this
//1前移4位 二进制1是 01 ,前移4位 则是 10000,即16 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
咱们先看一下源码如何获取index的值线程
static int indexFor(int h, int length) { // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2"; //hash 值与 数组的长度-1进行位运算,长度必须是2的幂。 return h & (length-1); }
index的值是经过key的hash值与数组长度-1进行位运算来的。举个例子,code
计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001;对象
HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111blog
把以上两个结果作与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,因此 index=9
能够说,Hash算法最终获得的index结果,彻底取决于Key的Hashcode值的最后几位。索引
==长度16或者其余2的幂,Length-1的值是全部二进制位全为1,这种状况下,index的结果等同于HashCode后几位的值。只要输入的HashCode自己分布均匀,Hash算法的结果就是均匀的。==ci
负载因子为何是0.75f
/** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f;
加载因子须要在时间和空间成本上寻求一种折衷。
加载因子太高,例如为1,虽然减小了空间开销,提升了空间利用率,但同时也增长了查询时间成本
加载因子太低,例如0.5,虽然能够减小查询时间成本,可是空间利用率很低,同时提升了rehash操做的次数。
hash冲突的产生与解决
产生
例如咱们如今要加入一个key=A,value=a,先须要计算A的index,假如是2。那么结果以下:
图1
再加入一个key=A', value=a',此时恰好计算A'的index也是2,这时候就会产生冲突。
解决hash冲突
这个时候咱们须要使用链表来解决hash冲突,当产生相同的index是,则经过Next指向同一个index的下一个节点,如图:
(
图二
)
注意:新来的Entry节点插入链表时,使用的是“头插法”。至于为何不插入链表尾部,由于HashMap的做者发现后插入的Entry被查找的可能性更大
何时会出现死锁
当扩容时,须要将老的table的数据转移到新的table上,调用transfer发放进行转移。
咱们先看下transfer方法
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //循环遍历老的table for (Entry<K,V> e : table) { while(null != e) { Entry<K,V> next = e.next; //判断是否须要从新计算hash值 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; } } }
正常状况下,转移前链表顺序是1->2->3,逆序转移后新的t顺序变成 3->2->1,可是在多线程环境中,使用HashMap进行put操做时会引发死循环,致使死循环的缘由是在多线程的状况下,会造成环形链表,这时是没有问题的,咱们再看看get()方法中的获取Entry的方法
final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : 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; }
加入咱们执行get方法找一个不存在的值的时候,for循环将会死循环,即会造成死锁