ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合。对于ConcurrentHashMap是如何提高其效率的,可能大多人只是知道它使用了多个锁代替HashTable中的单个锁,也就是锁分离技术(Lock Stripping)。实际上,ConcurrentHashMap对提高并发方面的优化,还有一些其它的技巧在里面(比如在get操作的时候它是否也使用了锁来保护?)。


相比较与HashMap、HashTable:

对于哈希表,Java中采用链表的方式来解决hash冲突的。
一个HashMap的数据结构看起来类似下图:

实现了同步的HashTable也是这样的结构,它的同步使用锁来保证的,并且所有同步操作使用的是同一个锁对象。这样若有n个线程同时在get时,这n个线程要串行的等待来获取锁。


ConcurrentHashMap中对这个数据结构,针对并发稍微做了一点调整。
它把区间按照并发级别(concurrentLevel),分成了若干个segment。默认情况下内部按并发级别为16来创建。对于每个segment的容量,默认情况也是16。当然并发级别(concurrentLevel)和每个段(segment)的初始容量都是可以通过构造函数设定的。

创建好默认的ConcurrentHashMap之后,它的结构大致如下图:

看起来只是把以前HashTable的一个hash bucket创建了16份而已。有什么特别的吗?没啥特别的。


继续看每个segment是怎么定义的:

static final class Segment<K,V> extends ReentrantLock implements Serializable

Segment继承了ReentrantLock,表明每个segment都可以当做一个锁。这样对每个segment中的数据需要同步操作的话都是使用每个segment容器对象自身的锁来实现。只有对全局需要改变时锁定的是所有的segment。

上面的这种做法,就称之为“分离锁(lock striping)”。有必要对“分拆锁”“分离锁”的概念描述一下:

分拆锁(lock spliting)就是若原先的程序中多处逻辑都采用同一个锁,但各个逻辑之间又相互独立,就可以拆(Spliting)为使用多个锁,每个锁守护不同的逻辑。
分拆锁有时候可以被扩展,分成可大可小加锁块的集合,并且它们归属于相互独立的对象,这样的情况就是分离锁(lock striping)。(摘自《Java并发编程实践》)


get,put,remove这三个函数怎么保证数据同步的:

看上去,单是这样就已经能大大提高多线程并发的性能了。下面继续看我们关注get,put,remove这三个函数怎么保证数据同步的。

先看get方法:

public V get(Object key) {int hash = hash(key); // throws NullPointerException if key nullreturn segmentFor(hash).get(key, hash);
}

它没有使用同步控制,交给segment去找,再看Segment中的get方法:

    V get(Object key, int hash) {if (count != 0) { // read-volatile // ①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;
}

它也没有使用锁来同步,只是判断获取的entry的value是否为null,为null时才使用加锁的方式再次去获取。

这个实现很微妙,没有锁同步的话,靠什么保证同步呢?我们一步步分析。

第一步,先判断一下 count != 0;count变量表示segment中存在entry的个数。如果为0就不用找了。
假设这个时候恰好另一个线程put或者remove了这个segment中的一个entry,会不会导致两个线程看到的count值不一致呢?
看一下count变量的定义: transient volatile int count;
它使用了volatile来修改。Java5之后,JMM实现了对volatile的保证:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
所以,每次判断count变量的时候,即使恰好其他线程改变了segment也会体现出来。

第二步,获取到要该key所在segment中的索引地址,如果该地址有相同的hash对象,顺着链表一直比较下去找到该entry。当找到entry的时候,先做了一次比较: if(v != null) 我们用红色注释的地方。
这是为何呢?

考虑一下,如果这个时候,另一个线程恰好新增/删除了entry,或者改变了entry的value,会如何?

先看一下HashEntry类结构。

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

除了 value,其它成员都是final修饰的,也就是说value可以被改变,其它都不可以改变,包括指向下一个HashEntry的next也不能被改变。(那删除一个entry时怎么办?后续会讲到。)

1) 在get代码的①和②之间,另一个线程新增了一个entry
如果另一个线程新增的这个entry又恰好是我们要get的,这事儿就比较微妙了。

下图大致描述了put 一个新的entry的过程。

因为每个HashEntry中的next也是final的,没法对链表最后一个元素增加一个后续entry
所以新增一个entry的实现方式只能通过头结点来插入了。

newEntry对象是通过 new HashEntry(K k , V v, HashEntry next) 来创建的。如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。

这里会出现我们常讨论的DCL的问题,没有锁同步的话,new 一个对象对于多线程看到这个对象的状态是没有保障的,这里同样有可能一个线程new这个对象的时候还没有执行完构造函数就被另一个线程得到这个对象引用。
所以才需要判断一下:if (v != null) 如果确实是一个不完整的对象,则使用锁的方式再次get一次。

有没有可能会put进一个value为null的entry? 不会的,已经做了检查,这种情况会抛出异常,所以 ②处的判断完全是出于对多线程下访问一个new出来的对象的状态检测。

2) 在get代码的①和②之间,另一个线程修改了一个entry的value
value是用volitale修饰的,可以保证读取时获取到的是修改后的值。

3) 在get代码的①之后,另一个线程删除了一个entry

假设我们的链表元素是:e1-> e2 -> e3 -> e4 我们要删除 e3这个entry
因为HashEntry中next的不可变,所以我们无法直接把e2的next指向e4,而是将要删除的节点之前的节点复制一份,形成新的链表。它的实现大致如下图所示:

如果我们get的也恰巧是e3,可能我们顺着链表刚找到e1,这时另一个线程就执行了删除e3的操作,而我们线程还会继续沿着旧的链表找到e3返回。
这里没有办法实时保证了。

我们第①处就判断了count变量,它保障了在 ①处能看到其他线程修改后的。
①之后到②之间,如果再次发生了其他线程再删除了entry节点,就没法保证看到最新的了。

不过这也没什么关系,即使我们返回e3的时候,它被其他线程删除了,暴漏出去的e3也不会对我们新的链表造成影响。

这其实是一种乐观设计,设计者假设 ①之后到②之间 发生被其它线程增、删、改的操作可能性很小,所以不采用同步设计,而是采用了事后(其它线程这期间也来操作,并且可能发生非安全事件)弥补的方式。
而因为其他线程的“改”和“删”对我们的数据都不会造成影响,所以只有对“新增”操作进行了安全检查,就是②处的非null检查,如果确认不安全事件发生,则采用加锁的方式再次get。

这样做减少了使用互斥锁对并发性能的影响。可能有人怀疑remove操作中复制链表的方式是否代价太大,这里我没有深入比较,不过既然Java5中这么实现,我想new一个对象的代价应该已经没有早期认为的那么严重。

我们基本分析完了get操作。对于put和remove操作,是使用锁同步来进行的,不过是用的ReentrantLock而不是synchronized,性能上要更高一些。它们的实现前文都已经提到过,就没什么可分析的了。

我们还需要知道一点,ConcurrentHashMap的迭代器不是Fast-Fail的方式,所以在迭代的过程中别其他线程添加/删除了元素,不会抛出异常,也不能体现出元素的改动。但也没有关系,因为每个entry的成员除了value都是final修饰的,暴漏出去也不会对其他元素造成影响。

ConcurrentHashMap的锁相关推荐

  1. 【转】ConcurrentHashMap分段锁原理

    前言:在分析ConcurrentHashMap的源码的时候,了解到这个并发容器类的加锁机制是基于粒度更小的分段锁,分段锁也是提升多并发程序性能的重要手段之一. 在并发程序中,串行操作是会降低可伸缩性, ...

  2. concurrenthashmap是什么锁_多线程为什么要用ConcurrentHashMap

    今天我们要说的是关于集合的问题,实际上跟锁也有一定的关系,让我们来一起看看吧. 一.简介 1.是什么 ConcurrentHashMap是Java5中新增加的一个线程安全的Map集合,可以用来替代Ha ...

  3. 阿里二面:redis分布式锁过期了但业务还没有执行完,怎么办

    面试官:你们系统是怎么实现分布式锁的? 我:我们使用了redis的分布式锁.具体做法是后端接收到请求后加入一个分布式锁,如果加锁成功,就执行业务,如果加锁失败就等待锁或者拒绝请求.业务执行完成后释放锁 ...

  4. 我使出这“三板斧”(分段锁、哈希锁、弱引用锁)灭霸跑了......

    有同学说,学了Java那么多锁,还是没能锁住灭霸,本文教你"三板斧",锁灭霸足矣. 据说,没几个人能真正参透这"三板斧"的精髓,你是不是那个有缘人呢? 最近,在 ...

  5. Java集合,ConcurrentHashMap底层实现和原理(常用于并发编程)

    为什么80%的码农都做不了架构师?>>>    概述 ConcurrentHashMap常用于并发编程,这里就从源码上来分析一下ConcurrentHashMap数据结构和底层原理. ...

  6. 一篇blog带你了解java中的锁

    前言 最近在复习锁这一块,对java中的锁进行整理,本文介绍各种锁,希望给大家带来帮助. Java的锁 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人 ...

  7. 如何基于 String 实现同步锁?

    作者:等你归去来 https://www.cnblogs.com/yougewe/p/11573911.html 如何基于String实现同步锁? 在某些时候,我们可能想基于字符串做一些事情,比如:针 ...

  8. java concurrentmap原理_Java集合番外篇 -- ConcurrentHashMap底层实现和原理

    概述 距离上一次集合篇结束已经过了好久了, 之前说要写一下番外,但是太忙了,总也找不出相对松散的时间,也有点静不下心来,最近花了点时间,于是便有了这篇博客. 在开始之前先介绍一个算法, 这个算法和Co ...

  9. ConcurrentHashMap 原理解析

    了解ConcurrentHashMap 实现原理,建议首先了解下HashMap实现原理. HashMap 源码解析(JDK1.8) 为什么要用ConcurrentHashMap     HashMap ...

  10. 分布式锁(Redisson)-从零开始,深入理解与不断优化

    分布式锁场景 互联网秒杀 抢优惠卷 接口幂等性校验 案例1 如下代码模拟了下单减库存的场景,我们分析下在高并发场景下会存在什么问题 package com.wangcp.redisson;import ...

最新文章

  1. 读书笔记--C陷阱与缺陷(三)
  2. 程序员经常去的 14 个顶级开发者社区(转)
  3. linux挂载windows共享的文件夹
  4. PostgreSQL和Kingbase中设置search_path
  5. java静态初始化块的作用_Java 中的 static 使用之静态初始化块
  6. 弹出框 每次打开 滚动条置顶_微信置顶文字怎么弄?微信置顶一句话教程
  7. mysqlbackup 重建带有gtid特性的slave
  8. 机器学习5-支持向量机
  9. SQL Server BI Step by Step SSIS 5 --- 通过Email发送查询结果
  10. 8代cpu能装linux 系统吗,Intel支持八九代酷睿的B365芯片组将登场亮相
  11. 洛谷OJ_P1009涉及的高精度算法
  12. Php把ts转为mp4,ts文件转换为mp4文件软件电脑版下载
  13. java导出带图片excel
  14. ECCV 2020预会议 直播笔记| Cross-Modal Weighting Network for RGB-D Salient Object Detection
  15. 解决问题最重要的习惯不是一直盯着屏幕和编写修改代码,某些时候,阻止你成功的东西恰恰会是过于努力。这时候你需要暂停一下,平缓你的思绪,换一种方法或许能带给你不一样的效果。
  16. rabbitmq配置guest用户远程访问失败
  17. golang runtime.Caller 学习笔记
  18. 如何加声调口诀_小学拼音大全含:记忆口诀.拼读.书写.标调规则
  19. 百度网盘会员怎么购买最便宜
  20. JS - 将tree(树形)数据结构格式改为一维数组对象格式(扁平化)

热门文章

  1. STM32USB鼠标+键盘串口控制
  2. Part2 正交、行列式、特征值
  3. HttpClient-HttpClient4.5使用代理服务器访问外网
  4. 华硕笔记本BIOS设置禁用UEFI后使用U盘装系统方法
  5. 我真的不懂微信营销(一)
  6. 修复0xc0000034的经历
  7. Ubuntu 15.10 x64 安装 Android SDK
  8. 中兴olt xpon开局及业务配置以及原理
  9. 2022-01-24:K 距离间隔重排字符串。 给你一个非空的字符串 s 和一个整数 k,你要将这个字符串中的字母进行重新排列,使得重排后的字符串中相同字母的位置间隔距离至少为 k。 所有输入的字符串
  10. Kafka之sync、async以及oneway