一 概述
我们都知道 HashMap 不是线程安全的,所以在处理并发的时候会出现问题。
而 HashTable 虽然是线程安全的,但是是通过整个方法来加锁的方式,当一个线程在写操作的时候,另外的线程则不能进行读写。
而 ConcurrentHashMap 则可以支持并发的读写。跟 1.7 版本相比,1.8 版本又有了很大的变化,已经抛弃了 Segment 的概念,虽然源码里面还保留了,也只是为了兼容性的考虑。
二 ConcurrentHashMap 原理
在 ConcurrentHashMap 中通过一个 Node[] 数组来保存添加到 map 中的键值对,而在同一个数组位置是通过链表和红黑树的形式来保存的。但是这个数组只有在第一次添加元素的时候才会初始化,否则只是初始化一个 ConcurrentHashMap 对象的话,只是设定了一个 sizeCtl 变量,这个变量用来判断对象的一些状态和是否需要扩容,后面会详细解释。
第一次添加元素的时候,默认初始长度为 16,当往 map 中继续添加元素的时候,通过 hash 值跟数组长度取与来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了 8 个以上,如果数组的长度还小于 64 的时候,则会扩容数组。如果数组的长度大于等于 64 了的话,则会将该节点的链表转换成红黑树。
通过扩容数组的方式来把这些节点给分散开。然后将这些元素复制到扩容后的新的数组中,同一个链表中的元素通过 hash 值的数组长度位来区分,是放在原来的位置还是放到扩容的长度的相同位置去 。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于 6 个了,则会将该树转为链表。
取元素的时候,相对来说比较简单,通过计算 hash 来确定该元素在数组的哪个位置,然后在通过遍历链表或树来判断 key 和 key 的 hash,取出 value 值。
往 ConcurrentHashMap 中添加元素的时候,里面的数据以数组的形式存放的样子大概是这样的:
这个时候因为数组的长度才为16,则不会转化为树,而是会进行扩容。
扩容后数组大概是这样的:
需要注意的是,扩容之后的长度不是 32,扩容后的长度在后面细说。
如果数组扩张后长度达到 64 了,且继续在某个节点的后面添加元素达到 8 个以上的时候,则会出现转化为红黑树的情况。
转化之后大概是这样:
三 相关概念
3.1 重要属性
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static final int MOVED = -1; // 表示正在转移
static final int TREEBIN = -2; // 表示已经转换成树
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
transient volatile Node<K,V>[] table; // 默认没初始化的数组,用来保存元素
private transient volatile Node<K,V>[] nextTable; // 转移的时候用的数组
/**
* 用来控制表初始化和扩容的,默认值为0,当在初始化的时候指定了大小,
* 这会将这个大小保存在 sizeCtl 中,大小为数组的 0.75
* 当为负的时候,说明表正在初始化或扩张,
* -1表示初始化
* -(1+n) n:表示活动的扩张线程
*/
private transient volatile int sizeCtl;
3.2 重要的类
Node,这是构成每个元素的基本类。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //key的hash值
final K key; //key
volatile V val; //value
volatile Node<K,V> next; //表示链表中的下一个节点
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() {
return key; }
public final V getValue() {
return val; }
public final int hashCode() {
return key.hashCode() ^ val.hashCode(); }
}
TreeNode,构造树的节点
static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
}
TreeBin 用作树的头结点,只存储 root 和 first 节点,不存储节点的 key、value 值。
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
}
ForwardingNode 在转移的时候放在头部的节点,是一个空节点
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
}
3.3 重要方法
在 ConcurrentHashMap 中使用了 unSafe 方法,通过直接操作内存的方式来保证并发处理的安全性,使用的是硬件的安全机制。
/*
* 用来返回节点数组的指定位置的节点的原子操作
*/
@SuppressWarnings("unchecked")
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);
}
/*
* 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);
}
/*
* 原子操作,在指定位置设定值
*/
static final <K