ConcurrentHashMap在使用时与HashMap效果一样,但ConcurrentHashMap时线程安全且高效的HashMap。在原来HashMap的设计上融入了并发编程的思想,在保证线程安全的同时又能保证高效的操作。
为什么要使用ConcurrentHashMap呢?
(1)HashMap线程不安全
在多线程环境下,使用 HashMap 进行 put 操作会引起死循环,导致 CPU 利用率接近 100%,所以在并发情况下不能使用 HashMap。
HashMap 在并发执行 put 操作时会引起死循环,是因为 多线程会导致 HashMap 的Entry 链表形成环形数据结构 ,一旦形成环形数据结构, Entry 的 next 节点永远不为空,就会产生死循环获取 Entry。
(2)HashTable效率低下
HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下HashTable 的效率非常低下。因为当一个线程访问 HashTable 的同步方法,其他线程也访问 HashTable 的同步方法时,会进入阻塞或轮询状态。所以也不选择使用HashTable。
(3)ConcurrentHashMap 的锁分段技术可有效提升并发访问率
HashTable 容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问 HashTable 的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是 ConcurrentHashMap 所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
如果桶数组未初始化,则执行初始化操作
如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置
如果正在扩容,则当前线程一起加入到扩容的过程中
如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁)
如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素
如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素
如果元素存在,则返回旧值
如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容
结构图:
/* ---------------- Constants -------------- *//*** 最大可能的表容量。这个值必须恰好为1<<30,以保持在Java数组分配和索引边界内,* 以保证两个表大小的幂,而且还需要这个值,因为32位哈希字段的前两位用于控制目的。*/
private static final int MAXIMUM_CAPACITY = 1 << 30;/*** 默认的初始表容量。必须是2的幂(即至少1),且最大值为MAXIMUM_CAPACITY。*/
private static final int DEFAULT_CAPACITY = 16;/*** 可能的最大(非2的幂)数组大小。toArray和相关方法需要。*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;/*** 此表的默认并发级别。未使用,但为了与该类的以前版本兼容而定义。* 默认并发级别是 jdk1.7遗留下来的,1.8只是初始化的时候用了,在1.8中不代表并发级别*/
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;/*** 此表的负载系数。在构造函数中重写此值只影响初始表容量。* 通常不使用实际的浮点值——使用n - (n >>> 2)等表达式作为相关的调整阈值更简单*/
private static final float LOAD_FACTOR = 0.75f;/*** 树化阈值 和MIN_TREEIFY_CAPACITY一起控制(链表长度大于8并且数组长度大于64才转为红黑树)*/
static final int TREEIFY_THRESHOLD = 8;/*** 树转链表阈值 */
static final int UNTREEIFY_THRESHOLD = 6;/*** 树化条件,与TREEIFY_THRESHOLD配合使用*/
static final int MIN_TREEIFY_CAPACITY = 64;/*** 线程迁移数据最小步长,控制线程迁移任务最小区间的一个值(即桶位的跨度)*/
private static final int MIN_TRANSFER_STRIDE = 16;/*** 扩容相关,计算扩容时生成的一个标识戳*/
private static int RESIZE_STAMP_BITS = 16;/*** 计算出来的值是65535((1 << 16) - 1) 代表并发扩容的最多线程数*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;/*** 在sizeCtl中记录大小戳的位移位。*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;/** 节点哈希字段的编码。*/
// 当node节点的hash值是-1时,表示当前节点时FWD节点(已经被迁移的节点)
static final int MOVED = -1;
// 当node节点的hash值为-2:表示当前节点已经树化,且当前节点为TreeBin对象,TreeBin对象代理操作红黑树
static final int TREEBIN = -2;
// 当node节点的hash值为-3:
static final int RESERVED = -3;
// 0x7fffffff 十六进制转二进制值为:1111111111111111111111111111111(31个1)
// 作用是将一个二进制负数与1111111111111111111111111111111 进行按位与(&)运算时,会得到一个正数,但不是取绝对值
static final int HASH_BITS = 0x7fffffff;/** 当前系统的CPU数量 */
static final int NCPU = Runtime.getRuntime().availableProcessors();/** JDK1.8 序列化为了兼容 JDK1.7的ConcurrentHashMap用到的属性 (非核心属性) */
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("segments", Segment[].class),new ObjectStreamField("segmentMask", Integer.TYPE),new ObjectStreamField("segmentShift", Integer.TYPE)
};
/*** 散列表table*/
transient volatile Node[] table;/*** 新表的引用;只有在调整大小时才是非空的。* 扩容过程中,会将扩容中的新table赋值给nextTable,(保持引用),* 扩容结束之后,这里就会被设置为NULL*/
private transient volatile Node[] nextTable;/*** // 与LongAdder中的baseCount作用相同: 当未发生线程竞争或当前LongAdder处于加锁状态时,增量会被累加到baseCount*/
private transient volatile long baseCount;// 表示散列表table的状态:
// sizeCtl<0时:
// 情况一 sizeCtl=-1: 表示当前table正在进行初始化(即,有线程在创建table数组),当前线程需要自旋等待...
// 情况二 表示当前table散列表正在进行扩容,高16位表示扩容的标识戳,低16位表示扩容线程数:(1 + nThread) 即,当前参与并发扩容的线程数量。
// sizeCtl=0时:
// 表示创建table散列表时,使用默认初始容量DEFAULT_CAPACITY=16
// sizeCtl>0时:
// 情况一 如果table未初始化,表示初始化大小
// 情况二 如果table已经初始化,表示下次扩容时,触发条件(阈值)
private transient volatile int sizeCtl;/*** 扩容过程中,记录当前进度。所有的线程都需要从transferIndex中分配区间任务,并去执行自己的任务* 调整大小时要拆分的下一个表索引(加一个)。*/
private transient volatile int transferIndex;/*** 自旋锁(通过CAS锁定)在调整大小和/或创建countercell时使用。* 0: 表示无锁状态* 1: 表示加锁状态*/
private transient volatile int cellsBusy;/*** 计数单元表。当非空时,size是2的幂。* 与LongAdder的cells数组作用类似,线程会通过计算hash值,找到对应数组中的位置* */
private transient volatile CounterCell[] counterCells;// views,遍历Map使用
private transient KeySetView keySet;
private transient ValuesView values;
private transient EntrySetView entrySet;
// Unsafe mechanics
// Unsafe类对象
private static final sun.misc.Unsafe U;
// 表示sizeCtl属性在ConcurrentHashMap中内存的偏移地址
private static final long SIZECTL;
// 表示transferIndex属性在ConcurrentHashMap中内存的偏移地址
private static final long TRANSFERINDEX;
// 表示baseCount属性在ConcurrentHashMap中内存的偏移地址
private static final long BASECOUNT;
// 表示cellsBusy属性在ConcurrentHashMap中内存的偏移地址
private static final long CELLSBUSY;
// 表示cellsValue属性在ConcurrentHashMap中内存的偏移地址
private static final long CELLVALUE;
// 表示数组第一个元素的偏移地址
private static final long ABASE;
// 该属性用于数组寻址(更详细见静态代码块部分)
private static final int ASHIFT;
静态代码块主要用来处理Unsafe类及其相关属性的初始化。
static {try {U = sun.misc.Unsafe.getUnsafe();Class> k = ConcurrentHashMap.class;SIZECTL = U.objectFieldOffset(k.getDeclaredField("sizeCtl"));TRANSFERINDEX = U.objectFieldOffset(k.getDeclaredField("transferIndex"));BASECOUNT = U.objectFieldOffset(k.getDeclaredField("baseCount"));CELLSBUSY = U.objectFieldOffset(k.getDeclaredField("cellsBusy"));Class> ck = CounterCell.class;CELLVALUE = U.objectFieldOffset(ck.getDeclaredField("value"));Class> ak = Node[].class;// 拿到数组第一个元素的偏移地址ABASE = U.arrayBaseOffset(ak);// 表示数组中每一个单元所占用的空间大小,即scale表示Node[]数组中每一个单元所占用的空间int scale = U.arrayIndexScale(ak);// 判断scale是否为2的幂// java语言规范中,要求数组中计算出的scale必须为2的次幂数if ((scale & (scale - 1)) != 0)throw new Error("data type scale not a power of two");// numberOfLeadingZeros()方法的作用是返回无符号整型数字的最高非零位前面的n个0的个数,包括符号位。// 例如numberOfLeadingZeros(4)的返回值就是29// 而ASHIFT的值就是31-29=2// 在来看ASHIFT的作用:// 当ASHIFT为2时,如何得到下标为5的Node[]数组元素的偏移地址?// ABASE + (5 << ASHIFT) == ABASE + (5 << 2) == ABASE + 5 * scale// 所以ASHIFT的作用就是为了通过位运算计算得到数组元素的偏移地址。ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);} catch (Exception e) {throw new Error(e);}
}
static class Node implements Map.Entry {// 存储节点哈希值, 是经过扰动运算后再次得到的哈希值final int hash;final K key; // keyvolatile V val; // value// 后继绩点引用volatile Node next;Node(int hash, K key, V val, Node 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(); }public final String toString(){ return key + "=" + val; }public final V setValue(V value) {throw new UnsupportedOperationException();}public final boolean equals(Object o) {Object k, v, u; Map.Entry,?> e;return ((o instanceof Map.Entry) &&(k = (e = (Map.Entry,?>)o).getKey()) != null &&(v = e.getValue()) != null &&(k == key || k.equals(key)) &&(v == (u = val) || v.equals(u)));}/*** Virtualized support for map.get(); overridden in subclasses.* 对map.get()的虚拟化支持;在子类覆盖。*/Node find(int h, Object k) {Node e = this;if (k != null) {do {K ek;if (e.hash == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;} while ((e = e.next) != null);}return null;}
}
// 如果是一个写的线程(并发扩容线程),则需要帮忙创建新表
// 如果是一个读的线程,则调用该内部类的find(int h, Object k)方法
static final class ForwardingNode extends Node {// 新hash表的引用final Node[] nextTable;ForwardingNode(Node[] tab) {super(MOVED, null, null, null);this.nextTable = tab;}// 到新表中读取数据Node find(int h, Object k) {// loop to avoid arbitrarily deep recursion on forwarding nodes// 翻译:循环以避免转发节点上的任意深度递归outer: for (Node[] tab = nextTable;;) {Node e; int n;if (k == null || tab == null || (n = tab.length) == 0 ||(e = tabAt(tab, (n - 1) & h)) == null)return null;for (;;) {int eh; K ek;if ((eh = e.hash) == h &&((ek = e.key) == k || (ek != null && k.equals(ek))))return e;if (eh < 0) {if (e instanceof ForwardingNode) {tab = ((ForwardingNode)e).nextTable;continue outer;}elsereturn e.find(h, k);}if ((e = e.next) == null)return null;}}}
}
红黑树节点。
static final class TreeNode extends Node {// 父节点TreeNode parent; // 红黑树的链接// 左子节点TreeNode left;// 右节点TreeNode right;// 前驱节点TreeNode prev; // 需要在删除时断开下一个链接// 节点有红、黑两种颜色boolean red;TreeNode(int hash, K key, V val, Node next,TreeNode parent) {super(hash, key, val, next);this.parent = parent;}Node find(int h, Object k) {return findTreeNode(h, k, null);}/*** Returns the TreeNode (or null if not found) for the given key* starting at given root.*/final TreeNode findTreeNode(int h, Object k, Class> kc) {if (k != null) {TreeNode p = this;do {int ph, dir; K pk; TreeNode q;TreeNode pl = p.left, pr = p.right;if ((ph = p.hash) > h)p = pl;else if (ph < h)p = pr;else if ((pk = p.key) == k || (pk != null && k.equals(pk)))return p;else if (pl == null)p = pr;else if (pr == null)p = pl;else if ((kc != null ||(kc = comparableClassFor(k)) != null) &&(dir = compareComparables(kc, k, pk)) != 0)p = (dir < 0) ? pl : pr;else if ((q = pr.findTreeNode(h, k, kc)) != null)return q;elsep = pl;} while (p != null);}return null;}
}
在构造器中可以看到,ConcurrentHashMap与HashMap的不同点,在ConcurrentHashMap中没有HashMap中具有的threshold
和loadFactor
,而是改用 sizeCtl
来控制扩容。
官网上sizeCtl
的解释:
-1
,表示有线程正在进行初始化操作。-(1 + nThreads)
,表示有n个线程正在一起扩容。0
,默认值,后续在真正初始化的时候使用默认容量。> 0
,初始化或扩容完成后下一次的扩容门槛 。// 用默认的初始表大小(16)创建一个新的空映射。
public ConcurrentHashMap() {
}// 指定初始化容量构造器
public ConcurrentHashMap(int initialCapacity) {// 校验initialCapacity合法性if (initialCapacity < 0)throw new IllegalArgumentException();// 如果initialCapacity大于(1 << 30 = 1073741824),就将cap设置为MAXIMUM_CAPACITY;// 如果小于,就计算容量,将其赋值给capint cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));// 当目前table未初始化时,sizeCtl的值表示初始化容量(这时sizeCtl>0)this.sizeCtl = cap;
}// 传入一个集合Map来初始化
public ConcurrentHashMap(Map extends K, ? extends V> m) {this.sizeCtl = DEFAULT_CAPACITY;putAll(m);
}// 指定initialCapacity初始容量和loadFactor负载因子,实际上调用了下面的构造器
public ConcurrentHashMap(int initialCapacity, float loadFactor) {this(initialCapacity, loadFactor, 1);
}// 指定initialCapacity初始容量和loadFactor负载因子还有concurrencyLevel并发级别
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {// 检验参数合法性if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();// 保证初始容量不小于并发级别, 即,JDK1.8以后并发级别由散列表长度决定if (initialCapacity < concurrencyLevel) // Use at least as many binsinitialCapacity = concurrencyLevel; // as estimated threads// 根据initialCapacity和loadFactor计算sizelong size = (long)(1.0 + (long)initialCapacity / loadFactor);// 再根据size重新计算cap,即初始化容量int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);// 当目前table未初始化时,sizeCtl的值表示初始化容量this.sizeCtl = cap;
}
从这几个构造方法可以看出,在调用构造方法后并不会创建table数组,而是先给一些属性赋初值。