先来看ConcurrentHashMap类的成员。

static final int DEFAULT_INITIAL_CAPACITY = 16;static final float DEFAULT_LOAD_FACTOR = 0.75f;static final int DEFAULT_CONCURRENCY_LEVEL = 16;static final int MAXIMUM_CAPACITY = 1 << 30;static final int MIN_SEGMENT_TABLE_CAPACITY = 2;static final int MAX_SEGMENTS = 1 << 16; static final int MAX_SCAN_RETRIES =Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

以上分别是默认的map的容量,默认负载因子,默认分段锁数量,map最大数量,每个分段锁最小容量,最大分段锁数量。最后一个代表当一个线程在试图往一个分段锁存数据的时候如果在一开始取得锁失败之后,再建立键值对的基础下循环往复尝试获得锁的最大尝试次数。

final int segmentMask;final int segmentShift;final Segment<K,V>[] segments;transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
transient Collection<V> values;

在这个类中,在得到key的hash之后,会取hash的高segementShift位与segementMask按位相与,所得到的结果将会作为来选择map中分段锁的依据。

Segments便是map中存储分段锁的数组。

接下来几个成员暂不赘述。

在map中通过内部类HashEntry来存储键值对。以下是HashEntry的成员。

final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;

在Java1.7的ConcurrentHashMap中通过链表来解决碰撞的问题。所以next指向链表下的下一个冲突键值对。

有意思的地方在于,我们可以看entryAt()方法,从字面意思就可以很清楚的理解为直接从所需要查找的HashEntry数组中取得相应下标的键值对,但是实现非常有意思。

static final <K,V> HashEntry<K,V> entryAt(HashEntry<K,V>[] tab, int i) {return (tab == null) ? null :(HashEntry<K,V>) UNSAFE.getObjectVolatile(tab, ((long)i << TSHIFT) + TBASE);
}

在这其中通过unsafe在静态块中的配置直接对内存地址偏移量进行操作。在接下来ConcurrentHashMap有大量这样的操作。

private static final sun.misc.Unsafe UNSAFE;
private static final long SBASE;
private static final int SSHIFT;
private static final long TBASE;
private static final int TSHIFT;
private static final long HASHSEED_OFFSET;
private static final long SEGSHIFT_OFFSET;
private static final long SEGMASK_OFFSET;
private static final long SEGMENTS_OFFSET;static {int ss, ts;try {UNSAFE = sun.misc.Unsafe.getUnsafe();Class tc = HashEntry[].class;Class sc = Segment[].class;TBASE = UNSAFE.arrayBaseOffset(tc);SBASE = UNSAFE.arrayBaseOffset(sc);ts = UNSAFE.arrayIndexScale(tc);ss = UNSAFE.arrayIndexScale(sc);HASHSEED_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("hashSeed"));SEGSHIFT_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentShift"));SEGMASK_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segmentMask"));SEGMENTS_OFFSET = UNSAFE.objectFieldOffset(ConcurrentHashMap.class.getDeclaredField("segments"));} catch (Exception e) {throw new Error(e);}if ((ss & (ss-1)) != 0 || (ts & (ts-1)) != 0)throw new Error("data type scale not a power of two");SSHIFT = 31 - Integer.numberOfLeadingZeros(ss);TSHIFT = 31 - Integer.numberOfLeadingZeros(ts);
}

以上是unsafe在静态块中通过本地方法对内存地址偏移量的设置。

接下来可以看ConcurrentHashMap中作为内部类的分段锁Segment的分析。

Segment直接继承了ReentrantLock类,可以直接作为锁来执行加锁解锁操作。同时,可以看下面分段锁中的成员。

transient volatile HashEntry<K,V>[] table;transient int count;transient int modCount;transient int threshold;final float loadFactor;

table则是分段锁中存放键值对的散列数组。

count作为分段锁的元素的个数。

modCount作为分段锁中数据被修改的次数。

threshold代表分段锁中容量乘负载因子的值,当分段锁中的元素超过这个值就会尝试给这个分段锁扩容。

LoadFactor表示前面提到的负载因子。

可以直接从ConcurrentHashMap的构造方法开始看起。

public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();if (concurrencyLevel > MAX_SEGMENTS)concurrencyLevel = MAX_SEGMENTS;int sshift = 0;int ssize = 1;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;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;
}

构造方法分别需要三个参数,分别是初始化的容量,负载因子,并发度。

这三个参数均是非负的,否则将会直接抛出异常。并发度不能大于最大分段锁数量,否则将会直接赋为最大分段锁数量。Map的初始化容量也是如此。

接下来使得map中分段锁的数量是2的第一个大于并发锁数量的次方值。在不断扩大分段锁的数量同时不断扩大在定位分段锁的位移量。

接下来给segmentShift和segmentMask依次根据之前并发度得到的结果进行赋值。

举个例子,当并发度为31的情况下,segmentMask为31,segmentShift为5,map中一共存在32个分段锁,在接下来的时候将key所得到的hash高5位与11111按位相与所得到的结果(一共有32中可能)来定位要存放的分段锁。

接下来根据分段锁的数量以及map的初始化容量来确定每个分段锁的容量。

接下来初始化第一个分段锁(负载因子,每个分段锁容量与负载因子相乘的结果,每个分段锁容量的键值对数组)。

初始化完毕segment数组后将第一个分段锁放进去,构造方法就结束了。

接下来是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           (segments, (j << SSHIFT) + SBASE)) == null) s = ensureSegment(j);return s.put(key, hash, value, false);
}

在获得key的hash之后取得hash的高segmentShift位与segmentMask按位相与(前面已经说明过),根据结果直接根据分段锁数组的内存地址偏移量取得所需要的分段锁。如果分段锁还没有初始化,则调用ensureSegment()方法确保该位置的分段锁建立以及初始化。

private Segment<K,V> ensureSegment(int k) {final Segment<K,V>[] ss = this.segments;long u = (k << SSHIFT) + SBASE; Segment<K,V> seg;if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {Segment<K,V> proto = ss[0]; int cap = proto.table.length;float lf = proto.loadFactor;int threshold = (int)(cap * lf);HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) { Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) {if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))break;}}}return seg;
}

在构造方法中,我们只建立了分段锁数组的第一个分段锁,与此同时,在这里,我们将以第一个分段锁作为原型将其的信息作为依据建立新的分段锁。

在定位到了所要存放的分段锁的位置后,调用该分段锁的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;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);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;if (c > threshold && tab.length < MAXIMUM_CAPACITY)rehash(node);elsesetEntryAt(tab, index, node);++modCount;count = c;oldValue = null;break;}}} finally {unlock();}return oldValue;
}

在一开始就尝试加锁,如果加锁失败,则会调用scanAndLockForPut()方法来先找到分段锁中相应的位置建立键值对,再试图取得锁的过程。具体方法如下。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {HashEntry<K,V> first = entryForHash(this, hash);HashEntry<K,V> e = first;HashEntry<K,V> node = null;int retries = -1; while (!tryLock()) {HashEntry<K,V> f; if (retries < 0) {if (e == null) {if (node == null)  node = new HashEntry<K,V>(hash, key, value, null);retries = 0;}else if (key.equals(e.key))retries = 0;elsee = e.next;}else if (++retries > MAX_SCAN_RETRIES) {lock();break;}else if ((retries & 1) == 0 &&(f = entryForHash(this, hash)) != first) {e = first = f; retries = -1;}}return node;
}

在这期间,一旦尝试取得锁的次数超过了最大的数量,则会直接取得锁,执行接下来的操作,值得注意的是,如果在这期间,相应分段锁上的hash位置上的头结点如果发生了变化,则会导致尝试次数重置。

在成功取得锁之后,则会根据键值对数组的长度减一与hash按位相与确定在键值对数组上的位置。如果已经有在该位置的结点,则直接把新节点放在整个链表的后面(如果key已经存在,则更新value的值,并且modCount+1)。在加入节点的过程中如果在尝试取得锁的过程中没有新建立节点,则需要重新建立节点,如果建立了,则设置该节点的前驱节点。此时如果个数已经大于分段锁的容量乘负载因子同时键值对数组的长度大还是小于最大容量的,则需要调用rehash()进行分段锁的扩容操作。

private void rehash(HashEntry<K,V> node) {HashEntry<K,V>[] oldTable = table;int oldCapacity = oldTable.length;int newCapacity = oldCapacity << 1;threshold = (int)(newCapacity * loadFactor);HashEntry<K,V>[] newTable =(HashEntry<K,V>[]) new HashEntry[newCapacity];int sizeMask = newCapacity - 1;for (int i = 0; i < oldCapacity ; i++) {HashEntry<K,V> e = oldTable[i];if (e != null) {HashEntry<K,V> next = e.next;int idx = e.hash & sizeMask;if (next == null)    newTable[idx] = e;else { HashEntry<K,V> lastRun = e;int lastIdx = idx;for (HashEntry<K,V> last = next;last != null;last = last.next) {int k = last.hash & sizeMask;if (k != lastIdx) {lastIdx = k;lastRun = last;}}newTable[lastIdx] = lastRun;for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {V v = p.value;int h = p.hash;int k = h & sizeMask;HashEntry<K,V> n = newTable[k];newTable[k] = new HashEntry<K,V>(h, p.key, v, n);}}}}int nodeIndex = node.hash & sizeMask; node.setNext(newTable[nodeIndex]);newTable[nodeIndex] = node;table = newTable;
}

扩容首先直接将原先的分段锁容量往左移一位,建立新的长度为新容量的键值对数组,根据新容量来确定新的与负载因子相乘的结果来控制下次扩容操作。取容量减一作为标准来控制老节点在新键值对数组的位置。

举个例子,一个键值对的hash为7,原容量为16,原来由于7(0111)与15(1111)按位相与的结果为0111,结果就是在扩容前的键值对数组的7上。那么扩容后,容量左移一位变为32,7(00111)与31(11111)按位相与之后仍旧在原位置。但是有一个键值对hash为55,容量16的条件下,55(1110111)与15(001111)按位相与结果则是0111,则也是在7上,但扩容为32后,55(110111)与31(011111)按位相与结果则是10111,在23(10000+ 00111)上。

具体是否需要更换位置以该节点的上一个key为基准,防止第一个key过于特殊。

完成旧键值对数组中的成员往新的键值对数组的转移,将所要放入的新节点放入新的数组的相应位置。

最后解锁,整个步骤宣告完成。

public V get(Object key) {Segment<K,V> s; 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()方法都没有加锁,只是在之前put()根据hash存数据的前提下根据hash取数据。很简单。

Java1.7ConcurrentHashMap类源码解析相关推荐

  1. java.lang 源码剖析_java.lang.Void类源码解析

    在一次源码查看ThreadGroup的时候,看到一段代码,为以下: /* * @throws NullPointerException if the parent argument is {@code ...

  2. Node 学习六、核心模块 events之 01 events 和 EventEmitter 类、发布订阅、EventEmitter 类源码解析和模拟实现

    events 事件模块 events 与 EventEmitter node.js 是基于事件驱动的异步操作架构,内置 events 模块 events 模块提供了 EventEmitter 类 这个 ...

  3. Scroller类源码解析及其应用(一)

    滑动是我们在自定义控件时候经常遇见的难题,让新手们倍感困惑,这篇文章主要介绍Scroller类的源码,告诉打击这个到底有什么用,怎么使用它来控制滑动.另外,我还会结合一个简单的例子,来看一下这个类的应 ...

  4. Java FileReader InputStreamReader类源码解析

    FileReader 前面介绍FileInputStream的时候提到过,它是从文件读取字节,如果要从文件读取字符的话可以使用FileReader.FileReader是可以便利读取字符文件的类,构造 ...

  5. Java String类源码解析

    String直接继承Object 含有一个char[] value,还有一个int hash默认值为0 new String()的构造产生的是一个值为""的字符数组 String( ...

  6. Java集合---Arrays类源码解析

    一.Arrays.sort()数组排序 Java Arrays中提供了对所有类型的排序.其中主要分为Primitive(8种基本类型)和Object两大类. 基本类型:采用调优的快速排序: 对象类型: ...

  7. Unsafe类源码解析

    前言 Unsafe,顾名思义,一个不安全的类,那么jdk的开发者为什么要设计一个不安全的类呢?这个类为什么会不安全呢?现在就让我们来揭开Unsafe类的神秘面纱. 1.概述 作为java开发者的我们都 ...

  8. 【多线程】ThreadPoolExecutor类源码解析----续(二进制相关运算)

    前言 在之前阅读 ThreadPoolExecutor 源码的时候,发现代码里用到了一些二进制相关的位运算之类的代码,看起来有些费劲了,所以现在大概总结了一些笔记,二进制这东西吧,不难,就跟数学一样, ...

  9. Java代码覆盖率框架JaCoCo的core-instr core.internal.instr 包类源码解析

    对类的植入锁定进行判断 几个可以对覆盖率跟踪的Java类定义进行instrument的API public byte[] instrument(final ClassReader reader) {f ...

最新文章

  1. VMM2012应用指南之4-向VMM中添加Hyper-V主机与应用服务器
  2. Python编程基础:第三十三节 文件复制Copy a File
  3. 为什么你需要将代码迁移到ASP.NET Core 2.0?
  4. 数据结构与算法--数组中的逆序对
  5. python 教材为什么喜欢用spam举例_斯坦福大学教授列举出的python入门最容易犯的错误,你中招了吗?...
  6. c# 跨线程访问窗体UI
  7. 毕啸南专栏 | 对话姚星:腾讯有后来居上的传统,我们的战略是全民AI
  8. 蜻蜓fm收音机电脑版_追寻逝去的时光:Tivoli Audio M1BT收音机蓝牙音箱体验
  9. ubuntu 修改ip后,老ip仍然存在的问题
  10. 云计算大败局:基因与宿命
  11. VC下揭开“特洛伊木马”的隐藏面纱
  12. TI单芯片毫米波雷达代码走读(十七)—— 恒虚警(CFAR)检测浅谈
  13. 消费品企业,会员营销四大痛点
  14. ChinaSoft 论坛巡礼 | CCF-华为胡杨林基金-系统软件专项论坛
  15. Git基础之(三)——时光穿梭机
  16. eDairy-我的白日梦
  17. 配置对即时负载的优化
  18. 从“小时购”看京东即时零售的新野望
  19. 斯坦福UE4 + C++课程学习记录 18:十字准星
  20. iOS开发之弱网测试

热门文章

  1. leetcodeT14-最长公共前缀(两种解法+图解)
  2. c++——抽象类以及string知识点补充
  3. vfp报表纸张设置_VFP 9.0中实现多种自定义纸张格式的报表打印
  4. tomcat顶层架构
  5. 企业局域网内如何跨网安全传输数据
  6. cocos2d-x中使用Http
  7. 如何在eclipse中使用XYLayout布局?在此介绍如何把XYLayout导入到eclipse .
  8. MS SQL 监控数据/日志文件增长
  9. 无法删除sqlserver的jobs的方式
  10. 使用XStream对Java对象进行序列化和反序列化