HashMap是线程不安全的,而线程安全类Hashtable只是简单的在方法上加锁实现线程安全,效率低下,所以通常使用ConcurrentHashMap
ConcurrentHashMap不允许空键值对,否则异常;
1.ConcurrentHashMap怎样做到线程安全的?
可以通过减少锁竞争来优化并发性能,而ConcurrentHashMap则在JDK8-使用了锁分段(减小锁范围)、JDK8开始大量使用CAS(乐观锁,减小上下文切换开销,无阻塞)和少量的同步代码块技术
Jdk1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)
Segment本身是基于ReentrantLock实现的加锁和释放锁的操作,这样就能保证多个线程同时访问ConcurrentHashMap时,同一时间只有一个线程能操作相应的节点,这样就保证了ConcurrentHashMap的线程安全了。也就是说ConcurrentHashMap的线程安全是建立在Segment 加锁的基础上的,所以我们把它称之为分段锁。
总结
ConcurrentHashMap 在 JDK 1.7 时使用的是数据加链表的形式实现的,其中数组分为两类:大数组 Segment 和小数组 HashEntry,而加锁是通过给 Segment 添加 ReentrantLock 锁来实现线程安全的。而 JDK 1.8 中 ConcurrentHashMap 使用的是数组+链表/红黑树的方式实现的,它是通过 CAS 或 synchronized 来实现线程安全的,并且它的锁粒度更小,查询性能也更高。
节点类型
Node节点
static class Node<K,V> implements Map.Entry<K,V>{
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
默认桶上的节点就是Node结点。Node只有一个next指针,是一个单链表,提供find方法实现链表查询
当出现Hash冲突时,Node节点会首先以链表的形式链接到table上,当结点数量超过一定数目时,链表会转化为红黑树。
TreeNode节点
static final class TreeNode<K,V> extends Node <K,V>{
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
static final int WRITER=1;
static final int WAITER=2;
static final int READER=4;
}
TreeBin 会直接链接到table[i] 一桶上面,该节点提供了一系列红黑树相关的操作,以及加锁、解锁操作。
另外TreeBin提供了一系列的操作
TreeBin(TreeNode<K,V> b),将以b为头结点的链表转换为红黑树
lockRoot(),对红黑树的根节点加写锁
unlockRoot(),释放写锁
find(int h,Object k), 从根结点开始遍历查找,找到相等的节点就返回它,没找到就返回null,当存在写锁时,以链表方式进行查找,不阻塞读锁。
ForwardingNode
static final class ForwardingNode<K,V> extends Node<K,V>{
final Node<K,V>[] nextTable;
}
ForwardingNode在table扩容时使用,内部记录了扩容后的table,即nextTable中,在原table 的槽内放置一个ForwardingNode
ForwardingNode是一种临时节点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据
如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode
ForwardingNode是一种临时结点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode读操作碰到ForwardingNode时,将操作转发到扩容后的新table数组上去执行;写操作碰见它时,则尝试帮助扩容,扩容是支持多线程一起扩容。
ReservationNode保留结点
static final class ReservationNode<K,V> extends Node<K,V>{
ReservationNode(){
super(RESERVED,null,null);
}
Node<K,V> find(int h,Object k){
return null;
}}
在并发场景下、在从Key不存在到插入的时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全
hash值固定为-3,不保存实际数据。只在computeIfAbsent和compute这两个函数式API中充当占位符加锁使用
总结:
就算有多个线程同时进行put操作,在初始化数组时使用了乐观锁CAS操作来决定 到底是哪个线程有资格进行初始化,其他线程均只能等待。
用到的并发技巧:
-
volatile变量(sizeCtl):它是一个标记位,用来告诉其他线程这个坑位有没有人在,其线程间的可见性由volatile保证。
-
CAS操作:CAS操作保证了设置sizeCtl标记位的原子性,保证了只有一个线程能设置成功