一、数据结构
二 hash算法
hash算法,不是简单的对key值进hash运算,而是是下面这个方法,是先将key进行hashcode后的一个hash值与数组长度-1后进行与运算得到的值,为啥这样操作:
进行与使不同的hash值发生的碰撞的概率比较小。这样会使得数据在table数组中分布较均匀,查询速度也快
我们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
源码分析:
/**
public V put(K key, V value) {
//判断当前Hashmap(底层是Entry数组)是否存值(是否为空数组)
if (table == EMPTY_TABLE) {
inflateTable(threshold);//如果为空,则初始化
}
//判断key是否为空
if (key == null)
return putForNullKey(value);//hashmap允许key为空
//计算当前key的哈希值
int hash = hash(key);
//通过哈希值和当前数据长度,算出当前key值对应在数组中的存放位置
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
//如果计算的哈希位置有值(及hash冲突),且key值一样,则覆盖原值value,并返回原值value
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
//存放值的具体方法
addEntry(hash, key, value, i);
return null;
}
具体put是否扩容需要两个条件
1、 存放新值的时候当前已有元素的个数必须大于等于阈值
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
扩容方法是在addEntry方法中
void addEntry(int hash, K key, V value, int bucketIndex) {
//1、判断当前个数是否大于等于阈值
//2、当前存放是否发生哈希碰撞
//如果上面两个条件否发生,那么就扩容
if ((size >= threshold) && (null != table[bucketIndex])) {
//扩容,并且把原来数组中的元素重新放到新数组中
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
扩容过程中会将原来的数据,放入到新的数组中,但是会重新计算hash值进行分配
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()方法把原数组中的值放到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//设置hashmap扩容后为新的数组引用
table = newTable;
//设置hashmap扩容新的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
transfer()在实际扩容时候把原来数组中的元素放入新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry e : table) {
while(null != e) {
Entry next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过key值的hash值和新数组的大小算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
扩容问题:
数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这个操作是极其消耗性能的。所以如果我们已经预知HashMap中元素的个数,那么预设初始容量能够有效的提高HashMap的性能。
重新调整HashMap大小,当多线程的情况下可能产生条件竞争。因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
四 线程安全
HashMap是线程不安全的,在多线程情况下直接使用HashMap会出现一些莫名其妙不可预知的问题。在多线程下使用HashMap,有几种方案:
A.在外部包装HashMap,实现同步机制
B.使用Map m = Collections.synchronizedMap(new HashMap(…));实现同步(官方参考方案,但不建议使用,使用迭代器遍历的时候修改映射结构容易出错)
D.使用java.util.HashTable,效率最低(几乎被淘汰了)
E.使用java.util.concurrent.ConcurrentHashMap,相对安全,效率高(建议使用)
注意一个小问题,HashMap所有集合类视图所返回迭代器都是快速失败的(fail-fast),在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器自身的 remove 或 add 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。。因此,面对并发的修改,迭代器很快就会完全失败。
一数据结构
2扩容-
主要是hash值的计算以及树的转换,1.8相对1.7进行了大量优化,性能提高很多
源码
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
* @return the table
*
*
*
* 初始化或者翻倍表大小。
* 如果表为null,则根据存放在threshold变量中的初始化capacity的值来分配table内存
* (这个注释说的很清楚,在实例化HashMap时,capacity其实是存放在了成员变量threshold中,
* 注意,HashMap中没有capacity这个成员变量)
* 。如果表不为null,由于我们使用2的幂来扩容,
* 则每个bin元素要么还是在原来的bucket中,要么在2的幂中
*
* 此方法功能:初始化或扩容
*/
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
//新的容量值,新的扩容阀界值
int newCap, newThr = 0;
//oldTab!=null,则oldCap>0
if (oldCap > 0) {
//如果此时oldCap>=MAXIMUM_CAPACITY(1 << 30),表示已经到了最大容量,这时还要往map中放数据,则阈值设置为整数的最大值 Integer.MAX_VALUE,直接返回这个oldTab的内存地址。
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果(当前容量*2<最大容量&&当前容量>=默认初始化容量(16))
//并将将原容量值<<1(相当于*2)赋值给 newCap
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//如果能进来证明此map是扩容而不是初始化
//操作:将原扩容阀界值<<1(相当于*2)赋值给 newThr
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//进入此if证明创建map时用的带参构造:public HashMap(int initialCapacity)或 public HashMap(int initialCapacity, float loadFactor)
//注:带参的构造中initialCapacity(初始容量值)不管是输入几都会通过 “this.threshold = tableSizeFor(initialCapacity);”此方法计算出接近initialCapacity参数的2^n来作为初始化容量(初始化容量==oldThr)
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//进入此if证明创建map时用的无参构造:
//然后将参数newCap(新的容量)、newThr(新的扩容阀界值)进行初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//进入此if有两种可能
// 第一种:进入此“if (oldCap > 0)”中且不满足该if中的两个if
// 第二种:进入这个“else if (oldThr > 0)”
//分析:进入此if证明该map在创建时用的带参构造,如果是第一种情况就说明是进行扩容且oldCap(旧容量)小于16,如果是第二种说明是第一次put
float ft = (float)newCap * loadFactor;
//计算扩容阀界值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;
//如果“oldTab != null”说明是扩容,否则直接返回newTab
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果该元素是TreeNode的实例
((TreeNode)e).split(this, newTab, j, oldCap);
else { // preserve order
Node loHead = null, loTail = null;//此对象接收会放在原来位置
Node hiHead = null, hiTail = null;//此对象接收会放在“j + oldCap”(当前位置索引+原容量的值)
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
详情可以参考博客 https://www.cnblogs.com/shianliang/p/9204942.html
3 ConcurrentHashMap 1.7与1.8的锁区别,以及数据结构区别
JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树
1.7时候put
Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值,然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒
1.8时候put
1)如果没有初始化就先调用initTable()方法来进行初始化过程
2)如果没有hash冲突就直接CAS插入
3如果还在进行扩容操作就先进行扩容
4)如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
5)最后一个如果Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构,break再一次进入循环
6)如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容