第10章 常用的并发集合
2.1 ConcurrentHashMap
2.1.1 ConcurrentHashMap介绍
为什么要学ConcurrentHashMap?
HashMap是线程不安全的。
也有一些线程安全的映射表,如下:
早期java版本中有HashTable。
由Collections包装的集合类,比如:Collections.SynchronizedMap,在Collections.SynchronizedMap类中对Map的方法进行包装,方法内部使用synchronized修饰,经过包装实现了map的线程安全。
ConcurrentHashMap是JUC包中提供的线程安全的映射表,它的性能相比其它映射表更高。
2.1.2 ConcurrentHashMap源码解析
ConcurrentHashMap比HashTable在性能上有很大的提升,HashTable是对全表进行锁定,ConcurrentHashMap则是局部锁定。ConcurrentHashMap在jdk1.7、1.8上设计不一样,1.7在数组+链表的基础上采用分段锁技术保证线程安全,1.8在数组+链表+红黑树的基础上采用CAS+synchronized保证线程安全。
ConcurrentHashMap实现Map接口,针对哈希表的一些常用方法这里不再测试,下边以jdk1.8为准通过阅读源码理解下它的运行原理:
1、put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
//onlyIfAbsent: false每次用新值覆盖旧值,true:已存在相同key不作任何操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算机哈希值并保证正数
//(h ^ (h >>> 16)) & HASH_BITS (h ^ (h >>> 16))是hashMap中计算哈希的方法,& HASH_BITS的作用是将负数转正数 HASH_BITS是31位1
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)
//使用cas初始化table
tab = initTable();
//桶的位置为空则用cas方式创建头结点,i是桶的位置
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;
//根据key查找,如果已经存在
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;
//查找key是否在红黑树中,如果没有则添加
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;
}initTable()
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//sizeCtl用于控制初始化和扩容
//如果sizeCtl小于0说明有线程执行CAS成功正在初始化,此时当前线程让出CPU
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
//默认大小16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}tabAt:
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
//直接获取指定内存数据,保证每次拿到的就是最新数据
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}2、get方法
get方法全程没有加锁,性能很高,源代码如下:
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
//计算机哈希值并保证正数
int h = spread(key.hashCode());
//如果表的长度大于0开始定位桶
if ((tab = table) != null && (n = tab.length) > 0 &&
//(n - 1) & h)桶的下标
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {//如果桶位置上的结点的哈希与查找的key的哈希相同则开始比较key是否相等,如果相等则返回key对应 的value
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)//如果eh为负数则表示链表在扩容中或为红黑树,调用find查找key
return (p = e.find(h, key)) != null ? p.val : null;
//以上两种情况都不符合则开始遍历每个结点
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}2.1.3 ConcurrentHashMap案例
统计字符串每个字符出现的次数。
1、首先准备等待统计的数据,将0到9的10个数字循环100遍放入集合。
2、首先用HashMap实现,创建一个HashMap对象,遍历集合中等待统计的字符,如果字符在map中存在则计数加1,否则初始为1。
代码如下:
package com.yjoffer.javase.thread.collection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
- 线程安全集合类测试
- @author 预见猿份(www.yjoffer.com)
- @version 1.0
*/
public class ConcurrentHashMapTest {//准备等待统计的数据,统一放入集合
private static List data_init(){
String data = "0123456789";
List<String> characters = new ArrayList<>();
for (int j = 0; j < 100; j++) {
for (int i = 0; i < data.length(); i++) {
char c = data.charAt(i);
characters.add(String.valueOf(c));
}
}
return characters;按预期map中共有10个key,每个key对应的value为100。
执行程序,存在错误。
下边用线程安全的集合类ConcurrentHashMap进行修改。
首先将new HashMap()更改为new ConcurrentHashMap(),再次运行程序,结果 仍然 存在错误。
下边分析错误,导致问题的代码如下:
Integer integer = map.get(c);
if(integer == null){
map.put(c,1);
}else{
map.put(c,integer+1);
}虽然get方法是线程安全,put方法是线程安全,但是由于是多线程执行存在线程切换,上边整体代码线程不安全。
修改方案1:使用synchronized将上边的代码保持原子性。
synchronized (map){
Integer integer = map.get(c);
if(integer == null){
map.put(c,1);
}else{
map.put(c,integer+1);
}
}运行程序,结果正确。
此方法是将整个表对象作为锁对象,性能较低。
修改方案2:使用CAS方法
下边使用putIfAbsent方法实现key判断的逻辑,如果key不存在则用一个初始值填充,这里用原子类填充,并利用原子类的CAS方法自增1。
代码修改如下:
AtomicLong atomicLong = map.putIfAbsent(c, new AtomicLong(1));
if(atomicLong!=null){
atomicLong.incrementAndGet();
}map.putIfAbsent(c, new AtomicLong(1));代码相当于下边的代码:
if (!map.containsKey(key)) return map.put(key, new AtomicLong(1)); else return map.get(key);完整代码如下:
public static void test_concurrentMap(){
List<String> characters = data_init();
Map<String, AtomicLong> map = new ConcurrentHashMap<>();
ExecutorService executorService = Executors.newCachedThreadPool();
characters.stream().forEach(c->{
executorService.execute(()->{
AtomicLong atomicLong = map.putIfAbsent(c, new AtomicLong(1));
if(atomicLong!=null){
atomicLong.incrementAndGet();
}
});
});
shutdown(executorService);
System.out.println(map.size());
System.out.println(map);
}请免费关注 预见猿份 公众号,发送:yjoffer.com,获取解锁密码
微信扫码关注关注公众号回复获取密码后,在下方输入并点击解锁,本机后续再读无需重复输入。
