1、首先我们查看hashMap底层是由什么组成的
先写一个代码了进入源码
HashMap
map.put(“James”, “James is handsome”);
进入到put中可以查看到其源码:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key);//计算hash值
int i = indexFor(hash, table.length);//计算下标
for (Entry e = table[i]; e != null; e = e.next) {//遍历下表为1的链表拿到下表就是拿到一个链表
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {//遍历循环判断这个链表是否有相同的key有旧值就替换
V oldValue = e.value;
e.value = value;//新值覆盖旧值
e.recordAccess(this);
return oldValue;//返回值
}
}
modCount++;
addEntry(hash, key, value, i);//没有就进行进入方法操作
return null;
}
/**
* The table, resized as necessary. Length MUST Always be a power of two.
*/
transient Entry[] table;
从上面的代码中我们可以看到hashmap首先有个table值然后在table是一个一维数组。以
Hash定义:
一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射, pre-image),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
详细可参考:https://blog.csdn.net/Beyond_2016/article/details/81286360
table的初始化固定长度我们可以通过debug更快的找到其值16如图所示在这里要提示一下debug先要在put上设置debug在到里面。不然可能不是你要的因为hashmap里面也有是实现put方法
从上面可以看出hashmap其实是一个一维数组和数组的每个值嵌套一个链表,至于每个值嵌套一个链表我们可以进入get方法源码里面有一个getEntry方法:
final Entry getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry 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))))//如果key相同就返回entry,否则next就继续查找
return e;
}
return null;
}
从上面的代码我们变量数组每个元素下都有一个e.next链接起来。现在我们知道了hashMap是由一维数组和每个元素下挂着一个链表其数组初始化长度是16。
2、hashMap是怎么存取数据的
从上面我们已经知道其put进去的时候会发现table的key有hash求余table长度得到,在其存入对应的链表表中其hash(key)%length是数据分片算法。但是数组长度固定时对应存入多个值时候会发现每个链表会变得很长。
我们通过源码get的方法:
final Entry getEntry(Object key) {
int hash = (key == null) ? 0 : hash(key);
for (Entry 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))))//如果key相同就返回entry,否则next就继续查找
return e;
}
return null;
}`
知道了其没有什么查找算法就是顺序擦或者,没有算法这样的性能是无法接受的,这时jdk是怎样解决的呢?
在put的方法的源码中我们可以发现
在链表中没有相同的key的时候就会去执行
addEntry(hash, key, value, i);
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {//当size值大于threshold阈值时候就会进行扩容数组其中
resize(2 * table.length);//扩容数组长度
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
/**
* The number of key-value mappings contained in this map.此映射中包含的键-值映射的数目。
*/
transient int size;
从上面的代码知道当其知道当其存入数据Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12大于threshold阈值就去扩容一维数组当null != table[i],当然其阈值用debug可以知道是12也可以自己代码中找只是比较麻烦。从上面代码知道了扩容但下面介绍扩容后怎么数据,在上面的代码中我们可以知道有一个resize(int newCapacity)方法里面有个transfer方法重置hashmap的数据
/**
* Transfers all entries from current table to newTable.
*/
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);
}
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
可以知道其实遍历旧的table数据插入到新的table和链表中。这样我们大概知道了其存储数据的过程,但我们发现当多个数据存入时候,在扩容的时候其性能很差, map有一个代码通过构造方法初始数组大小
* Constructs an empty HashMap with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
这里我们可以通过数据量,计算initialCapacity避免扩容。
jdk1.8其源代码跟jdk1.7差不多,只是多个红黑树结构,我们从上面知道虽然通过扩容和初始化值可以解决链表查询性能问题,但还是没彻底解决链表长度的问题,jdk1.8新增的红黑树就是为了解决其问题。下面我们看一下其put源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//若table为null
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//计算下标i取i处的元素为p
tab[i] = newNode(hash, key, value, null); //创建新的node,放到数组中
else {
Node e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//若key相同直接覆盖
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//如果为树节点
else { //如果key不相同也不为树节点
for (int binCount = 0; ; ++binCount) {//遍历i处的链表
if ((e = p.next) == null) {//找到尾部
p.next = newNode(hash, key, value, null);//从末尾添加一个node
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1s如果链表长度大于阈值8
treeifyBin(tab, hash);//将链表转成红黑树
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
从上面源码解析知道其余的基本差不多,只是在当链表长度大于8之后转成红黑树,至于为什么在8才转呢?其实
链表的特点:结构简单,插入起来相对损耗比较低。
树的数据:结构复杂, 查损耗相对损耗比较低。
其是在hashMap的基础上改造的采用了数组+Segment+分段锁的方式实现我们查看其put的源码
@SuppressWarnings("unchecked")
public V put(K key, V value) {
Segment s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);//计算Hash值
int j = (hash >>> segmentShift) & //计算下标jsegmentMask;
if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);//若j处有segment就返回,若没有就创建并返回
return s.put(key, hash, value, false);//将值Put到segment中去
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry first = entryAt(tab, index);
for (HashEntry e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
final Segment[] segments;
从上面可以知道多了一层segment且是一维数组,而第二层是一个 HashEntry也就是由一个一维数组加上hashtable组成。可以看到currentHashMap是一个数组通过一个数据分片放入多个hashtable,分别加锁并发度就提升起来了,segments的长度就是并发度但segments数组不能扩容。
JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作中彻底放弃了Segment转而采用的是Node(Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。)如下源码:
如上图可知其当数组其没值的时候就用cas操作添加node值,有值的话就用synchronizd锁住表头,也就是锁住对象(这个是java的约定)
jdk1.7和jdk1.8的区别:每一个分段都会很多的数据,锁的粒度虽然比hashtable要好,仍然还是粒度很多,并发度是一定的,(hashtable是一个比较中的数据,所以你并不能为了追求并发增加更多的Hashtable,一个HashTbale也就是一个分组,里面存储的数据比较多,一个分段锁是一个比较大力度的锁)
jdk1.8锁中是链表,链表的粒度非常小,并发度相对来说就大很多,而且不固定,table扩容,链表增多,并发度就更高。