`
alphafox
  • 浏览: 17950 次
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

concurrentHashMap 到底采取了什么措施使得它比synchronized(HashMap)好

 
阅读更多

免锁容器背后的通用策略:

1使用Lock机制 而不是synchronized

2 对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。(volatile)

3 修改是在容器数据结构的某一部分的一个单独副本上执行的,这样就能避免锁住整个容器

Java Memory Model中,Memory分为两类,

main memory和working memory,main memory为所有线程共享,working memory中存放的是线程所需要的变量的拷贝(线程要对main memory中的内容进行操作的话,首先需要拷贝到自己的working memory,一般为了速度,working memory一般是在cpu的cache中的)。

volatile的变量在被操作的时候不会产生working memory的拷贝,而是直接操作main memory,当然volatile虽然解决了变量的可见性问题,但没有解决变量操作的原子性的问题,这个还需要synchronized或者CAS相关操作配合进行。

关于ConcurrentHashMap

http://www.iteye.com/topic/1103980

ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和HashTable功能相同但是线程安全的方法。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

ConcurrentHashMap的内部结构

ConcurrentHashMap为了提高本身的并发能力,在内部采用了一个叫做Segment的结构,一个Segment其实就是一个类Hash Table的结构,Segment内部维护了一个链表数组,我们用下面这一幅图来看下ConcurrentHashMap的内部结构:
图表1
从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

Segment

我们再来具体了解一下Segment的数据结构:

Java代码

  1. static final class Segment<K,V> extends ReentrantLock implements Serializable {
  2. transient volatile int count;
  3. transient int modCount;
  4. transient int threshold;
  5. transient volatile HashEntry<K,V>[] table;
  6. final float loadFactor;
  7. }

详细解释一下Segment里面的成员变量的意义:

· count:Segment中元素的数量

· modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)

· threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容

· table:链表数组,数组中的每一个元素代表了一个链表的头部

· loadFactor:负载因子,用于确定threshold

HashEntry

Segment中的元素是以HashEntry的形式存放在链表数组中的,看一下HashEntry的结构:

Java代码

  1. static final class HashEntry<K,V> {
  2. final K key;
  3. final int hash;
  4. volatile V value;
  5. final HashEntry<K,V> next;
  6. }

ConcurrentHashMap的get操作

前面提到过ConcurrentHashMap的get操作是不用加锁的,我们这里看一下其实现:

Java代码

public V get(Object key) {

int hash = hash(key.hashCode());

return segmentFor(hash).get(key, hash);

}

看第三行,segmentFor这个函数用于确定操作应该在哪一个segment中进行,几乎对ConcurrentHashMap的所有操作都需要用到这个函数,我们看下这个函数的实现:

Java代码

  1. final Segment<K,V> segmentFor(int hash) {
  2. return segments[(hash >>> segmentShift) & segmentMask];
  3. }

final Segment<K,V> segmentFor(int hash) {

return segments[(hash >>> segmentShift) & segmentMask];

}

这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中。

在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法:

Java代码

  1. V get(Object key, int hash) {
  2. if (count != 0) { // read-volatile
  3. HashEntry<K,V> e = getFirst(hash);
  4. while (e != null) {
  5. if (e.hash == hash && key.equals(e.key)) {
  6. V v = e.value;
  7. if (v != null)
  8. return v;
  9. return readValueUnderLock(e); // recheck
  10. }
  11. e = e.next;
  12. }
  13. }
  14. return null;
  15. }

先看第二行代码,这里对count进行了一次判断,其中count表示Segment中元素的数量,我们可以来看一下count的定义:

Java代码

  1. transient volatile int count;

可以看到count是volatile的,实际上这里里面利用了volatile的语义:

写道

对volatile字段的写入操作happens-before于每一个后续的同一个字段的读操作。

因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

然后,在第三行,调用了getFirst()来取得链表的头部:

Java代码

  1. HashEntry<K,V> getFirst(int hash) {
  2. HashEntry<K,V>[] tab = table;
  3. return tab[hash & (tab.length - 1)];
  4. }

同样,这里也是用位操作来确定链表的头部,hash值和HashTable的长度减一做与操作,最后的结果就是hash值的低n位,其中n是HashTable的长度以2为底的结果。

在确定了链表的头部以后,就可以对整个链表进行遍历,看第4行,取出key对应的value的值,如果拿出的value的值是null,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value。

这似乎有些费解,理论上结点的值不可能为空,这是因为 put的时候就进行了判断,如果为空就要抛NullPointerException。

空值的唯一源头就是HashEntry中的默认值,因为 HashEntry中的value不是final的,非同步读取有可能读取到空值。

仔细看下put操作的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序,这就可能导致结点的值为空。

这种情况应当很罕见,一旦发生这种情况,ConcurrentHashMap采取的方式是在持有锁的情况下再读一遍,这能够保证读到最新的值,并且一定不会为空值。
1. V readValueUnderLock(HashEntry<K,V> e) {
2. lock();
3. try {
4. return e.value;
5. } finally {
6. unlock();
7. }
8. }

ConcurrentHashMap的put操作

看完了get操作,再看下put操作,put操作的前面也是确定Segment的过程,这里不再赘述,直接看关键的segment的put方法:

Java代码

  1. V put(K key, int hash, V value, boolean onlyIfAbsent) {
  2. lock();
  3. try {
  4. int c = count;
  5. if (c++ > threshold) // ensure capacity
  6. rehash();
  7. HashEntry<K,V>[] tab = table;
  8. int index = hash & (tab.length - 1);
  9. HashEntry<K,V> first = tab[index];
  10. HashEntry<K,V> e = first;
  11. while (e != null && (e.hash != hash || !key.equals(e.key)))
  12. e = e.next;
  13. V oldValue;
  14. if (e != null) {
  15. oldValue = e.value;
  16. if (!onlyIfAbsent)
  17. e.value = value;
  18. }
  19. else {
  20. oldValue = null;
  21. ++modCount;
  22. tab[index] = new HashEntry<K,V>(key, hash, first, value);
  23. count = c; // write-volatile
  24. }
  25. return oldValue;
  26. } finally {
  27. unlock();
  28. }
  29. }

首先对Segment的put操作是加锁完成的,然后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash,关于rehash的过程大家可以自己去了解,这里不详细讲了。

第8和第9行的操作就是getFirst的过程,确定链表头部的位置。

第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素,如果找到,就直接更新更新key的value,如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。

分享到:
评论

相关推荐

    hashmap和hashtable的区别.docx

    hashmap和hashtable的区别 HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。...Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

    HashMap的特点与使用方法详解.docx

    多线程环境下,建议使用 ConcurrentHashMap,或者使用 Collections.synchronizedMap(hashMap) 将 HashMap 转成线程同步的。 只能使用关联的键来获取值。 HashMap 只能存储对象,所以基本数据类型应该使用其包装器...

    HashMap如何添加元素详解

    如果我么需要有一个线程安全的HashMap,可以使用Collections.synchronizedMap(Map m)方法获得线程安全的HashMap,也可以使用ConcurrentHashMap类创建线程安全的map。 存储的元素在jdk1.7当中是Entry作为存储的

    【并发】为什么HashMap是线程不安全的?

    经常会看到说HashMap是线程不安全的,ConcurrentHashMap是线程安全的等等说法,不禁有个疑问,什么是线程安全?什么样的类是线程安全的? 1.什么是线程安全性(what) 线程安全定义,最核心是正确性, 正确性:多个...

    java7hashmap源码-AndroidOffer:只为帮助您获得更好的报价

    对比:Hashtable、HashMap、LinkedHashMap、ConcurrentHashMap、TreeMap (看第六条就可以) HashMap 用什么数据结构实现的 加载因子是什么 HashMap 初始化传入的容量参数的值是就是 HashMap 实际分配的空间么 ...

    【面试系列】并发容器之ConcurrentHashMap

    看你简历里写了 HashMap,那你说说它存在什么缺点? 线程不安全 迭代时无法修改值 那你有用过线程安全的 Map 吗? 有,回答在哪用过。 没有,不过我了解过。 那你说说它们的实现。 Hashtable Hashtable 本身比较低效...

    Java岗位常规面试题.pdf

    3. Java中的HashMap和ConcurrentHashMap有什么区别?请比较它们的特点和适用场景。 4. 请解释Java中的线程安全(Thread Safety)是什么,以及如何实现线程安全的代码。 5. 什么是Java中的异常处理(Exception ...

    阿里百度美团面试题合集

    Synchronized 和 Lock 哪个更好? . HashMap 中的 get()方法是如何实现的? . HashMap 可以用在哪些场景? . JVM,垃圾回收机制,内存划分等 . SQL 优化,常用的索引? . 还有什么问题需要问的。 多线程状态图,...

    sesvc.exe 阿萨德

    本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 HashMap,没有它就不会有后面的 ConcurrentHashMap。 HashMap 众所周知 HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk...

    涵盖了90%以上的面试题

    HashMap,Hashtable和ConcurrentHashMap的区别 在ArrayList和LinkedList尾部添加元素,谁的效率更高 如果HashMap或者hashTable的key是一个自定义的类该怎么办 为什么重写equals还要重写hashCode? 介绍一下volatile jdk...

    袋鼠云面试(凉)

    4、为什么使用ConcurrentHashMap? 因为前面提了hashmap是线程不安全的容器,如果要使用线程安全的map 推荐使用 concurrentHashMap 然后问了这个问题 根据 hashtable的底层用synchronized关键字修饰的方法,和 ...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    180多页面试题,前前后后不间断的更新了两年,准备换工作时,总是拿来看看,有比较好的面试题,也不间断的更新,面试题目录如下: 【基础】面向对象的特征有哪些方面 13 抽象 13 继承 13 封装 13 多态性 13 【基础】...

    Java容器.xmind

    ConcurrentHashMap 初始容量默认为16段(segment),使用分段锁设计 ConcurrentLinkedQueue 高并发下性能最好的队列 无锁,采用CAS比较算法,核心参数(V,E,N) V:要更新的变量 E:预期值 N:新值 只有当V==E时,V=N;...

    百度地图毕业设计源码-interview-guide:面试指南

    ConcurrentHashMap 1.7和1.8的区别 redis 保证缓存与数据库中数据一致性 数据类型 hash扩容 MySQL 索引 消息队列 如何保证消息的可靠性 分布式 分布式锁 分布式事务 redis和zookeeper如何实现分布式锁

    Java服务器端开发面试.doc

    set, list, queue这些接口间的区别,set不可重复, arraylist的实现和linkedlist的实现区别,HashMap, HashTable。涉及到各种效率问题等,里面最好阅读一下源码 集合的遍历方法和使用iterator来遍历的区别,集合...

    最近面试一些厂的面经整理(阿里,腾讯,字节等)

    1. 经历了一个半月的时间学习,已拿到阿里,腾讯,...Hashmap和Hashtable和concurrentHashmap的区别 二面 简答Spring与Springboot之间的问题 回答JVM上的问题 回答Java中锁机制 回答Java中的数据库问题 三面 回答Java

    leetcode下载-JavaTopic:Java面试题总结

    Java并发锁synchronized、reentractlock的用法 HashMap数据结构及原理 ConcurrentHashMap数据结构及原理,分段锁机制 NIO、BIO相关 Java多线程篇: 线程池的核心参数及用法说明? 多线程死锁如何排查(考察linux

    java8源码-JavaRobot:Java学习笔记,JavaLearningNote

    synchronized关键字 Java String Java Arrays Java Collections Java 泛型 Java NIO Buffer Channel Selector Java 8 Features 源码解读 String源码系列 List源码系列 ArrayList LinkedList CopyOnWriteArrayList ...

    leetcode题库-java-interview:Java研发基础相关

    ConcurrentHashMap 详解 异常相关 代理机制 JDBC BIO NIO AIO 创建类的方法 final finally finalize 反射 序列化与反序列化 transient 枚举 注解 JDK7新特性 JDK8新特性 JDK9新特性 JDK10新特性 运行时数据区 对象 ...

    java核心知识点整理.pdf

    1. 目录 1. 2. 目录 .........................................................................................................................................................1 JVM .........................

Global site tag (gtag.js) - Google Analytics