JDK1.8,首先他是数组+链表+红黑树的结构,默认长度是16,负载因子是0.75,扩容为原来的两倍(即将达到负载因子时),采用的是尾插法,可以有一个空键和多个空值。
在HashMap刚创建时,不会初始化,当第一次调用put()方法时,会调用putVal()方法,来执行resize()方法,对数组进行初始化扩容,调用put()方法时,首先将K,V值存入Node节点,然后调用HashCode()方法计算hash值,然后判断是否数组hash值是否为空,若为空直接放入;若不为空,key值相等直接替换value;若为红黑树,直接放入节点;若都不是,先遍历链表,查看是否出现key值相同的情况,直接覆盖;若没有则加入链表尾。当链表长度>=8并且数组长度>=64时,调用treeifyBin()方法来实现链表转换为红黑树。最后modCount++,最后判断是否需要进行扩容。
1.1线程安全的Map集合:
1.HashTable线程安全的,每个方法都加上synchronized(全表锁),底层结构是:数组+链表,不允许存储空key,空值。
2.Collections.synchronizedMap()方法
3.ConcurrentHashMap底层原理
在JDK1.7时,使用Segments数组+HashEntry数组+链表,采用分段锁保证安全性。
//一个ConcurrentHashMap中有一个Segments数组,一个Segments中存储一个HashEntry数组,每个HashEntry是一个链表结构的元素。 //segment继承自ReentrantLock锁。 首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一段数据时,其他段的数据也能被其他线程访问,实现了真正的并发访问。 //可以通过构造函数指定,数组扩容不会影响其他的segment,get无需加锁,volatile保证内存可见性
JDK1.8之后,结构为:Synchronized + CAS +Node +红黑树.Node的val和next都用volatile保证,保证可见性,查找,替换,赋值操作都使用CAS CAS是乐观锁--->如果出现高并发情况就会使用synchronized 同步代码块(JDK1.6之后有锁升级的过程),大大提高了低并发下的效率。
锁 : 锁是锁的链表的head的节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作(因为扩容的时候使用的是Synchronized锁,锁全表),并发扩容。
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
为什么JDK1.8之后的ConcurrentHashMap效率会更高?
11.Segment可重入锁锁住的是一个HashEntry数组,synchronized锁住的只是发生hash冲突的链表的头节点或红黑树的节点,提高了并发性能。
2.从JDK1.6开始,对 synchronized 锁的实现引入了大量的优化,并且 synchronized 有多种锁状态,会从偏向锁 -> 轻量级锁 -> 重量级锁一步步转换。
只要并发的线程可以在一定次数的自旋内拿到锁(偏向锁不用自旋),那么synchronized就不会升级为重量级锁,等待的线程也不会被挂起,减少了线程挂起和唤醒的切换的过程开销。
而ReentrantLock不会自旋,会直接挂起,这样一来就很容易会多出线程上下文开销的代价。
3.减少内存开销 。假设使用可重入锁来获得同步支持,那么每个节点都需要通过继承 AQS 来获得同步支持。但并不是每个节点都需要获得同步支持的,只有链表的头节点(红黑树的根节点)需要同步,