深入ConcurrentHashMap一,已经介绍了主要的ConcurrentHashMap的结构,Segment组成,HashEntry的组成以及包含ConcurrentHashMap的创建。

这篇文章主要关注往ConcurrentHashMap放入元素的情况。即put(K key,V value)方法。

ConcurrentHashMap put进一个key,value的简化的过程例如以下:
    1.取key的hash值,算出在存放的Segment数组下标。

    2.找到segment数组下标后,取出这个Segment。然后计算出须要存放在Segment中HashEntry的数组下标
    3.最后将key,value放入

具体步骤如图:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvemhhb3poZW56dW8=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" />

接下去分析源代码。首先是获取要存放元素的segment的源代码,代码例如以下:

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);
return s.put(key, hash, value, false);
}

这里首先会计算key的hash值,hash(key)方法会尽量打散hash,降低hash冲突。
计算出hash后。会使用语句int j = (hash >>> segmentShift) & segmentMask; 来得到segment数组下标。
然后使用UNSAFE.getObject来尝试从segment数组获取segment。

假设为空这时会调用ensureSegment(j)方法创建一个并CAS设置到segment数组其中。

因为相对来讲segment仅仅在第一次不存在的时候才会创建并放入segment数组中,也仅仅有在这一步会发生与其他线程的竞争。
因此对于segment的创建及放入到segment数组其中,ConcurrentHashMap採用的是CAS操作,来原子性的放入segment。这也符合对于少量或
中度并发的情形适合用CAS操作。

有了segment后。调用segment的put方法将元素插入到对应的HashEntry数组其中。

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;
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;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}

这部分因为对于每一个segment的put操作。相对来讲竞争会比較激烈。因此这里会使用lock来进行同步控制。

分下面步骤进行:
1.这里会首先调用ReentrantLock的tryLock方法,看能否够获取到锁。能则进入到第3步。

否则进入第2步。调用scanAndLockForPut获取锁。

2.调用scanAndLockForPut方法尝试获取CAS获取锁。

进入这种方法时先尝试获取一次。假设获取到则立即返回。

否则会进行多次CAS获取锁,
在获取锁的过程中假设要放入的元素在HashEntry数组中对应位置不存在则先创建一个。

源代码例如以下:

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; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}

能够看到一旦retries次数假设超过MAX_SCAN_RETRIES(这里是64次)则会调用ReentrantLock的lock方法堵塞的获取锁(在lock方法中还是会用CAS获取锁。假设还是不能获取到
则将此线程放入CLH队列,最后堵塞。具体见深入并发AQS二)。
比較有意思的是最后一个
else if ((retries & 1) == 0 &&
                         (f = entryForHash(this, hash)) != first)
这部分代码,这里推断假设当前HashEntry要存放位置的首结点,假设有其他线程已经完毕了插入的操作,则会将retries置为-1。

ConcurrentHashMap觉得这样的情况之后会非常快获取到锁。

一直反复CAS获取锁,获取到后返回node。

3.已经获取到锁后,就能够放HashEntry中放入元素了。

这里分两种情况:
 一种是当前HashEntry数组对应位置存在这个key元素,这时会将当前HashEntry的value替换成新的value。
 一种是当前HashEntry数组对应位置不存在这个key元素。这时则会将元素插入到对应位置。

对于于第一种情况的源代码在上述put方法其中,部分片断例如以下:

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) {
//这里假设e不为空。则说明当前HashEntry位置已经元素在,这时遍历这个冲突链,看是否当前key已经存在于HashEntry其中
 K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
                   //当前存在key元素,这时替换它
                   oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//这个分支觉得当前位置HashEntry无元素存在或者当前不存在同样key的HashEntry。                //这时假设前面scanAndLockForPut已经返回创建的HashEntry结点,则直接将这个新node结点的next指针指向HashEntry位置的首结点
 if (node != null)
node.setNext(first);
else
  //node为空则新建一个HashEntry,用于存放key。value
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}

能够看到这里有个优化部分是调用

scanAndLockForPut

方法时。在CAS尝试获取锁时。假设当前要存放元素的HashEntry数组位置没有不论什么元素。这时觉得竞争较少。所以会投机地先创建一个node 用于存放key,value。

否则这时因为不确定是否该创建新的结点,因为有可能key值已经存在。这时其实仅仅须要进行更新就可以。当然也有觉得竞争较激烈的因素存在。

深入ConcurrentHashMap二的更多相关文章

  1. 深入剖析ConcurrentHashMap二

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt200 我们关注的操作有:get,put,remove 这3个操作.对于哈希表 ...

  2. Java笔记(十六)并发容器

    并发容器 一.写时复制的List和Set CopyOnWrite即写时复制,或称写时拷贝,是解决并发问题的一种重要思路. 一)CopyOnWriteArrayList 该类实现了List接口,它的用法 ...

  3. BATJ等大厂最全经典面试题分享

    金九银十,又到了面试求职高峰期,最近有很多网友都在求大厂面试题.正好我之前电脑里面有这方面的整理,于是就发上来分享给大家. 这些题目是网友去百度.蚂蚁金服.小米.乐视.美团.58.猎豹.360.新浪. ...

  4. Mybatis源码分析之Cache二级缓存原理 (五)

    一:Cache类的介绍 讲解缓存之前我们需要先了解一下Cache接口以及实现MyBatis定义了一个org.apache.ibatis.cache.Cache接口作为其Cache提供者的SPI(Ser ...

  5. [集合]线程安全的HashMap

    一.一般模式下线程安全的HashMap 默认情况常用的HashMap都是线程不安全的,在多线程的环境下使用,常常会造成不可预知的,莫名其妙的错误.那么,我们如何实现一个线程安全的HashMap呢?其中 ...

  6. 双列集合Map相关面试题

    一.了解Map集合吗?Map集合都有哪些实现 HashMap HashTable LinkedHashMap TreeMap ConcurrentHashMap 二.HashMap和HashTable ...

  7. 笔记 BAT 面试题 及部分答案

    题目出自:https://www.jianshu.com/p/c70989bd5f29本文出自 AWeiLoveAndroid的博客 2018年2月20日星期二 随笔 笔记 BAT 面试题 一.jav ...

  8. ConcurrentHashMap原理分析(二)-扩容

    概述 在上一篇文章中介绍了ConcurrentHashMap的存储结构,以及put和get方法,那本篇文章就介绍一下其扩容原理.其实说到扩容,无非就是新建一个数组,然后把旧的数组中的数据拷贝到新的数组 ...

  9. java基础 (二)之HashMap,HashTable,ConcurrentHashMap区别

    HashTable: put方法加了同步锁synchronized,底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable, ...

随机推荐

  1. JAVA Eclipse如何重新设置工作空间workspace

    窗口-首选项-常规-启动和关闭,勾选启动时提示工作空间,然后移除现有的工作空间,最好也勾选启动时刷新工作空间   重启之后就可以设置工作空间了  

  2. Centos修改文件打开数限制

    一.查看系统限制最大打开数 cat /proc/sys/fs/file-max 还有一个问题是file-max最大能设置多大呢?一个经验算法是 256个fd 需4M内存.例如8G内存,8*1024/4 ...

  3. ES6 动态计算属性名

    在ES5之前,如果属性名是个变量或者需要动态计算,则只能通过 对象.[变量名] 的方式去访问. <script type="text/javascript"> var ...

  4. 程序猿的量化交易之路(24)--Cointrader之RemoteEvent远程事件实体(11)

    转载需注明出处:http://blog.csdn.net/minimicall,http://cloudtrader.top/ 在量化交易系统中.有些事件是远端传来的,比方股票的价格数据等.所以,在这 ...

  5. Java Swing界面编程(21)---事件处理:窗口事件

    WindowLIstener是专门处理窗口的事件监听窗口.一个窗口的全部变化.如窗口的打开.关闭等都能够使用这个接口进行监听. 实现WIndowListener: package com.beyole ...

  6. [[NSBundle mainBundle] pathForResource:@"name" ofType:@"type"] 找不到对应的文件解决方法

    最近在使用[[NSBundle mainBundle] pathForResource:@"name" ofType:@"type"]时,找不到其对应的文件,文 ...

  7. HTTP ----通信机制

    HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤: (1)    建立TCP连接 在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建立 ...

  8. Linux系统控制文件 /etc/sysctl.conf详解

    /etc/sysctl.conf这个目录主要是配置一些系统信息,/etc/sysctl.conf参数解释: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 ...

  9. 解决Windows平台通过cURL上传APP到蒲公英pgyer平台时无法使用中文升级描述的问题

    解决Windows平台通过cURL上传APP到蒲公英pgyer平台时无法使用中文升级描述的问题 官方上传命令 curl -F file=@"315.apk" -F uKey=XXX ...

  10. Cygwin 版本的 Curl 安装,提取,使用笔记

    Cygwin 版本的 Curl 安装,提取,使用笔记 Cygwin 版本的 Curl 使其恢复 HTTPS 请求功能Cygwin 版本的 Curl 依赖的 DLL 清单提取 Cygwin 版本的 Cu ...