ConcurrentHashMap源码分析2(总结)
来源:互联网 发布:淘宝网风骑士俱乐部 编辑:程序博客网 时间:2024/06/05 03:47
一些问题总结:
最大的分段(segment)数为2的16次方,每一个segment的HashEntry[]的最大容量为2的30次方。
默认的分段数和每个segment的HashEntry[]的初始容量均为16。segment的默认加载因子为0.75。定位segment段需要用的两个参数:segmentMask,segmentShift。
这两个参数在构造函数中进行处理,如下:
//ssize为经传入的参数concurrencyLevel计算得到的segment数组的大小,sshift为移位次数int sshift = 0;int ssize = 1;// segment数组的长度while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1;}segmentShift = 32 - sshift;// eg.segmentShift==32-4=28 segmentMask = ssize - 1;// eg.segmentMask==16-1==15
/** * 根据给定的key的hash值定位到一个Segment * @param hash */ final Segment<K, V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }
3.Segment类(ConcurrentHashMap的内部类):继承ReentrantLock类
属性:
count(用变量volatile修饰),表示该Segment中的包含的所有HashEntry中的key-value的个数。
HashEntry[] table(用变量volatile修饰),表示该Segment中的链表数组。
modCount,并发标记。
threshold,元素个数超出了这个值就扩容 threshold==(int)(capacity * loadFactor),注意是当前segment进行扩容。
4.HashEntry节点的定义:
static final class HashEntry<K, V> { final K key;// 键 final int hash;//hash值 volatile V value;// 实现线程可见性 final HashEntry<K, V> next;// 下一个HashEntry .........}
注意的是:值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。
5.三参构造函数: (int initialCapacity,float loadFactor,int concurrencyLevel)
/** * 创建ConcurrentHashMap * @param initialCapacity 用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量 * @param loadFactor * @param concurrencyLevel 用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数) * * 步骤: * 这里以默认的无参构造器参数为例,initialCapacity==16,loadFactor==0.75f,concurrencyLevel==16 * 1)检查各参数是否符合要求 * 2)根据concurrencyLevel(16),计算Segment[]的容量ssize(16)与扩容移位条件sshift(4) * 3)根据sshift与ssize计算将来用于定位到相应Segment的参数segmentShift与segmentMask * 4)根据ssize创建Segment[]数组,容量为ssize(16) * 5)根据initialCapacity(16)与ssize计算用于计算HashEntry[]容量的参数c(1) * 6)根据c计算HashEntry[]的容量cap(1) * 7)根据cap与loadFactor(0.75)为每一个Segment[i]都实例化一个Segment * 8)每一个Segment的实例化都做下面这些事儿: * 8.1)为当前的Segment初始化其loadFactor为传入的loadFactor(0.75) * 8.2)创建一个HashEntry[],容量为传入的cap(1) * 8.3)根据创建出来的HashEntry的容量(1)和初始化的loadFactor(0.75),计算扩容因子threshold(0) * 8.4)初始化Segment的table为刚刚创建出来的HashEntry */ public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel) { // 检查参数情况 if (loadFactor <= 0f || initialCapacity < 0 || concurrencyLevel <= 0) throw new IllegalArgumentException(); if (concurrencyLevel > MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; /** * 找一个能够正好小于concurrencyLevel的数(这个数必须是2的几次方的数) * eg.concurrencyLevel==16==>sshift==4,ssize==16 * 当然,如果concurrencyLevel==15也是上边这个结果 */ int sshift = 0; int ssize = 1;// segment数组的长度 while (ssize < concurrencyLevel) { ++sshift; ssize <<= 1;// ssize=ssize*2 } segmentShift = 32 - sshift;// eg.segmentShift==32-4=28 用于根据给定的key的hash值定位到一个Segment segmentMask = ssize - 1;// eg.segmentMask==16-1==15 用于根据给定的key的hash值定位到一个Segment this.segments = Segment.newArray(ssize);// 构造出了Segment[ssize]数组 eg.Segment[16] /* * 下面将为segment数组中添加Segment元素 */ if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; int c = initialCapacity / ssize;// eg.initialCapacity==16,c==16/16==1 if (c * ssize < initialCapacity)// eg.initialCapacity==17,c==17/16=1,这时1*16<17,所以c=c+1==2 ++c;// 为了少执行这一句,最好将initialCapacity设置为2的几次方 int cap = 1;// 每一个Segment中的HashEntry[]的初始化容量 while (cap < c) cap <<= 1;// 创建容量 for (int i = 0; i < this.segments.length; ++i) // 这一块this.segments.length就是ssize,为了不去计算这个值,可以直接改成i<ssize this.segments[i] = new Segment<K, V>(cap, loadFactor); }
默认的构造函数:调用上面的构造函数
/** * 创建ConcurrentHashMap */ public ConcurrentHashMap() { this(DEFAULT_INITIAL_CAPACITY, // 16 DEFAULT_LOAD_FACTOR, // 0.75f DEFAULT_CONCURRENCY_LEVEL);// 16 }
注意:
(1)传入的concurrencyLevel只是用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数),并非真正的Segment数组的大小。
(2)传入的initialCapacity只是用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量,而每一个HashEntry[]的容量不是2的几次方。
(3)非常值得注意的是,在默认情况下,创建出的HashEntry[]数组的容量为1,并不是传入的initialCapacity(16),证实了上一点;而每一个Segment的扩容因子threshold,一开始算出来是0,即开始put第一个元素就要扩容,不太理解JDK为什么这样做。
(4)想要在初始化时扩大HashEntry[]的容量,可以指定initialCapacity参数,且指定时最好指定为2的几次方的一个数,这样的话,在代码执行中可能会少执行一句”c++”,具体参看三参构造器的注释。
(5)对于Concurrenthashmap的扩容而言,只会扩当前的Segment,而不是整个Concurrenthashmap中的所有Segment都扩。
6.put函数:put(Object key, Object value)
/** * 将key-value放入map * 注意:key和value都不可以为空 * 步骤: * 1)计算key.hashCode()的hash值 * 2)根据hash值定位到某个Segment * 3)调用Segment的put()方法 * Segment的put()方法: * 1)上锁 * 2)从主内存中读取key-value对个数count * 3)count+1如果大于threshold,执行rehash() * 4)计算将要插入的HashEntry[]的下标index * 5)获取HashEntry的头节点HashEntry[index]-->first * 6)从头结点开始遍历整个HashEntry链表, * 6.1)若找到与key和hash相同的节点,则判断onlyIfAbsent如果为false,新值覆盖旧值,返回旧值;如果为true,则直接返回旧值(相当于不添加重复key的元素) * 6.2)若没有找到与key和hash相同的节点,则创建新节点HashEntry,并将之前的有节点作为新节点的next,即将新节点放入链头,然后将新节点赋值给HashEntry[index],将count强制写入主内存,最后返回null */ public V put(K key, V value) { if (key == null || value == null) throw new NullPointerException(); int hash = hash(key.hashCode());//计算key.hashCode()的hash值 /** * 根据hash值定位到某个Segment,调用Segment的put()方法 */ return segmentFor(hash).put(key, hash, value, false); }
里面的segment的put源码如下:
/** * 往当前segment中添加key-value * 注意: * 1)onlyIfAbsent-->false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素(不可添加重复key的元素) * 2)ReentrantLock的用法 * 3)volatile只能配合锁去使用才能实现原子性 */ V put(K key, int hash, V value, boolean onlyIfAbsent) { lock();//加锁:ReentrantLock try { int c = count;//当前Segment中的key-value对(注意:由于count是volatile型的,所以读的时候工作内存会从主内存重新加载count值) if (c++ > threshold) // 需要扩容 rehash();//扩容 HashEntry<K, V>[] tab = table; int index = hash & (tab.length - 1);//按位与获取数组下标:与HashMap相同 HashEntry<K, V> first = tab[index];//获取相应的HashEntry[i]中的头节点 HashEntry<K, V> e = first; //一直遍历到与插入节点的hash和key相同的节点e;若没有,最后e==null while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue;//旧值 if (e != null) {//table中已经有与将要插入节点相同hash和key的节点 oldValue = e.value;//获取旧值 if (!onlyIfAbsent) e.value = value;//false 覆盖旧值 true的话,就不添加元素了 } else {//table中没有与将要插入节点相同hash或key的节点 oldValue = null; ++modCount; tab[index] = new HashEntry<K, V>(key, hash, first, value);//将头节点作为新节点的next,所以新加入的元素也是添加在链头 count = c; //设置key-value对(注意:由于count是volatile型的,所以写的时候工作内存会立即向主内存重新写入count值) } return oldValue; } finally { unlock();//手工释放锁 } }
注意:
(1)key和value都不可为null,这一点与HashMap不同。
(2)nlyIfAbsent–>false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素
(3)ReentrantLock的用法:必须手工释放锁。可实现Synchronized的效果,原子性。
(4)volatile需要配合锁去使用才能实现原子性,否则在多线程操作的情况下依然不够用,在程序中,count变量(当前Segment中的key-value对个数)通过volatile修饰,实现内存可见性。在有锁保证了原子性的情况下:
a. 当我们读取count变量的时候,会强制从主内存中读取count的最新值;
b. 当我们对count变量进行赋值之后,会强制将最新的count值刷到主内存中去。
通过以上两点,我们可以保证在高并发的情况下,执行这段流程的线程可以读取到最新值。
(5)ConcurrentHashMap基于concurrencyLevel划分出多个Segment来存储key-value,这样的话put的时候只锁住当前的Segment,可以避免put的时候锁住整个map,从而减少了并发时的阻塞现象。
7.rehash操作:在上一步put操作时,可能会引发rehash操作。(非jdk版,自行修改版)
/** * 步骤: * 需要注意的是:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是hash&(table.length-1)的结果相同 * 1)创建一个新的HashEntry数组,容量为旧数组的二倍 * 2)计算新的threshold * 3)遍历旧数组的每一个元素,对于每一个元素(即一个链表) * 3.1)获取头节点e * 3.2)从头节点开始到最后一个节点(null之前的那个节点)的所有节点计算其将要存储的索引k,然后创建新节点,将新节点赋给newTable[k],并将之前newTable[k]上存在的节点作为新节点的下一节点 */ void rehash() { HashEntry<K, V>[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity >= MAXIMUM_CAPACITY) return; HashEntry<K, V>[] newTable = HashEntry.newArray(oldCapacity << 1);//扩容为原来二倍 threshold = (int) (newTable.length * loadFactor);//计算新的扩容临界值 int sizeMask = newTable.length - 1; for (int i = 0; i < oldCapacity; i++) {//遍历每一个数组元素 // We need to guarantee that any existing reads of old Map can // proceed. So we cannot yet null out each bin. HashEntry<K, V> e = oldTable[i];//头节点 if (e != null) { for (HashEntry<K, V> p = e; p != null; p = p.next) {//遍历数组元素中的链表 int k = p.hash & sizeMask; HashEntry<K, V> n = newTable[k];//获取newTable[k]已经存在的HashEntry,并将此HashEntry赋给n //创建新节点,并将之前的n作为新节点的下一节点 newTable[k] = new HashEntry<K, V>(p.key, p.hash, n,p.value); } } } table = newTable; }
注意:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是index = hash&(table.length-1)的结果相同,当table.length发生变化时,同一个桶下各个HashEntry算出来的index会不同。
8.get操作
ConcurrentHashMap的get(Object key)
/** * 根据key获取value * 步骤: * 1)根据key获取hash值 * 2)根据hash值找到相应的Segment * 调用Segment的get(Object key, int hash) * 3)根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index] * 4)遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e, * 4.1)如没找到e,返回null * 4.2)如找到e,获取e.value * 4.2.1)如果e.value!=null,直接返回 * 4.2.2)如果e.value==null,则先加锁,等并发的put操作将value设置成功后,再返回value值 */ public V get(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).get(key, hash); }
Segment的get(Object key, int hash)
/** * 根据key和hash值获取value */ V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K, V> e = getFirst(hash);//找到HashEntry[index] while (e != null) {//遍历整个链表 if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; /* * 如果V等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功, * 这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值 */ return readValueUnderLock(e); } e = e.next; } } return null; }
/** * 根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index] */ HashEntry<K, V> getFirst(int hash) { HashEntry<K, V>[] tab = table; return tab[hash & (tab.length - 1)]; }
V readValueUnderLock(HashEntry<K, V> e) { lock(); try { return e.value; } finally { unlock(); } }
注意:get操作基本不用加锁。这个理解起来需要知道两点:
(1)第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
(2)对得到key相对应的值v是否为null的判断。如果v等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值。
9.remove操作
ConcurrentHashMap的remove(Object key)
/** * 删除指定key的元素 * 步骤: * 1)根据key获取hash值 * 2)根据hash值获取Segment * 调用Segment的remove(Object key, int hash, Object value) * 1)count-1 * 2)获取将要删除的元素所在的HashEntry[index] * 3)遍历链表, * 3.1)若没有hash和key都与指定参数相同的节点e,返回null * 3.2)若有e,删除指定节点e,并将e之前的节点重新排序后,将排序后的最后一个节点的下一个节点指定为e的下一个节点 * (很绕,不知道JDK为什么这样实现) */ public V remove(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).remove(key, hash, null); }
Segment的remove(Object key, int hash, Object value)
V remove(Object key, int hash, Object value) { lock(); try { int c = count - 1;//key-value对个数-1 HashEntry<K, V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K, V> first = tab[index];//获取将要删除的元素所在的HashEntry[index] HashEntry<K, V> e = first; //从头节点遍历到最后,若未找到相关的HashEntry,e==null,否则,有 while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) {//将要删除的节点e V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned. ++modCount; HashEntry<K, V> newFirst = e.next; /* * 从头结点遍历到e节点,这里将e节点删除了,但是删除节点e的前边的节点会倒序 * eg.原本的顺序:E3-->E2-->E1-->E0,删除E1节点后的顺序为:E2-->E3-->E0 * E1前的节点倒序排列了 */ for (HashEntry<K, V> p = first; p != e; p = p.next) newFirst = new HashEntry<K, V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile } } return oldValue; } finally { unlock(); } }
注意:
(1)remove操作需要加锁
(2)之前介绍过HashEntry结构时,它的值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。
10.containsKey(Object key)/keySet().iterator() 不加锁,比较简单,就不贴源码了。
11.size() (jdk1.7)
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 (;;) { //retry两次,RETRIES_BEFORE_LOCK=2 //当两次均是当前modCount与前一次不一致时,则进行加锁统计总数 if (retries++ == RETRIES_BEFORE_LOCK) { 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; } } //若当前modCount总数和前一次modCount总数相同,表示没有其他操作,循环跳出 //否则继续进行下次retry 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; }
注意:
(1)统计所有Segment里元素的大小然后求它们的和,如果直接将各个segment中的count(volatile修饰)相加,这是不可取的。volatile只能保证得到的count是最新的,但是在相加过程中,可能有其他的线程改变count,得到的是不精确的结果
(2)最安全的做法时把segement的put,remove等操作给全部锁住,但是这种方法很低效。
(3)因为在累加count操作过程中,之前累加过的count发生变化的概率太小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
总结
数据结构:一个指定个数的Segment数组,数组中的每一个元素Segment相当于一个HashTable。
加锁情况(锁分离技术):
(1)put
(2)get中找到了hash与key都与指定参数相同的HashEntry,但是value==null的情况
(3)remove
(4)size():两次尝试后,还未成功,遍历所有Segment,分别加锁(即建立全局锁)
http://www.cnblogs.com/java-zhao/p/5113317.html
- ConcurrentHashMap源码分析2(总结)
- Java ConcurrentHashMap 源码分析(2)
- ConcurrentHashMap 源码分析 (一)
- ConcurrentHashMap 源码分析 (一)
- ConcurrentHashMap 源码分析 (一)
- ConcurrentHashMap源码分析(一)
- ConcurrentHashMap源码分析(JDK8版本)
- ConcurrentHashMap源码分析(JDK8版本)
- ConcurrentHashMap源码分析(JDK8版本)
- ConcurrentHashMap源码分析(JDK1.8)
- ConcurrentHashMap 源码分析 (二)
- ConcurrentHashMap源码分析
- ConcurrentHashMap 源码分析
- ConcurrentHashMap源码分析--Java8
- Java源码分析:ConcurrentHashMap
- Java-ConcurrentHashMap源码分析
- ConcurrentHashMap之源码分析
- ConcurrentHashMap源码分析
- 详解PHP文件下载的原理和实现
- opencv挑选指定像素值
- pandas学习转载自http://blog.csdn.net/yhb315279058
- LeetCode: 448. Find All Numbers Disappeared in an Array
- nginx的配置、虚拟主机、负载均衡和反向代理(1)
- ConcurrentHashMap源码分析2(总结)
- WSAStartup( )详解
- MVC简单的工作原理
- 将二维数组排列组合
- GIT 常用命令总结
- yppasswd unknown user (uid=0).
- Connection.setAutoCommit()的使用思考
- 2017年7月,我要学习Unity 3D
- Python 3从入门到精通1-环境安装和运行环境