1.引子

并发编程中使用HashMap可能导致程序死循环。因为多线程会put方法添加键值对时将导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

另外Hashtable只是简单地使用阻塞式锁(synchronized关键字)来保证线程安全,在并发编程中使用HashTable效率较低。

基于此,ConcurrentHashMap解决了上面的两方面的不足,它能够高效安全地对键值对进行读写操作。

ConcurrentHashMap的锁分段技术可有效提升并发访问率。Hashtable效率低的原因是多个线程竞争同一把锁。如果将容器中的数据划分为不同的部分或段,并为这些不同的段分别分配一把锁,当将线程要访问某数据,只需要竞争此数据所属分段的锁,而其他段的数据还是能被其他线程访问。这样就将锁的粒度细化了,实现了更高效的并发处理。

现在商业市场主要还是使用JDK1.8作为开发、生产环境,这里对ConcurrentHashMap的分析基于JDK1.8。

2 结构

ConcurrentHashMap在JDK1.7和JDK1.8中的内部实现有较大的差异。JDK1.7中,ConcurrentHashMap直接使用Segment类型的数组segments作为分段锁数组(segments是成员变量),同时又是每个分段数据的的容器,每个segments又包含一个Entry数组。可将segments的每个元素看作一个HashMap对象,基于此可进一步想象,ConcurrentHashMap包含多个HashMap,每个HashMap是一个分段数据集,每个HashMap上有一把锁。

而在JDK1.8中,ConcurrentHashMap还是像HashMap一样,使用Node类型数组table作用为哈希表(table是其成员变量),将Node对象作为储存包含键值对节点的容器,不像JDK1.7中可以直接看到RentantLock锁。虽然也定义了Segment静态内部类,但JDK1.8中只有在(反)序列化方法中使用了这个类,只是为了(反)序列化时与JDK1.7相兼容而已。与JDK1.7的版本相比,这里Segment的重要性已经降低太多了,它与ConcurrentHashMap之间的类关系只是依赖而已。

JDK1.7  
JDK1.8

1)常量与成员变量

常量

private static final int MAXIMUM_CAPACITY = 1 << 30;  //table数组的最大长度

private static final int DEFAULT_CAPACITY = 16; //table数组的默认长度

static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; //toArray相关方法,将Cmap转成数组会用到的最大长度

private static final int DEFAULT_CONCURRENCY_LEVEL = 16;//默认并发线程数

private static final float LOAD_FACTOR = 0.75f;//加载因子

static final int TREEIFY_THRESHOLD = 8;//从链表转为红黑树的阀值

static final int UNTREEIFY_THRESHOLD = 6;//从红黑树转化为链表的阀值

static final int MIN_TREEIFY_CAPACITY = 64;//从链表转为红黑树时table的最小长度

private static final int MIN_TRANSFER_STRIDE = 16; //多线程扩容时每个线程处理的最少哈希桶个数

private static int RESIZE_STAMP_BITS = 16;//sizeCtl中生成标记的位数,16位,sizeCtl的高16位是标记位

private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN = -2; // hash for roots of trees
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

RESIZE_STAMP_BITS :是sizeCtl中生成标记的位数,sizeCtl的高16位是标记符。

RESIZE_STAMP_SHIFT :表示获取sizeCtl中标记符所需位移的位数,sizeCtl是int类型(32位),因此需要右移16位(32 - RESIZE_STAMP_BITS)。

MAX_RESIZERS:表示最大的扩容线程数,sizeCtl的低16位是扩容线程数,低16全为1时可得最大值,国此最大扩容线程数是65535

MOVED :转移节点的hash,所有转移节点的hash都是常量-1。

TREEBIN: 红黑树哈希桶的hash,为常量-2。

RESERVED: 临时保存节点的hash,为常量-3。

HASH_BITS:正常节点的hash位,在spread方法中会用到这个常量,最终使正常节点的hash的最高位设为0.

成员变量

   transient volatile Node<K,V>[] table;

    private transient volatile Node<K,V>[] nextTable;

    private transient volatile long baseCount;

    private transient volatile int sizeCtl;

    private transient volatile int transferIndex;

    private transient volatile int cellsBusy;

    private transient volatile CounterCell[] counterCells;

    // views
private transient KeySetView<K,V> keySet;
private transient ValuesView<K,V> values;
private transient EntrySetView<K,V> entrySet;

table :哈希表,其长度始终是2的幂次方。

nextTable: 下个要用到的哈希表,在重新调用哈希表table的容量是会用到且只有此时不为null。这主要在保证resize时并发访问,ConcurrentHashMap还是可用的。

baseCount: 键值对个数的计数器,主要在没有线程竞争的时候使用。

sizeCtl: table初始化和大小调整控制的依据。 如果为负,则table将被初始化或resize:-1用于初始化,若为其它负数时,|sizeCtl|=(1 +正在resize的线程数)。若sizeCtl是非负数且table为null时,它表示数组table初始化时的长度。初始化之后,表示下次要扩容的阀值。

transferIndex: resize时要拆分的nextTable表索引。

cellsBusy: 基于CAS的自旋锁,它会在resize和创建CounterCells时被使用。

counterCells:当有多线程同时添加或移除节点(键值对)时,它的每个元素记录一个线程添加或移除节点的个数。这在并发多线程竞争时,计算真实的键值对个数时会用到这个成员变量。

而keySet 、values、entrySet就是各种视图。

2)静态内部类Node及其子类

Node是一个存储键值对的基本类,正常节点的hash属性是非负数,而它的一些子类的hash属性是负数常量,这非正常子类不保存具体意义的键值对。哈希桶是单向链表时,不使用Node子类,它直接使用Node本类表示。

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
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;
}
//......

ForwardingNode是Node的一个子类,它的hash属性是常量-1,它主要起着指示当前正在扩容的标识性作用,它不保存键值对。若需要查找键值对,需要调用其find方法在Cmp的nextTable上去查找(扩容期间,原table不可用,只能在nextTable上去查找,ForwardingNode重写了父类的find方法)。

static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
//......

ReservationNode是Node的一个子类,它也不保存键值对,没有后继节点,只是为 computeIfAbsent 、compute这两个方法单独设计的类,如果哈希桶是这种类类型,则表明当前正在初始化哈希桶,还没完全将键值对添加到哈希桶中。若要查找键值对,find方法始终返回空。

static final class ReservationNode<K,V> extends Node<K,V> {
ReservationNode() {
super(RESERVED, null, null, null);
} Node<K,V> find(int h, Object k) {
return null;
}
}

TreeBin是Node的另一个子类,它的hash属性是常量-2,它不保存键值对,此类型节点指示当前哈希桶是红黑树结构,它是代表一个红黑树哈希桶,而实际的能存入键值对的红黑树是其属性root, root是红黑树的根节点。若需要查找键值对,需要调用其find方法从红黑树根节点root开始遍历查找。

static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root;
volatile TreeNode<K,V> first;
volatile Thread waiter;
volatile int lockState;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
//......

TreeNode是Node的另一个子类,它是正常节点(hash是非负数),它表示红黑树节点,它可以保存键值对。

static final class TreeNode<K,V> extends Node<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red; TreeNode(int hash, K key, V val, Node<K,V> next,
TreeNode<K,V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
//......
}

3)数据结构

与HashMap类似,ConcurrentHashMap的基本数据结构是"数组+链表(或红黑树)",当哈希桶所含的节点个数达到相应的阀值时,哈希桶的数据结构会进行相应的变化,即单向链表和红黑树进行相互转化。但另外引入了ForwardingNode和ReservationNode、TreeBin这三类节点类型,这三类节点类型都是非正常节点,它们各自的hash属性是负数常量(链表节点Node、红黑树节点TreeNode这两种正常节点的hash属性是非负数变量)。其实这里的红黑树也与HashMap的红黑树不尽相同,这里不是将表示根节点的TreeNode作为哈希桶容器,而是另外用了TreeBin来表示哈希桶,TreeBin的属性root指向了红黑树的根节点,通过这个属性可以找到红黑树。

4)初始化

ConcurrentHashMap有多个构造方法,我们直接研究其参数最多的构造方法。

    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;
}

参数concurrencyLevel代表预估的并发线程数,它会影响table的长度。构造方法首先通过“if (initialCapacity < concurrencyLevel)”确定用户设定的初始容量和并发级别是否匹配。如果inititalCapacity小于concurrencyLevel,就让inititalCapacity重设为concurrencyLevel。如果环境中真的有concurrencyLevel个线程,那么就应该有concurrencyLevel个锁,因为每个锁管理一段数据,那么就至少要有concurrencyLevel个段数据,每段数据至少包含一个哈希桶,也就是说至少table的长度至少要是concurrencyLevel。

根据“thread=loadFactor*capacity”公式可以看出,“(1.0 + (long)initialCapacity / loadFactor)”算出理论上table的最小长度size,这里加"+1"的原因是考虑不能整除时,要保留小数位的值,只能在整数位进一。而这个长度可size可能并是我们真正会使用的table的长度,因为我们要使用按位与的哈希算法来确定table的下标,就必须使table的长度为2的幂次方,而此时的size可能并不是2的幂次方,我们需要进一步处理。tableSizeFor()方法很巧妙使用了位运算,此方法能求出一个大于等于size的最小且是2的幂次方的值cap。如initialCapacity为14,loadFactor为0.75,concurrencyLevel为4,那么size=20,cap=32.所以最终初始化table时,其长度是32.

private static final int tableSizeFor(int c) {
int n = c - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

3 核心API

1)添加键值对

put(K,V)和putIfAbsent(K,V)都是直接调用putVal(K,V,boolean)方法。putVal方法包含了添加键值对的流程框架。

public V put(K key, V value) {
return putVal(key, value, false);
}
public V putIfAbsent(K key, V value) {
return putVal(key, value, true);
}

putVal()方法的主要逻辑:①先确认table是空,若为空则将其初始化。②再根据位运算“(n-1)& hash”取余求出table的索引i,并进一步获取table下标为i的元素f,此f代表一个哈希桶,它一个单向链表或红黑树。③然后再确认f是否为空,若f为空,就使用CAS更新将f初始化。若CAS更新成功,则退出死循环,否则将再次进入“for (Node<K,V>[] tab = table;

ConcurrentHashMap核心源码浅析的更多相关文章

  1. JMeter5.0核心源码浅析[转]

    [转自:https://blog.csdn.net/zuozewei/article/details/85042829] 源码下载地址:https://github.com/apache/jmeter ...

  2. Java内存管理-掌握类加载器的核心源码和设计模式(六)

    勿在流沙筑高台,出来混迟早要还的. 做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 上一篇文章介绍了类加载器分类以及类加载器的双亲委派模型,让我们能够从整体上对类加载器有 ...

  3. HashMap的结构以及核心源码分析

    摘要 对于Java开发人员来说,能够熟练地掌握java的集合类是必须的,本节想要跟大家共同学习一下JDK1.8中HashMap的底层实现与源码分析.HashMap是开发中使用频率最高的用于映射(键值对 ...

  4. Android版数据结构与算法(五):LinkedHashMap核心源码彻底分析

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 上一篇基于哈希表实现HashMap核心源码彻底分析 分析了HashMap的源码,主要分析了扩容机制,如果感兴趣的可以去看看,扩容机制那几行最难懂的 ...

  5. 并发编程之 SynchronousQueue 核心源码分析

    前言 SynchronousQueue 是一个普通用户不怎么常用的队列,通常在创建无界线程池(Executors.newCachedThreadPool())的时候使用,也就是那个非常危险的线程池 ^ ...

  6. iOS 开源库系列 Aspects核心源码分析---面向切面编程之疯狂的 Aspects

    Aspects的源码学习,我学到的有几下几点 Objective-C Runtime 理解OC的消息分发机制 KVO中的指针交换技术 Block 在内存中的数据结构 const 的修饰区别 block ...

  7. Backbone事件机制核心源码(仅包含Events、Model模块)

    一.应用场景 为了改善酷版139邮箱的代码结构,引入backbone的事件机制,按照MVC的分层思想搭建酷版云邮局的代码框架.力求在保持酷版轻量级的基础上提高代码的可维护性.   二.遗留问题 1.b ...

  8. 6 手写Java LinkedHashMap 核心源码

    概述 LinkedHashMap是Java中常用的数据结构之一,安卓中的LruCache缓存,底层使用的就是LinkedHashMap,LRU(Least Recently Used)算法,即最近最少 ...

  9. 3 手写Java HashMap核心源码

    手写Java HashMap核心源码 上一章手写LinkedList核心源码,本章我们来手写Java HashMap的核心源码. 我们来先了解一下HashMap的原理.HashMap 字面意思 has ...

随机推荐

  1. 1552146271@qq.com

    北京时间9月27日早间消息,美国外卖服务DoorDash周四宣布,一项安全漏洞暴露了该公司大约490万客户.商家和送货员的个人数据. 这家总部位于旧金山的公司在一份声明中说,此次泄露的信息可能包括大约 ...

  2. 第1节 storm编程:3、storm的架构模型的介绍

    nimbus:主节点,接收客户端提交的任务,并且分配任务,新的版本当中nimbus已经可以有多个了 zookeeper集群:storm依靠zk来保存一些节点信息,nimbus将分配的任务信息都写入到z ...

  3. java学习-循环结构-查找算法-顺序查找

    今天回顾了简单算法,顺序查找.发现了数组出现重复数字,无法输出第二个位置就跳出循环了. 利用所学知识解决了.放上代码,同时在代码里给大家分享思路. 欢迎大神教导,欢迎指正. ; System.out. ...

  4. (转)C#的 GC工作原理基础

    作为一位C++出身的C#程序员,我最初对垃圾收集(GC)抱有怀疑态度,怀疑它是否能够稳定高效的运作:而到了现在,我自己不得不说我已经逐渐习惯并依赖GC与我的程序“共同奔跑”了,对“delete”这个习 ...

  5. JuJu团队1月3号工作汇报

    JuJu团队1月3号工作汇报 JuJu   Scrum 团队成员 今日工作 剩余任务 困难 飞飞 测试dataloader 将model嵌入GUI 无 婷婷 调试代码 提升acc 无 恩升 -- 写p ...

  6. ElasticSearch学习,入门篇(一)

    概念解析 1.什么是搜索 搜索就是在任何场景下,找寻你想要的信息,这个时候你会输入一段要搜索的关键字,然后期望找到这个关键字相关的有效信息. 2.如果用数据库做搜素会怎么样 select * from ...

  7. 图像检索:CEDD(Color and Edge Directivity Descriptor)算法 颜色和边缘的方向性描述符

    颜色和边缘的方向性描述符(Color and Edge Directivity Descriptor,CEDD) 本文节选自论文<Android手机上图像分类技术的研究>. CEDD具有抽 ...

  8. linux 常用文件命令记录

    服务开启命令 service  服务  start/stop/stauts 查看ip ifconfig 清屏 clear 显示当前所在位置 pwd 切换目录 cd 查看所有文件(包括隐藏) ls -a ...

  9. 二叉树 - DFS与BFS

    二叉树 - DFS与BFS ​ 深度优先遍历 (DFS Depth First Search) 就是一个节点不到头(叶子节点为空) 不回头 ​ 广度有点遍历(BFS Breadth First Sea ...

  10. Vulkan SDK Demo 之一 熟悉

    DiligentEngine的API是D3d11和D3D12风格的,vulkan也被封装成了这种风格的API. 在了解Diligent Engine是如何对vulkan进行封装之前,我准备先学习下Vu ...