ConcurrentHashMap
ConcurrentHashMap是线程安全,性能出色的Map的线程安全实现,相比较HashMap他是线程安全的,相比较HashTable他的性能优势非常明显。他的使用很简单,这里主要是想要探究一下ConcurrentHashMap的实现原理。
在这里一共有 个问题需要搞明白。
- ConcurrentHashMap为什么比HashTable的性能要高?
- ConcurrentHashMap在JDK8和JDK7有什么变化,为什么会有这种变化,对我们开发有什么启示?
- 为什么在JDK8中使用Synchronized而不是用ReentrantLock来实现加锁?
带着这几个问题我们来分析一下ConcurrentHashMap的源码吧。
ConcurrentHashMap定义
在JDK8(JDK7也是一样)中ConcurrentHashMap的定义如下:
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
ConcurrentHashMap在JDK7中的实现
Java7中ConcurrentHashMap的实现是基于分段锁实现的。他的底层数据结构仍然是数组+链表,与HashTable不同的是,ConcurrentHashMap的最外层不是一个大的数组,而是一个Segment数组(分段锁的实现)。分段锁减小了锁的粒度,提高了并发程度。这也是为什么比HashTable效率要高的原因。
HashTable的源码其实很简单,HashTable和HashMap的结构一致,但是每一个方法都是用Synchronized来修饰,以保证操作是线程安全的。这样在多线程的情况下,只有一个线程获取锁操作hashTable中的数据。而CourrentHashMap则不是,它允许最多有segment数组长度个线程同时操作ConcurrentHashMap中的数据。
ConcurrentHashMap的整体结构如下(图片来源:http://www.jasongj.com/java/c...):
ConcurrentHashMap的定义:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
private static final long serialVersionUID = 7249069246763182397L;
/**
* 表的默认容量
*/
static final int DEFAULT_INITIAL_CAPACITY = 16;
/**
* 默认扩容因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* segments数组的默认长度,为了能通过按位与的散列算法来定位segments数组的索引,必须保证segments数组的长度是2的N次方
*/
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
/**
* HashEntry最大容量
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* segment的最小容量
*/
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
/**
* segment的最大容量
*/
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
/**
* 重试次数,无锁的情况下尝试两次
*/
static final int RETRIES_BEFORE_LOCK = 2;
/**
* 散列运算的掩码,等于ssize-1
*/
final int segmentMask;
/**
* 定位参与散列运算的位数,等于32-sshift
*/
final int segmentShift;
/**
* 定义segment数组
*/
final Segment<K,V>[] segments;
Segment定义:
static final class Segment<K,V> extends ReentrantLock implements Serializable {
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
//扩容量,默认为表的容量*加载因子,实际量超过这个值的时候,进行扩容
transient int threshold;
//segment中hashEntry的扩容因子
final float loadFactor;
}
- get()操作经过两次散列找到HashEntry,然后进行遍历的操作,get方法使用的变量都是volatile类型的,可以保证线程可见,能够被多线程同时读,并且保证不会读取到过期的值,在get操作中需要读count和value两个共享变量,所以不需要加锁,volatile字段的写入操作会先于读操作,所有即使有一个线程在修改,get也能获取到最新的值。
- put() 先对变量的hashCode进行一次再散列然后定位到Segment,然后再Segment中进行插入操作。如果HashEntry数组超过threshold,那么扩容,扩容只是针对Segment进行扩容。
- size() 统计ConcurrentHashMap中元素的个数,需要对Segment中所有元素进行求和,Segment里全局变量count是一个volatile类型的变量,累加可以获取元素的总个数,但是不一定准确,因为使用过的count再后面可以改变,最后的方法就是阻塞put,remove,clean等元素操作的方法,但是这样非常低效。所以Concurrenthashmap通过尝试两次不锁来统计segment的元素大小,如果两次结果不一样,那么使用加锁的方式来统计,容器是否变化是通过modCount是否变化来确定的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。