我们都知道HashTable是线程安全的类,因为使用了Synchronized来锁整张Hash表来实现线程安全,让线程独占;

ConcurrentHashMap的锁分离技术就是用多个锁来控制对Hash表的不同部分进行修改,因为我可能只需要对一小块部分进行操作,而如果锁整张表开销太大了,其内部实现就是用Semgent来控制的,每个Semgent都是一个小的HashTable,他们有自己的锁;

然后有些方法需要访问整个表,比如Size,ContainsValue肯定是要访问整个表的数据,这个时候就需要锁整张表,ConcurrentHashMap就会按顺序锁定每个Segment,操作完毕之后再按顺序释放每个Segment,按顺序是为了防止出现死锁;

引用一张图片解释ConcurrentHashMap的结构:

可以看出就是在HashMap的基础上多了一个数组(暂且这样理解),数组的每个元素就是Segment;Segment<K,V> extends ReentrantLock implements Serializable;

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;
// Find power-of-two sizes best matching arguments
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;
// 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); // ordered write of segments[0]
this.segments = ss;
}

ConcurrentHahsMap进行Put操作的时候,对Key需要进行3次Hash运算才能确定最终的位置:hash1(key) -> x1(得到一个值);hash2(x1) -> x2(确定Segment位置,第二次hash是为了减少hash碰撞);hash3(x2) -> x3(确定元素放入哪个HashEntry);这里只是用比较好理解的白话说,下面会说原理;

ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点);

 static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}

可以看出,除了value不是final,其next属性都是final,说明不能从hash链的中间或尾部添加或删除节点;对于Put操作,可以一律将元素添加到hash链的头部,而对于remove操作,从中间删除一个节点,将该节点的前面所有节点复制一份并指向删除节点的下一节点;这会产生两条Hash链,这也是为了保证多线程访问的同步性;将value设置成volatile,这样避免了加锁;因为一个线程在执行get,另一个线程在执行remove,remove还没有执行完的时候,get能看到remove之前的值,如下图:

下面说说Segment的数据结构:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
/**
* count用来统计该段数据的个数,它是volatile,如增加/删除节点,都要写count值,每次读取操作开始都要读取count的值。
*/
transient volatileint count;
/**
* modCount统计段结构改变的次数,主要是为了检测对多个段进行遍历过程中某个段是否发生改变
*/
transient int modCount;
/**
* rehash界限值
*/
transient int threshold;
/**
* 就是上面的HashEntry
*/
transient volatile HashEntry<K,V>[] table;
/**
* 负载因子
*/
final float loadFactor;
}

ConcurrentHashMap的get方法是直接调用Segment的get方法:

public V get(Object key) {
int hash = hash(key); // throws NullPointerException if key null
return segmentFor(hash).get(key, hash);
}
V get(Object key, int hash) {
if (count != 0) { // read-volatile 当前桶的数据个数是否为0
HashEntry<K,V> e = getFirst(hash); 得到头节点
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
 V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}

get方法是不需要加锁的,因为上面已经看到了count和HashEntry都是volatile;

源码中增加value可能为null的情况,是为了保险起见,当节点正在发生改变而又未锁定时就可能为空,而HashEntry中的value不是final的,所以个人认为是为了加此判断;

如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景;

接下来看看Put操作:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
int c = count;
if (c++ > threshold) // ensure capacity
rehash();
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) {
oldValue = e.value;
if (!onlyIfAbsent)
e.value = value;
}
else {
oldValue = null;
++modCount;
tab[index] = new HashEntry<K,V>(key, hash, first, value);
count = c; // write-volatile
}
return oldValue;
} finally {
unlock();
}
}

先上个锁,做个预判,容量+1的时候需不需要扩容,这个判断相比于HashMap做的非常精妙,因为HashMap是在增加元素之后才判断是否需要扩容,如果我扩容之后不增加元素,就白扩容了;

通过索引定位到HashEntry,判断如果不为空,替换节点的value,否则new一个Entry,将此Entry插入到链头,由构造函数得知,它的next则是first;

扩容默认也是会创建两倍容量的数组,进行计算将原数组的数据插入到新数组,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容;

ConcurrentHashMap的size方法

需要统计所有segment的元素总和,为了不影响性能,它会先2次尝试不锁住segment来统计大小,如果统计的过程中count发生了变化,则会把所有的segment的put和remove方法全部锁住;通过modCount来协助判断容器是否发生变化;统计size前后判断modCount是否发生变化即可;

参考:http://www.cnblogs.com/ITtangtang/p/3948786.html

浅谈ConcurrentHashMap实现原理的更多相关文章

  1. 浅谈html运行原理

    浅谈HTML运行原理,所谓的HTML简单的来说就是一个网页,虽然第一节就讲html原理可能大家会听不懂,就当是给一个初步印象把,至少大概知道一个网页的运行流程是怎样的,下面上一张图: 大致的一个htm ...

  2. 浅谈React工作原理

    浅谈React工作原理:https://www.cnblogs.com/yikuu/p/9660932.html 转自:https://cloud.tencent.com/info/63f656e0b ...

  3. [iOS]浅谈NSRunloop工作原理和相关应用

    一. 认识NSRunloop  1.1 NSRunloop与程序运行 那么具体什么是NSRunLoop呢?其实NSRunLoop的本质是一个消息机制的处理模式.让我们首先来看一下程序的入口——main ...

  4. 【JDK源码分析】浅谈HashMap的原理

    这篇文章给出了这样的一道面试题: 在 HashMap 中存放的一系列键值对,其中键为某个我们自定义的类型.放入 HashMap 后,我们在外部把某一个 key 的属性进行更改,然后我们再用这个 key ...

  5. 浅谈 pid的原理与差异

    pid  官方语言就是:比例   积分 微分.究其本质意义,比例到底是什么,原理是什么,这三个到底如何在物理世界这种运作的,大概了解的人又很少.过惯了拿起数据公式无脑推的日子的人更是如此,数学公式是很 ...

  6. 浅谈 foreach 的原理

    package com.shenzhou; import java.util.ArrayList; import java.util.Iterator; import java.util.List; ...

  7. 浅谈一致性Hash原理及应用

    在讲一致性Hash之前我们先来讨论一个问题. 问题:现在有亿级用户,每日产生千万级订单,如何将订单进行分片分表? 小A:我们可以按照手机号的尾数进行分片,同一个尾数的手机号写入同一片/同一表中. 大佬 ...

  8. 浅谈P2P终结者原理及其突破

    P2P终结者按正常来说是个很好的网管软件,但是好多人却拿它来,恶意的限制他人的流量,使他人不能正常上网,下面我们就他的功能以及原理还有突破方法做个详细的介绍! 我们先来看看来自在网上PSP的资料:P2 ...

  9. 10分钟浅谈CSRF突破原理,Web安全的第一防线!

    CSRF攻击即跨站请求伪造(跨站点请求伪造),是一种对网站的恶意利用,听起来似乎与XSS跨站脚本攻击有点相似,但实际上彼此相差很大,XSS利用的是站点内的信任用户,而CSRF则是通过伪装来自受信任用户 ...

随机推荐

  1. Java字符串工具类

    import java.io.ByteArrayOutputStream;import java.io.UnsupportedEncodingException;import java.lang.re ...

  2. crm动态载入js库

    function load_script(url) {     var xmlHTTPRequest;     if (window.ActiveXObject) {         xmlHTTPR ...

  3. Struts2注解开发

    Hibernate和spring框架的开发前边总结了,这次看一下流行的MVC流程框架Struts2的注解开发吧.Struts2主要解决了从JSP到Action上的流程管理,如何进行Uri和action ...

  4. UVa 10491 - Cows and Cars(全概率)

    链接: https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...

  5. BZOJ2654:tree(最小生成树,二分)

    Description 给你一个无向带权连通图,每条边是黑色或白色.让你求一棵最小权的恰好有need条白色边的生成树. 题目保证有解. Input 第一行V,E,need分别表示点数,边数和需要的白色 ...

  6. Educational Codeforces Round 56 (Rated for Div. 2) D. Beautiful Graph 【规律 && DFS】

    传送门:http://codeforces.com/contest/1093/problem/D D. Beautiful Graph time limit per test 2 seconds me ...

  7. Entity Framework——性能测试

    内容提要 一.对EF框架的性能测试 增.删.改,查测试及性能优化 二.使用sql执行 增.删.改,查测试 三.对以上两种方式对比分析 一 对EF框架的测试 1插入操作测试 测试代码(关键部分) Lis ...

  8. Semi-Supervised Dimensionality Reduction

    今天阅读了一篇关于半监督降维的论文,做个总结.这篇论文的全名叫<Semi-Supervised Dimensionality Reduction>(2006),是南大周志华老师的大作. 本 ...

  9. zabbix-agent安装

    1.下载yum源库 rpm -i http://repo.zabbix.com/zabbix/3.4/rhel/7/x86_64/zabbix-release-3.4-2.el7.noarch.rpm ...

  10. Lua库-bit32库

    Global = Global or {}; local bits = {}; function bits.bxor(num1,num2) local ret=bit32.bxor(num1,num2 ...