HashTable
HashMap是线程不安全的,在多线程环境下,使用HashMap进行put操作时,可能会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。
于是有了HashTable,HashTable是线程安全的。但是HashTable线程安全的策略实在不怎么高明,将get/put等所 有相关操作都整成了synchronized的。
ConcurrentHashMap
由于直接加synchronized太过粗暴,基于分段式锁的ConcurrentHashMap出现
分段式
ConcurrentHashMap使用Segment(分段锁)技术,将数据分成一段一段的存储,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,
Segment数组中每一个元素一把锁,每一个Segment元素存储的是 HashEntry数组+链表,这个和HashMap的数据存储结构一样。
当访问其中一个段数据被某个线程加锁的时候,其他段的数据也能被其他线程访问,这就使得ConcurrentHashMap不仅保证了线程安全,而且提高了性能。
但是这也引来一个负面影响:ConcurrentHashMap 定位一个元素的过程需要进行两次Hash操作,
第一次 Hash 定位到 Segment,第二次 Hash 定位到元素所在的链表。所以 Hash 的过程比普通的 HashMap 要长。
jdk8后的新原理
源码分析
散列桶+cas+synchronized+红黑树(达到临界值后)
//存值
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode()); // 计算hash
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) {
// 否则 插入元素 看下面的casTabAt 方法
// cas 比较是否为null,如果null才会设置break,否则到else
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(类似分段锁)
synchronized (f) {
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;//当前插槽上的节点数量
//沿着Node链往后找
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果找到相同key,说明之前put过覆盖
if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {
oldVal = e.val;
//put方法上 onlyIfAbsent=flase 要不要覆盖
if (!onlyIfAbsent)
e.val = value;
break;
}
//否则,新key, 新node插入到最后
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
// 如果是红黑树,说明已经转化过,按树的规则放入Node
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;
}
//compareAndSwapObject 典型的CAS
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// 取值
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
//判断table是不是空的,当前桶上是不是空的 如果是空 返回null
if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
//找到对应hash槽的第一个node,如果key相等,返回value
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
//如果正在扩容,不影响,继续顺着node找即可
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
//其他情况,遍历 对比key 找到后返回value
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
总结:
put过程:
1.根据key的hash值定位到桶位置
2.如果table为空if,先初始化table。
3.如果table当前桶里没有node,cas添加元素。成功则跳出循环,失败则进入下一轮for循环。
4.判断是否有其他线程在扩容,有则帮忙扩容,扩容完成再添加元素。
5.如果桶的位置不为空,遍历该桶的链表或者红黑树,若key已存在,则覆盖,不存在则将key插入到链表或红黑树的尾部。
get过程:
1.根据key的hash值定位到桶位置。
2.map是否初始化,没有初始化则返回null
3.定位的桶是否有头结点,没有返回null
4.是否有其他线程在扩容,有的话调用find方法沿node指针往后查找。扩容与find可以并行,因为node的next指针不会变
5.若没有其他线程在扩容,则遍历桶对应的链表或者红黑树,使用equals方法进行比较。key相同则返回value,不存 在则返回null
并发容器
-
ConcurrentHashMap
对应:HashMap package
目标:代替Hashtable、synchronizedMap,使用最多
原理:前面详细介绍过 -
CopyOnWriteArrayList
对应:ArrayList
目标:代替Vector、synchronizedList
原理:高并发往往是读多写少的特性,读操作不加锁,而对写操作加Lock独享锁,先复制一份新的集合,在新的集合上面修改,然后将新集合赋值给旧的引用,并通过volatile 保证其可见性。
查看源码:volatile array,lock加锁,数组复制 -
CopyOnWriteArraySet
对应:HashSet
目标:代替synchronizedSet
原理:与CopyOnWriteArrayList实现原理类似。 -
ConcurrentSkipListMap
对应:TreeMap
目标:代替synchronizedSortedMap(TreeMap)
原理:基于Skip list(跳表)来代替平衡树,按照分层key上下链接指针来实现。 -
ConcurrentSkipListSet
对应:TreeSet
目标:代替synchronizedSortedSet(TreeSet)
原理:内部基于ConcurrentSkipListMap实现,原理一致 -
ConcurrentLinkedQueue
对应:LinkedList 、无界线程安全队列
原理:通过队首队尾指针,以及Node类元素的next实现FIFO队列 -
BlockingQueue
对应:Queue
特点:拓展了Queue,增加了可阻塞的插入和获取等操作
原理:通过ReentrantLock实现线程安全,通过Condition实现阻塞和唤醒
实现类:
LinkedBlockingQueue:基于链表实现的可阻塞的FIFO队列
ArrayBlockingQueue:基于数组实现的可阻塞的FIFO队列
PriorityBlockingQueue:按优先级排序的队列