ConcurrentHashMap在java1.8进行了重大的改进。
在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成,如下图所示:
Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,也就是上面的提到的锁分离技术,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样。
JDK1.8的实现
JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。ConcurrentHashMap是HashMap的升级版,HashMap是非线程安全的集合,ConcurrentHashMap则可以支持并发操作。 因为ConcurrentHashMap 和HashMap都是采用相同的数据结构,因此在分析ConcurrentHashMap 之前,最好先了解HashMap,这样更容易理解。
在深入学习JDK1.8 ConcurrentHashMap之前,先看一下其静态常量:
// node数组最大容量:2^30=1073741824
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
static final int DEFAULT_CAPACITY = 16;
//数组可能最大值,需要与toArray()相关方法关联
final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//并发级别,遗留下来的,为兼容以前的版本
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 负载因子
static final float LOAD_FACTOR = 0.75f;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
static final int UNTREEIFY_THRESHOLD = 6;
//存储方式由链表转成红黑树 table 容量的最小阈值
static final int MIN_TREEIFY_CAPACITY = 64;
//用于hash 表扩容后,搬移数据的步长(下面几个属性都是用于扩容或者控制sizeCtl 变量)
static final int MIN_TRANSFER_STRIDE = 16;
static int RESIZE_STAMP_BITS = 16;
// 2^15-1,help resize的最大线程数
static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED = -1;
// 树根节点的hash值
static final int TREEBIN = -2;
// ReservationNode的hash值
static final int RESERVED = -3;
// 可用逻辑处理器数量,如果cup是2核4线程,返回4.
static final int NCPU = Runtime.getRuntime().availableProcessors();
Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,源代码如下:
static class Node implements Map.Entry {
final int hash; //HashMap中该字段存放的是key.hashCode();而ConcurrentHashMap存放的是spread(key.hashCode())
final K key; //key值
volatile 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;
}
}
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
TreeNode 用于构建红黑树节点,但是ConcurrentHashMap 中的TreeNode和HashMap中的TreeNode用途有点差别,HashMap中hash 表的部分位置上存储的是一颗树,具体存储的就是TreeNode型的树根节点,而ConcurrentHashMap 则不同,其hash 表是存储的被TreeBin 包装过的树,也就是存放的是TreeBin对象,而不是TreeNode对象,同时TreeBin 带有读写锁,当需要调整树时,为了保证线程的安全,必须上锁。
static final class TreeNode extends Node {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next,
TreeNode parent) {
super(hash, key, val, next);
this.parent = parent;
}
}
TreeBin 对象
static final class TreeBin extends Node {
TreeNode root; // 树根
volatile TreeNode 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 extends Node {
final Node[] nextTable;
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null); // hash 值为MOVED 进行标识
this.nextTable = tab;
}
ForwardingNode 用于在hash 表扩容过程中的过渡节点,当hash 表进行扩容进行数据转移的时候,其它线程如果还不断的往原hash 表中添加数据,这个肯定是不好的,因此就引入了ForwardingNode 节点,当对原hash 表进行数据转移时,如果hash 表中的位置还没有被占据,那么就存放ForwardingNode 节点,表明现在hash 表正在进行扩容转移数据阶段,这样,其它线程在操作的时候,遇到ForwardingNode 节点,就知道hash 现在的状态了,就可以协助参与hash 表的扩容过程。到这里,ConcurrentHashMap 中的重要的数据结构基本都了解了,一个是hash 表(table),一个是链表节点Node,其实呢就是红黑树节点TreeNode。
Unsafe类提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是不安全的,它所分配的内存需要手动free(不被GC回收)。java不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作。
在ConcerrentHashMap中:
// Unsafe mechanics
private static final sun.misc.Unsafe U;
private static final long SIZECTL; //sizeCtl字段的偏移量
private static final long TRANSFERINDEX;//transferIndex字段的偏移量
private static final long BASECOUNT;//baseCount字段的偏移量
private static final long CELLSBUSY;//cellsBusy字段的偏移量
private static final long CELLVALUE;//CounterCell类中value字段的偏移量
private static final long ABASE;//Node数组的头部偏移量
private static final int ASHIFT;//保存的是
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);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)//检测scale是不是2的幂。
throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);//童年ASHIFT =√scale
} catch (Exception e) {
throw new Error(e);
}
}
/**
* 获取tab数组中索引为i的值。
*/
static final Node tabAt(Node[] tab, int i) {
return (Node)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
/**
* tab数组中,如果a[i]等于c,将a[i]设置成v并返回true,否则返回false
*/
static final boolean casTabAt(Node[] tab, int i,
Node c, Node v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
/**
* 取tab数组中索引为i的格子设置成v。
*/
static final void setTabAt(Node[] tab, int i, Node v) {
U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}
Unsafe这里不多说,但是我们要理解 "偏移量"、Unsafe.getObjectVolatile方法、Unsafe.compareAndSwapObject方法 和 Unsafe.putObjectVolatile方法的意思。
举个例子:
class Person {
private String name="无";
}
public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// Unsafe unsafe = Unsafe.getUnsafe(); //不允许外部使用,会抛异常。
//通过反射获取unsafe对象
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);
//获取Person.name字段的偏移量
long nameOffset = unsafe.objectFieldOffset
(Person.class.getDeclaredField("name"));
Person person = new Person();
System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//无,获取name值。
unsafe.compareAndSwapObject(person, nameOffset, "呜呜", "小黑");//false,”呜呜“不等于”无“,无法成功设置新值
System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//无
unsafe.compareAndSwapObject(person, nameOffset, "无", "小黑");//true ,成功设置新值
System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//小黑
unsafe.putObjectVolatile(person, nameOffset,"小强"); //这种方法强制设置新值,不关心旧值。
System.out.println((String)unsafe.getObjectVolatile(person,nameOffset));//小强
}
}
结果:
这个例子中,通过unsafe类完成了对person对象的操作。
unsafe类如何操作数组呢?
首先普及一下数组知识,数组是块连续的存储空间,如果数组中存放的是基本数据类型,是将数据直接保存在数组中;如果保存的是对象,其实只是保存的是对象的地址。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
//通过反射获取unsafe对象
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeInstance.get(Unsafe.class);
long ABASE = unsafe.arrayBaseOffset(String[].class);//数组的头部偏移量
int scale = unsafe.arrayIndexScale(String[].class);//如果是基本数据类型的数组,返回数据类型长度,如long,返回8;否则,返回对象的地址长度,32位系统返回4,64位系统返回4或8.
String[] stringArray = new String[10];
String str = "哈哈";
int index = 5;
unsafe.putObjectVolatile(stringArray, ABASE + (index *scale), str);
String StrNew = (String) unsafe.getObjectVolatile(stringArray, ABASE + (index *scale));
System.out.println(StrNew);
System.out.println(stringArray[index]);
}
结果:
2、PUT方法
对于集合类的增删改查操作,其中最为重要的就是添加操作,理解了添加操作就掌握了这个集合。
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//tab为空时初始化tab。
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//tab[ (n - 1) & hash)]不存在时
if (casTabAt(tab, i, null,
new Node(hash, key, value, null)))//新创建的填node加到tab中,如果填加成功,跳出循环。
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)// 如果table位置上的节点状态时MOVE,则表明table正在进行扩容搬移数据的过程中
tab = helpTransfer(tab, f); //协助扩容
else {// hash 表该位置上有数据,可能是链表,也可能是一颗树
V oldVal = null;
synchronized (f) {/将table表该位置进行上锁,保证线程安全
if (tabAt(tab, i) == f) {// 上锁后,只有再该位置数据和上锁前一致才进行,否则需要重新循环
if (fh >= 0) {// hash 值>=0 表明这是一个链表结构
binCount = 1;
for (Node e = f;; ++binCount) {// 遍历链表
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node pred = e;
if ((e = e.next) == null) { // 将新数据添加到链表尾
pred.next = new Node(hash, key,
value, null);
break;
}
}
}
else if (f instanceof TreeBin) {// 该位置是红黑树,是TreeBin对象(注意是TreeBin,而不是TreeNode)
Node p;
binCount = 2;
if ((p = ((TreeBin)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
//添加了数据,需要进行检查
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //if 成立,说明遍历的是链表结构,并且超过了阀值,需要将链表转换为树
treeifyBin(tab, i); //将table 索引i 的位置上的链表转换为红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
// ConcurrentHashMap 容量增加1,检查是否需要扩容
addCount(1L, binCount);
return null;
}
主要知识点:
1、计算key的hash 值。
HashMap中Node.hash存放的是key.hashCode(),为什么ConcurrentHashMap 中Node.hash存放 spread(key.hashCode())?因为ConcurrentHashMap 中Node.hash还被用来作标识,
当Node.hash=-1时表示这是一个forwardNode节点;
当Node.hash=-2时表示这时一个TreeBin节点;
当Node.hash=-3时表示短暂保留节点;
当Node.hash>=0时表示这时一个普通node节点。
因为key.hashCode()有可能返回负数(-1、-2),因此ConcurrentHashMap中Node.hash就不能在直接使用key.hashCode(),所以使用spread方法进行转换,spread方法的返回值一定大于等于0。
static final int MOVED = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
2、initTable方法
如果table没有被初始化,则执行table的初始化过程,使用initTable方法对table进行始化。
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义:
*当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
*当为0时:代表当时的table还没有被初始化
*当为正数时:表示初始化或者下一次进行扩容的大小
*/
private transient volatile int sizeCtl;
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
private final Node[] initTable() {
Node[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)//sizeCtl为-1代表别的线程正在初始化,-N代表有N-1个线程正在 进行扩容
Thread.yield(); // 线程让步,其实就是等待别的线程操作完成。
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//sizeCtl为0表于table还没有被初始化;设置成-1,开始初始化
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//获取table初始化大小
@SuppressWarnings("unchecked")
Node[] nt = (Node[])new Node,?>[n];
table = tab = nt;
sc = n - (n >>> 2);//设置下一次进行扩容的大小。
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}