Java-Concurrent框架--ConcurrentHashMap源码解析(JDK1.7)
来源:互联网 发布:阿里云备案域名介入 编辑:程序博客网 时间:2024/06/06 00:49
在Map的实现类中除了HashMap和HashTable以外还有一个ConcurrentHashMap。HashMap和HashTable一都非常熟悉,HashTable是线程安全的、且不能存储Null值,HashMap是非线程安全的、可以存储Null值。
ConcurrentHashMap是java.util.concurrent包中的类,他是结合HashMap和HashTable而来的Map实现类。从名字就知道是一种线程安全的Map类,那他和线程安全的HashTable有什么区别,往下看就知道了。
一 内部结构
在介绍ConcurrentHashMap内部结构之前,先复习下HashTable,就知道ConcurrentHashMap内部结构的设计原理了
1.1 HashTable
先看一下HashTable主要方法的方法定义:
public synchronized int size()public synchronized boolean isEmpty()public synchronized V get(Object key)public synchronized V put(K key, V value)
通过上面的方法定义,可以知道HashTable之所以能够保证线程安全,是因为基本上HashTable所有的方法都通过synchronized进行了锁保护,而且是将整个HashTable对象加锁,也就是同时只能有一个线程访问、修改HashTable。这样带来的问题就是在多线程环境中代价大、效率低。
1.2 ConcurrentHashMap
HashTable效率低速度慢的原因就在于每次都要将整个HashTable锁起来,ConcurrentHashMap正是巧妙的解决了这个问题,每次只将要被修改的局部地方加上锁,其他地方还可以访问和修改。
ConcurrentHashMap的内部结构通过两个内部类实现:Segment类和HashEntry类。
每个ConcurrentHashMap对象所有数据都是存在Segment< K,V>[] segments数组里,数组的每个元素都是一个Segment对象;Segment类的主要数据结构是HashEntry< K,V>[] table数组,table数组中的HashEntry是一个链表,链表的每个结点存储一个键值对。ConcurrentHashMap中每一个键值对最终就是存储在这里。
所以说ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点)。
然后每次只对segments数组中的一个Segment对象加锁,这样segments数组中跟其他Segment对象就可以被正常访问了。所以说ConcurrentHashMap是HashMap和HashTable的结合。
ConcurrentHashMap就是通过这种局部加锁的方式提高并发访问的速度。下图就是两种Map实现类的结构对比。
二 类关系图
ConcurrentHashMap类位于java.util.concurrent包当中,实现了ConcurrentMap接口,继承了AbstractMap。
然后我们再看看ConcurrentHashMap内部类Segment的类关系图,可以发现Segment继承了Reentrantlock类,正是他完成了多线程的同步控制
三 ConcurrentHashMap类定义
我们从主要成员变量、构造函数、常用API三个方面了解ConcurrentHashMap的定义
3.1 主要成员变量
上面第一节讲了ConcurrentHashMap有两个重要的内部类Segment类和HashEntry类,下面我们就看看他们的源码
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { final Segment<K,V>[] segments; static final class Segment<K,V> extends ReentrantLock implements Serializable { transient int count; //用于记录每个Segment桶中键值对的个数 transient int modCount; //对table的大小造成影响的操作的数量(比如put或者remove操作) transient int threshold; //阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容 transient volatile HashEntry<K,V>[] table; //链表数组,数组中的每一个元素代表了一个链表的头部 final float loadFactor; //负载因子,用于确定threshold,默认是1 } static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; }}
可以看出来Segment是个锁对象,HashEntry是链表的一个结点,HashEntry.Key存的是每个键值对的键值,HashEntry.Value存的键值对的值。而且大多数变量都是final修饰的。
3.2 构造函数
ConcurrentHashMap一共有五个构造函数,我们重点分析下面第一个就行,其他三个都是这个构造函数实现的。最后一个构造函数相当于是复制一个新的对象。
- public ConcurrentHashMap(int ,float , int)
- public ConcurrentHashMap(int ,float)
- public ConcurrentHashMap(int)
- public ConcurrentHashMap()
- public ConcurrentHashMap(Map
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { // initialCapacity是Map中键值对初始个数 // loadFactor是负载参数 // concurrentLevel代表ConcurrentHashMap内部的Segment的数量 if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; // Find power-of-two sizes best matching arguments int sshift = 0; int ssize = 1; // Segment[]数组大小 while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1; } this.segmentShift = 32 - sshift; this.segmentMask = ssize - 1; if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize; if (c * ssize < initialCapacity) ++c; int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c) cap <<= 1; // table[]数组大小 // create segments and segments[0] Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor), (HashEntry<K,V>[])new HashEntry[cap]); Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize]; UNSAFE.putOrderedObject(ss, SBASE, s0); this.segments = ss; }
在初始化过程中涉及几个关键的量,我们先看构造函数的三个参数:
- initialCapacity:ConcurrentHashMap中键值对总个数,如果不指定是默认取16
- loadFactor:负载参数,用于确定table[]数组扩容阈值,就是当table[ ]数组长度大于(cap * loadFactor)时,进行扩容,未指定是默认取0.75
- concurrencyLevel:代表ConcurrentHashMap内部的segment[ ]数组的长度,concurrencyLevel在初始化后是不可改变的,也就是说segment[ ]数组长度是不可变得,所以扩容操作主要针对于table[ ]数组。这样的好处在于扩容是不需要对整个ConcurrentHashMap做rehash,只对某个table[ ]数组做rehash即可。未指定是默认取16
构造函数主要功能就是创建了sagment[ ]数组和table[ ]数组,而重点就在于计算这两个数组的大小。
- ssize:sagment[ ]数组的真实大小,sszie是不大于concurrencyLevel的最大的2的指数,他的好处在于方便采用移位操作进行hash,加快速度
- cap:table[ ]数组的大小,cap是不大于initialCapacity / ssize的最大的2的指数,好处同样也是加快hash
另外还有两个重要的变量segmentShift和segmentMask:
- segmentShift
- segmentMask
这两个量是用来定位segment桶在segments[]数组中位置用的,详见“4.1定位sagment桶”
3.3 常用API
- public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) 构造函数
- public boolean isEmpty( )
- public int size()
- public V get(Object key)
- public boolean containsKey(Object key)
- public boolean containsValue(Object value)
- public V put(K key, V value)
- public V remove(Object key)
- public void clear()
- public Set< K> keySet()
四 常用方法源码分析
4.1 定位sagment桶
在ConcurrentHashMap的增删改查操作中有一个步骤至关重要,就是要先通过key值确定键值对究竟是存在segments[ ]数组中哪个位置,table[ ]数组中哪个位置。
private Segment<K,V> segmentForHash(int h) { long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u); }
参数h就是hash值,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,这里的segmentShift和segmentMask值是在构造函数中计算得到的,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。
这里有两个地方我一直还没搞懂,一是“>>> segmentShift”操作和“& segmentMask”感觉是重复的,不知道为什么要这样,多一次计算确保正确?二是后面的“<< SSHIFT) + SBASE”一直还没搞明白是在干啥。。。
计算table[]数组中的位置通过int index = (table.length - 1) & hash;实现。
4.2 put()
// ConcurrentHashMap类的put()方法 public V put(K key, V value) { Segment<K,V> s; if (value == null) throw new NullPointerException(); int hash = hash(key); int j = (hash >>> segmentShift) & segmentMask; if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment s = ensureSegment(j); // 调用Segment类的put方法 return s.put(key, hash, value, false); } // Segment类的put()方法 final V put(K key, int hash, V value, boolean onlyIfAbsent) { // 加锁 HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { HashEntry<K,V>[] tab = table; // 根据hash计算在table[]数组中的位置 int index = (tab.length - 1) & hash; HashEntry<K,V> first = entryAt(tab, index); for (HashEntry<K,V> e = first;;) { if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry<K,V>(hash, key, value, first); int c = count + 1; // 判断table[]是否需要扩容,并通过rehash()函数完成扩容 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }
可以发现put的基本过程就是,通过hash值确定segments[]中segment桶的位置,然后调用Segment的put方法将键值对插入segment桶的table[]数组中,先确定table[]数组是否存在该key值和对应的位置,再插入到具体的链表位置。
有以下几点值得关注:
- 同步锁:多线程同步锁是在调用Segment类的put方式时使用的,此时其他线程不能访问当前segment桶,但是可以其他segment桶
- 扩容:在添加新的键值对之前会判断当前segment桶大小是否超过阈值threshold ,如果超过就调用rehash()进行扩容,newCapacity = oldCapacity << 1;将table[]大小扩容到原来的两倍
4.3 get( )
public V get(Object key) { Segment<K,V> s; // manually integrate access methods to reduce overhead HashEntry<K,V>[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }
get( )方法的实现过程比较简单,首先计算hash值并确定segment桶,然后确定table[]数组中HashEntry的位置,遍历HashEntry链表,获取Value值
4.4 remove( )
// ConcurrentHashMap类的remove方法 public V remove(Object key) { int hash = hash(key); // 获取segment桶 Segment<K,V> s = segmentForHash(hash); // 调用Segment类的remove方法 return s == null ? null : s.remove(key, hash, null); } Segment类的remove方法 final V remove(Object key, int hash, Object value) { // 进行加锁 if (!tryLock()) scanAndLock(key, hash); V oldValue = null; try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash; HashEntry<K,V> e = entryAt(tab, index); HashEntry<K,V> pred = null; while (e != null) { K k; HashEntry<K,V> next = e.next; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { V v = e.value; if (value == null || value == v || value.equals(v)) { if (pred == null) setEntryAt(tab, index, next); else pred.setNext(next); ++modCount; --count; oldValue = v; } break; } pred = e; e = next; } } finally { unlock(); } return oldValue; }
理解了put( )和get( )操作后,删除操作也就自然理解了。
之前有的版本的JDK中,HashEntry类的实现中next的声明为:final HashEntry
4.5 size( )
public int size() { // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. final Segment<K,V>[] segments = this.segments; int size; boolean overflow; // true if size overflows 32 bits long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn't retry try { for (;;) { if (retries++ == RETRIES_BEFORE_LOCK) { // 给所有的segment桶加锁 for (int j = 0; j < segments.length; ++j) ensureSegment(j).lock(); // force creation } sum = 0L; size = 0; overflow = false; for (int j = 0; j < segments.length; ++j) { Segment<K,V> seg = segmentAt(segments, j); if (seg != null) { sum += seg.modCount; int c = seg.count; if (c < 0 || (size += c) < 0) overflow = true; } } if (sum == last) break; last = sum; } } finally { if (retries > RETRIES_BEFORE_LOCK) { for (int j = 0; j < segments.length; ++j) segmentAt(segments, j).unlock(); } } return overflow ? Integer.MAX_VALUE : size; }
可以发现size( )方法很不一样:
- size( )方法会将整个ConcurrentHashMap遍历一次,来获得map大小
- 遍历之前会将ConcurrentHashMap中每个segment桶都加锁
五 总结
ConcurrentHashMap是结合HashTable和HashMap得到的线程安全的保证高并发的Map实现类
- ConcurrentHashMap主要结构是一个Segment[]数组,每个Segment元素都有一个HashEntry[]数组,Segment相当于一个小的HashTable
- Segment[]数组大小是不可变的,扩容操作是将Segment类中HashEntry[]扩容到原来的两倍
- put( )和remove( )方法是要加锁的,而且只对单个segment桶加锁
- size( )需要遍历整个ConcurrentHashMap,而且每个segment桶都要加锁
参考资料:
http://www.iteye.com/topic/1103980
http://www.cnblogs.com/ITtangtang/p/3948786.html
- Java-Concurrent框架--ConcurrentHashMap源码解析(JDK1.7)
- ConcurrentHashMap源码解析——JDK1.7
- Java集合框架--HashMap源码解析(JDK1.7)
- Java集合框架--ArrayList源码解析(JDK1.7)
- Java集合框架--LinkedList源码解析(JDK1.7)
- java.util.concurrent.ConcurrentHashMap并发哈希表源码解析
- Java 并发工具包-java.util.concurrent-源码jdk1.7全面解析
- ConcurrentHashMap的jdk1.7源码
- JDK1.7ConcurrentHashMap源码分析
- Java--String源码解析(JDK1.7)
- java.util.concurrent 之ConcurrentHashMap 源码分析
- java concurrentHashMap 源码解析
- ConcurrentHashmap(JDK1.7)
- Java集合: ConcurrentHashMap源码分析 JDK1.8
- JDK1.8源码学习之ConcurrentHashMap.java
- 《Java源码分析》:ConcurrentHashMap JDK1.8
- 《Java源码分析》:ConcurrentHashMap JDK1.8
- 【Java并发】- ConcurrentHashMap原理解析(JDK1.8)
- 面向对象框架封装思路
- Hdu1874 畅通工程续
- 让history显示时间
- java实现正整数分解质因数(改良版)
- struts2之自动匹配<action name="*" class="" method="{1}">
- Java-Concurrent框架--ConcurrentHashMap源码解析(JDK1.7)
- 时间转字符串,时间比较
- 自动切割Nginx日志case
- ajax 发送post请求传参给SpringMVC
- Python与人工神经网络(9)——其他改进技术介绍
- Java核心基础知识 (一)
- 电商峰值系统架构设计
- 使用apr-util中的DBD API
- 面试题整理