《Java源码分析》:ConcurrentHashMap JDK1.8
最近一直在看关于J.U.C中的源码,了解原子操作,了解锁机制,了解多线程并发等等。但是ConcurrentHashMap一直拖着到今天才算告一段落。
也要感谢ConcurrentHashMap这个类,刚开始就是想弄懂里面的工作原理,但是,无奈看了网上关于介绍ConcurrentHashMap这个类的资料或博客都是基于JDK1.8以前的,而刚好此类在JDK1.8之后有很大的变化。因此,由于里面涉及到关于原子操作CAS,自己以前并不知道是什么,于是就开始对原子操作进行了解,看了java.util.concurrent.atom包下相关类源码对其有了一定的了解。接着为了了解锁机制,看了java.util.concurrent.lock包下相关的类库,对锁机制有了大概的了解之后,看了线程池相关的类,对线程池也有了一定的了解。
关于阻塞队列相关的类,自己也大致看了下,但是并没有形成相应的博文,以后有时间重新来了解他们的时候才记录吧。整个过程大概花费了我将近一个来月的时间,虽然对看过的类库的内部实现都只是一个大致的了解,但是确实收获还是挺多的。让我们更好的明白在多线程并发中他们是如何来工作的。
回到正题,刚好借着今天星期天,花了将近一天的时间来看ConcurrentHashMap的实现原理,总算看了一个大概,有了一个大致的了解。也就有了这篇博文。
ConcurrentHashMap 在JDK1.8版本以前的实现原理
既然本篇博文的标题明确的标出了是基于JDK1.8版本的,也就暗示了这个版本和以前的版本关于ConcurrentHashMap有些许的不同,对吧。x
下面我们就先借助网上的资料来看下以前版本的ConcurrentHashMap的实现思路。
我们都知道HashMap是线程不安全的。Hashtable是线程安全的。看过Hashtable源码的我们都知道Hashtable的线程安全是采用在每个方法来添加了synchronized关键字来修饰,即Hashtable是针对整个table的锁定,这样就导致HashTable容器在竞争激烈的并发环境下表现出效率低下。
效率低下的原因说的更详细点:是因为所有访问HashTable的线程都必须竞争同一把锁。当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
基于Hashtable的缺点,人们就开始思考,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率呢??这就是我们的“锁分离”技术,这也是ConcurrentHashMap实现的基础。
ConcurrentHashMap使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含很多Node,也就是我们的键值对了),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
因此,关于ConcurrentHashMap就转化为了对Segment的研究。这是因为,ConcurrentHashMap的get、put操作是直接委托给Segment的get、put方法,但是自己上手上的JDK1.8的具体实现确不想网上这些博文所介绍的。因此,就有了本篇博文的介绍。
推荐几个JDK1.8以前版本的关于ConcurrentHashMap的原理分析,方便大家比较。
1、http://www.iteye.com/topic/344876
2、http://ifeve.com/concurrenthashmap/
如需要更多,请自己网上搜索即可。
下面就开始JDK1.8版本中ConcurrentHashMap的介绍。
JDK1.8 版本中ConcurrentHashMap介绍
1、前言
首先要说明的几点:
1、JDK1.8的ConcurrentHashMap中Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。
2、ConcurrentHashMap的底层与Java1.8的HashMap有相通之处,底层依然由“数组”+链表+红黑树来实现的,底层结构存放的是TreeBin对象,而不是TreeNode对象;
3、ConcurrentHashMap实现中借用了较多的CAS算法,unsafe.compareAndSwapInt(this, valueOffset, expect, update); CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。
ConcurrentHashMap既然借助了CAS来实现非阻塞的无锁实现线程安全,那么是不是就没有用锁了呢??答案:还是使用了synchronized关键字进行同步了的,在哪里使用了呢?在操作hash值相同的链表的头结点还是会synchronized上锁,这样才能保证线程安全。
看完ConcurrentHashMap整个类的源码,给自己的感觉就是和HashMap的实现基本一模一样,当有修改操作时借助了synchronized来对table[i]进行锁定保证了线程安全以及使用了CAS来保证原子性操作,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的实现思路为:根据key的hash值找到其在table所对应的位置i,然后在table[i]位置所存储的链表(或者是树)进行查找是否有键为key的节点,如果有,则返回节点对应的value,否则返回null。思路是不是很熟悉,是不是和HashMap中该方法的思路一样。所以,如果你也在看ConcurrentHashMap的源码,不要害怕,思路还是原来的思路,只是多了些许东西罢了。
2、ConcurrentHashMap类中相关属性的介绍
为了方便介绍此类后面的实现,这里需要先将此类中的一些属性给介绍下。
sizeCtl最重要的属性之一,看源码之前,这个属性表示什么意思,一定要记住。
0、private transient volatile int sizeCtl;//控制标识符
此属性在源码中给出的注释如下:
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
翻译如下:
sizeCtl是控制标识符,不同的值表示不同的意义。
- 负数代表正在进行初始化或扩容操作 ,其中-1代表正在初始化 ,-N 表示有N-1个线程正在进行扩容操作
- 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容。
1、 transient volatile Node<K,V>[] table
;是一个容器数组,第一次插入数据的时候初始化,大小是2的幂次方。这就是我们所说的底层结构:”数组+链表(或树)”
2、private static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
3、private static final intDEFAULT_CAPACITY = 16;
4、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647
5、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;
6、private static final float LOAD_FACTOR = 0.75f;
7、static final int TREEIFY_THRESHOLD = 8; // 链表转树的阀值,如果table[i]下面的链表长度大于8时就转化为数
8、static final int UNTREEIFY_THRESHOLD = 6; //树转链表的阀值,小于等于6是转为链表,仅在扩容tranfer时才可能树转链表
9、static final int MIN_TREEIFY_CAPACITY = 64;
10、private static final int MIN_TRANSFER_STRIDE = 16;
11、private static int RESIZE_STAMP_BITS = 16;
12、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大线程数
13、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
14、static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、标示位
15、static final int TREEBIN = -2; // hash for roots of trees(树根节点的hash值)
16、static final int RESERVED = -3; // hash for transient reservations(ReservationNode的hash值)
3、ConcurrentHashMap的构造函数
和往常一样,我们还是从类的构造函数开始说起。
/**
* Creates a new, empty map with the default initial table size (16).
*/
public ConcurrentHashMap() {
}
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;
}
/*
* Creates a new map with the same mappings as the given map.
*
*/
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
有过HashMap和Hashtable源码经历,看这些构造函数是不是相当easy哈。
上面的构造函数主要干了两件事:
1、参数的有效性检查
2、table初始化的长度(如果不指定默认情况下为16)。
这里要说一个参数:concurrencyLevel,表示能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认值为16,(即允许16个线程并发可能不会产生竞争)。为了保证并发的性能,我们要很好的估计出concurrencyLevel值,不然要么竞争相当厉害,从而导致线程试图写入当前锁定的段时阻塞。
ConcurrentHashMap类中相关节点类:Node/TreeNode/TreeBin
1、Node类
Node类是table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中。
Node类是没有提供修改入口的(唯一的setValue方法抛异常),因此只能用于只读遍历。
此类的具体代码如下:
/*
*Node类是没有提供修改入口的(setValue方法抛异常,供子类实现),
即是可读的。只能用于只读遍历。
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;//volatile,保证可见性
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; }
/*
HashMap中Node类的hashCode()方法中的代码为:Objects.hashCode(key) ^ Objects.hashCode(value)
而Objects.hashCode(key)最终也是调用了 key.hashCode(),因此,效果一样。写法不一样罢了
*/;
public final int hashCode() { return key.hashCode() ^ val.hashCode(); }
public final String toString(){ return key + "=" + val; }
public final V setValue(V value) { // 不允许修改value值,HashMap允许
throw new UnsupportedOperationException();
}
/*
HashMap使用if (o == this),且嵌套if;ConcurrentHashMap使用&&
个人觉得HashMap格式的代码更好阅读和理解
*/
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.
*增加find方法辅助get方法 ,HashMap中的Node类中没有此方法
*/
Node<K,V> find(int h, Object k) {
Node<K,V> e = this;
if (k != null) {
do {
K ek;
if (e.has