`
wang吖
  • 浏览: 238399 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ConcurrentHashMap 的实现原理及与HashTable的比较

 
阅读更多
集合是编程中最常用的数据结构。而谈到并发,几乎总是离不开集合这类高级数据结构的支持。比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存作为外部文件的副本(HashMap)。这篇文章主要分析jdk1.5的3种并发集合类型(concurrent,copyonright,queue)中的ConcurrentHashMap,让我们从原理上细致的了解它们,能够让我们在深度项目开发中获益非浅。
    在tiger之前,我们使用得最多的数据结构之一就是HashMap和Hashtable。大家都知道,HashMap中未进行同步考虑,而Hashtable则使用了synchronized,带来的直接影响就是可选择,我们可以在单线程时使用HashMap提高效率,而多线程时用Hashtable来保证安全。
    当我们享受着jdk带来的便利时同样承受它带来的不幸恶果。通过分析Hashtable就知道,synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,安全的背后是巨大的浪费,慧眼独具的Doug Lee立马拿出了解决方案----ConcurrentHashMap。
    ConcurrentHashMap和Hashtable主要区别就是围绕着锁的粒度以及如何锁。如图
 
 
    左边便是Hashtable的实现方式---锁整个hash表;而右边则是ConcurrentHashMap的实现方式---锁桶(或段)。ConcurrentHashMap将hash表分为16个桶(默认值),诸如get,put,remove等常用操作只锁当前需要用到的桶。试想,原来只能一个线程进入,现在却能同时16个写线程进入(写线程才需要锁定,而读线程几乎不受限制,之后会提到),并发性的提升是显而易见的。
    更令人惊讶的是ConcurrentHashMap的读取并发,因为在读取的大多数时候都没有用到锁定,所以读取操作几乎是完全的并发操作,而写操作锁定的粒度又非常细,比起之前又更加快速(这一点在桶更多时表现得更明显些)。只有在求size等操作时才需要锁定整个表。而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(见之前的文章《JAVA API备忘---集合》)的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。
    接下来,让我们看看ConcurrentHashMap中的几个重要方法,心里知道了实现机制后,使用起来就更加有底气。
    ConcurrentHashMap中主要实体类就是三个:ConcurrentHashMap(整个Hash表),Segment(桶),HashEntry(节点),对应上面的图可以看出之间的关系。
    get方法(请注意,这里分析的方法都是针对桶的,因为ConcurrentHashMap的最大改进就是将粒度细化到了桶上),首先判断了当前桶的数据个数是否为0,为0自然不可能get到什么,只有返回null,这样做避免了不必要的搜索,也用最小的代价避免出错。然后得到头节点(方法将在下面涉及)之后就是根据hash和key逐个判断是否是指定的值,如果是并且值非空就说明找到了,直接返回;程序非常简单,但有一个令人困惑的地方,这句return readValueUnderLock(e)到底是用来干什么的呢?研究它的代码,在锁定之后返回一个值。但这里已经有一句V v = e.value得到了节点的值,这句return readValueUnderLock(e)是否多此一举?事实上,这里完全是为了并发考虑的,这里当v为空时,可能是一个线程正在改变节点,而之前的get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值,这里不得不佩服Doug Lee思维的严密性。整个get操作只有很少的情况会锁定,相对于之前的Hashtable,并发是不可避免的啊!
        V get(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry 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 e) {
            lock();
            try {
                return e.value;
            } finally {
                unlock();
            }
        }

 

    put操作一上来就锁定了整个segment,这当然是为了并发的安全,修改数据是不能并发进行的,必须得有个判断是否超限的语句以确保容量不足时能够rehash,而比较难懂的是这句int index = hash & (tab.length - 1),原来segment里面才是真正的hashtable,即每个segment是一个传统意义上的hashtable,如上图,从两者的结构就可以看出区别,这里就是找出需要的entry在table的哪一个位置,之后得到的entry就是这个链的第一个节点,如果e!=null,说明找到了,这是就要替换节点的值(onlyIfAbsent == false),否则,我们需要new一个entry,它的后继是first,而让tab[index]指向它,什么意思呢?实际上就是将这个新entry插入到链头,剩下的就非常容易理解了。

        V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();
                HashEntry[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry first = (HashEntry) tab[index];
                HashEntry 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(key, hash, first, value);
                    count = c; // write-volatile
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

 

    remove操作非常类似put,但要注意一点区别,中间那个for循环是做什么用的呢?(*号标记)从代码来看,就是将定位之后的所有entry克隆并拼回前面去,但有必要吗?每次删除一个元素就要将那之前的元素克隆一遍?这点其实是由entry的不变性来决定的,仔细观察entry定义,发现除了value,其他所有属性都是用final来修饰的,这意味着在第一次设置了next域之后便不能再改变它,取而代之的是将它之前的节点全都克隆一次。至于entry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关,关于不变性的更多内容,请参阅之前的文章《线程高级---线程的一些编程技巧》

        V remove(Object key, int hash, Object value) {
            lock();
            try {
                int c = count - 1;
                HashEntry[] tab = table;
                int index = hash & (tab.length - 1);
                HashEntry first = (HashEntry)tab[index];
                HashEntry e = first;
                while (e != null && (e.hash != hash || !key.equals(e.key)))
                    e = e.next;

                V oldValue = null;
                if (e != null) {
                    V v = e.value;
                    if (value == null || value.equals(v)) {
                        oldValue = v;
                        // All entries following removed node can stay
                        // in list, but all preceding ones need to be
                        // cloned.
                        ++modCount;
                        HashEntry newFirst = e.next;
                    *    for (HashEntry p = first; p != e; p = p.next)
                    *        newFirst = new HashEntry(p.key, p.hash, 
                                                          newFirst, p.value);
                        tab[index] = newFirst;
                        count = c; // write-volatile
                    }
                }
                return oldValue;
            } finally {
                unlock();
            }
        }

 

    static final class HashEntry {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry next;

        HashEntry(K key, int hash, HashEntry next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }
    }

 

    以上,分析了几个最简单的操作,限于篇幅,这里不再对rehash或iterator等实现进行讨论,有兴趣可以参考src。

    接下来实际上还有一个疑问,ConcurrentHashMap跟HashMap相比较性能到底如何。这在Brian Goetz的文章中已经有过评测http://www.ibm.com/developerworks/cn/java/j-jtp07233/

分享到:
评论

相关推荐

    HashMap底层实现原理HashMap与HashTable区别HashMap与HashSet区别.docx

    HashMap与HashTable的主要区别在于线程安全性和对null值的支持。HashMap是非同步的,意味着在多线程环境中,如果不进行适当的同步控制,可能会导致数据不一致。而HashTable是同步的,因此它在多线程环境下的安全性更...

    ConcurrentHashMap的实现原理(JDK1.7和JDK1.8).pdf

    在理解`ConcurrentHashMap`的实现原理之前,我们先来看看哈希表的基本概念。 哈希表是一种键值对存储的数据结构,通过键(key)进行索引,可以高效地查找、插入和删除数据。如果键是整数,我们可以直接用它作为数组...

    HashMap,HashTable,ConcurrentHashMap之关联.docx

    ConcurrentHashMap 是 Java 中的另一个线程安全的类,它与 HashTable 在线程同步上有什么不同?ConcurrentHashMap 使用了分段锁的机制,每个段都独立锁定,提高了并发性能。 HashMap 和 HashTable 的区别 1. 线程...

    Java并发编程笔记之ConcurrentHashMap原理探究.docx

    相比HashTable,ConcurrentHashMap通过采用锁分离技术和更细粒度的锁定策略来提升性能。HashTable使用全局同步锁,即在读写操作时都需要对整个哈希表加锁,这会导致在多线程环境下性能瓶颈。 ConcurrentHashMap的...

    ConcurrentHashMap之实现细节

    ### ConcurrentHashMap实现细节详解 #### 一、概述 `ConcurrentHashMap`是Java 5引入的一种高性能、线程安全的散列表实现。相较于传统的`HashMap`,`ConcurrentHashMap`能够支持高并发环境下的多线程读写操作。...

    java软件技术文档-深入java8的集合5:Hashtable的实现原理.pdf

    尽管现在更多地使用HashMap或ConcurrentHashMap,但是了解Hashtable的实现原理仍然有助于深入理解Java的并发控制和数据结构。 **一、Hashtable概述** Hashtable类源自Java 1.0,它是一个基于哈希表的Map接口实现。...

    ConcurrentHashMap源码剖析

    与传统的基于synchronized关键字实现线程安全的HashTable相比,ConcurrentHashMap通过采用锁分段技术显著提高了并发性能。本文将深入探讨ConcurrentHashMap的内部结构、工作原理及其在实际场景中的应用。 #### 二、...

    java源码剖析-ConcurrentHashMap

    与传统的`Hashtable`相比,`ConcurrentHashMap`具有更高的并发性能,这主要得益于它的分段锁技术和非阻塞算法。 #### 二、`ConcurrentHashMap`的基本概念 1. **分段锁技术**:`ConcurrentHashMap`内部采用了分段锁...

    HashMap与HashTable的区别(含源码分析)

    在Java编程语言中,`HashMap`和`HashTable`都是实现键值对存储的数据结构,但它们之间存在一些显著的区别,这些区别主要体现在线程安全性、性能、null值处理以及一些方法特性上。以下是对这两个类的详细分析: 1. ...

    hashtable存储数据.rar

    此外,`Hashtable`实现了`Map`接口,所以也可以使用`entrySet()`方法获取所有键值对的`Set`视图,方便进行遍历和操作。 在实际开发中,由于`Hashtable`的线程安全特性,它通常在需要跨线程共享数据时使用。但是,`...

    经典讲解List和ArrayList和Vector和HashTable和HashMap区别

    `Vector`也是`List`接口的实现,与`ArrayList`类似,但它是线程安全的。这意味着在多线程环境下,多个线程可以同时操作`Vector`而不会产生数据不一致的问题。但是,由于其同步机制,性能通常低于`ArrayList`。 4. ...

    Java 中ConcurrentHashMap的实现

    与`Hashtable`不同,`ConcurrentHashMap`不会锁定整个Map,而是将Map分成多个段(默认16个),每个段由一个独立的锁控制。这意味着在任何时候,最多可以有16个线程进行写操作(取决于并发级别)。这种设计允许读操作...

    hashtable存储IP

    实际应用中,我们可以根据具体需求进一步优化,例如使用更现代的`HashMap`替代`Hashtable`(因为`Hashtable`是同步的,可能在多线程场景下性能不佳),或者考虑使用`ConcurrentHashMap`以实现线程安全且高效的并发...

    Java-并发容器之ConcurrentHashMap

    传统的线程安全解决方案,如Hashtable或使用Collections.synchronizedMap包装HashMap,虽然实现了线程安全,但性能上并不理想,因为它们采用了独占锁,只允许单个线程执行操作。 在JDK 1.6版本中,...

    java ConcurrentHashMap锁分段技术及原理详解

    在实际应用中,当需要在一个大数组上实现多线程共享时,可以考虑使用`ConcurrentHashMap`的锁分段策略。例如,通过哈希算法将数据分散到多个模块,避免单一锁的性能瓶颈。此外,这种思想也适用于数据库设计,如将大...

    实例5 哈希表(Hashtable)和枚举器

    在Java标准库中,`java.util.Hashtable`类实现了可存储任意对象的键值对容器。这个类自Java 1.0以来就已经存在,它是线程安全的,意味着在多线程环境下无需额外同步即可保证数据的一致性。 哈希表的工作原理基于...

    71-ConcurrentHashMap笔记1

    相较于早期版本,JDK1.8的ConcurrentHashMap放弃了Segment分段锁的设计,转而采用更细粒度的锁策略,结合Unsafe类的CAS操作和Node节点加锁,实现了高效且线程安全的并发哈希映射。 首先,让我们回顾一下HashMap和它...

    Java 多线程与并发(13-26)-JUC集合- ConcurrentHashMap详解.pdf

    JDK 1.8对`ConcurrentHashMap`进行了重大优化,放弃了分段锁机制,而是使用了基于数组的链表和红黑树数据结构,并结合CAS(Compare and Swap,比较并交换)原语实现无锁或轻量级锁的并发控制。这种设计大大减少了锁...

    2022 最全 Java 面试笔试题汇总

    与 HashTable、ConcurrentHashMap 的区别是什么? IO * BIO、NIO、AIO 的区别是什么?IO 和 NIO 的区别是什么?NIO 的优点是什么? Java 基础 * Hashcode 和 equals 方法的联系是什么?什么是重写和重载? * ...

Global site tag (gtag.js) - Google Analytics