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

HashMap和ConcurrentHashMap的实现原理和冲突解决

    博客分类:
  • java
 
阅读更多

本篇源码来自JDK1.8.

 

Java的位运算和取模取余操作说明:

 

位运算(&)效率要比取余运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

 

X % 2^n = X & (2^n - 1)

2^n表示2的n次方,也就是说,一个数对2^n取余 == 一个数和(2^n - 1)做按位与运算

假设n为3,则2^3 = 8,表示成2进制就是1000。2^3 -1 = 7 ,即0111。

此时X & (2^3 - 1) 就相当于取X的2进制的最后三位数。

 

右移和取模

从2进制角度来看,X / 8相当于 X >> 3,即把X右移3位,此时得到了X / 8的商,而被移掉的部分(后三位),则是X % 8,也就是余数。

 

HashMap的初始化:

可以是无参数的初始化,也可以指定初始大小和加载因子。

第一次添加元素时,进行第一次扩容,设置一个大小为16的数组,其中包含的元素为Node<K,V>,扩容的临界值为0.75 * 16 = 12,即等到大小超过12时再次进行扩容。

Node<K,V>结点包含4个元素:

 Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;//next用于处理有冲突的情况,
        }

扩容时,会把旧的数组中的元素,重新计算索引,放入新的数组中。

newTab[e.hash & (newCap - 1)] = e;//根据元素的hash值与当前的容量-1,进行位元素,相当于取余操作

  

扩容时,新的size计算,新的容量=旧的容量*2:

 if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

 

HashMap初始化时,如果指定了一个集合,或者指定了一个初始大小,则会自动计算出一个比它大的最近的2^n值作为初始化数组的大小。计算数组大小的方法如下:

 static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

 

 

 

HashMap的put方法:

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);//通过key,获取一个hash值
    }

 

    获取hash值的算法,用key的hashcode值高16位于低16位进行异或操作。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

 

HashMap中解决冲突的方法, 先用线性链表来保存冲突,如果冲突链表长度超过8,则使用红黑树结构。

 

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)//put时,没有冲突,直接新建一个Node。
            tab[i] = newNode(hash, key, value, null);
        else {  //如果此处有Node,则解决冲突
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

put方法(新增元素)总结:

计算元素数组下标的方法:通过元素Key的hash值与数组容量取余。

hash值相同,则产生冲突。但是hash值相同,不一定key相同。

1,通过key值获取一个hash值。

2,判断当前是否达到临界值,确定是否扩容,扩容则会把当前的容量扩大一倍。扩容时,会重新根据元素的hash值和新的数组容量,计算元素的数组下标。

3,新建一个Node结点,计算数组下标,放入对应的位置,如果有冲突,则添加进链表或者红黑树。产生冲突时,如果新增的Node结点的key与之前的key相同,则返回之前的key对应的value值。

 

根据Key值获取对象:

 final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))//判断hash和equals,都相等,则获取成功;
                return first;
            if ((e = first.next) != null) {//否则从链表中往下寻找。
                if (first instanceof TreeNode)  //如果是红黑树,则从红黑树中获取Node.
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {  // 否则从线性链表中搜索.
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

 

 

由于HashMap的扩容需要重新计算各个已有元素的hash值,并重新调整数组下标,是一个耗时的操作,因此最好初始化时就指定HashMap的容量initialCapacity。

initicalCapacity = (需要存储的元素个数/负载因子) + 1。  负载因子默认为0.75。如果不确定存储的元素个数,设置初始大小为16(默认值)。

 

只有当HashMap中一次性添加一个集合putAll(),或者初始化指定容量大小时,才会通过算法计算容量大小,保证容量大小为2的n次幂。 put操作,如果容量为空,则初始化为16,容量不够,则只会简单的把当前容量扩大一倍。HashMap的容量因此能一直保持为2的N次幂大小。

 

ConcurrentHashMap:

ConcurrentHashMap的初始化时如果指定大小initialCapacity

则以initialCapacity + (initialCapacity >>> 1) + 1 开始调整,计算出比它大的最近的2^n值作为初始大小

 

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }
 

 JDK 8之后,ConcurrentHashMap的实现原理与之前有较大的变化:

它摒弃了Segment(锁段)的概念,改用了Synchronized 和 CAS原理来实现并发。它的底层与HashMap一致,仍然采用数组+链表+红黑树的结构。

 

第一次插入数据时,进行初始化数组initTable();

putVal方法中有一个无限循环:

for (Node<K,V>[] tab = table;;),只有操作成功才会跳出循环。

这其实是因为并发操作时,有时需要不断自旋等待。  

 

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();//初始化数组,
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
               //利用CAS算法,设置位置i的元素node
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)//如果正在扩容,移动元素,则加入帮助扩容
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {//节点插入链表或者插入红黑树时,进行同步
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }
  

 

 

获取几个相关参数的内存地址,使用CAS操作直接进行修改。

 

 // Unsafe mechanics
    private static final sun.misc.Unsafe U;
    private static final long SIZECTL;
    private static final long TRANSFERINDEX;
    private static final long BASECOUNT;
    private static final long CELLSBUSY;
    private static final long CELLVALUE;
    private static final long ABASE;
    private static final int ASHIFT;

    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            Class<?> k = ConcurrentHashMap.class;
            SIZECTL = U.objectFieldOffset
                (k.getDeclaredField("sizeCtl"));
            TRANSFERINDEX = U.objectFieldOffset
                (k.getDeclaredField("transferIndex"));
            BASECOUNT = U.objectFieldOffset
                (k.getDeclaredField("baseCount"));
            CELLSBUSY = U.objectFieldOffset
                (k.getDeclaredField("cellsBusy"));
            Class<?> ck = CounterCell.class;
            CELLVALUE = U.objectFieldOffset
                (ck.getDeclaredField("value"));
            Class<?> ak = Node[].class;
            ABASE = U.arrayBaseOffset(ak);//获取数组第一个元素在内存中的偏移位置
            int scale = U.arrayIndexScale(ak);//获取数组中元素的增量地址
              //将arrayBaseOffset和arrayIndexScale配合使用,可以获取数组中每个元素的偏移位置
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
 

 

相关的几个CAS操作的方法:

  static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

 

 

EnumMap使用

EnumMap是专门为枚举类型量身定做的Map实现。

EnumMap要求其Key必须为Enum类型,它只能接收同一枚举类型的实例作为键值且不能为null。

EnumMap使用数组来存放与枚举类型对应的值,由于枚举类型实例的数量相对固定并且有限,所以使用EnumMap不会涉及到数组扩容,性能会更加高效。

2
0
分享到:
评论

相关推荐

    java7-8中的 HashMap和ConcurrentHashMap全解析.pdf

    总的来说,HashMap和ConcurrentHashMap的实现都依赖于高效的哈希函数和冲突解决策略。在Java 7中,HashMap适用于单线程环境,而ConcurrentHashMap通过分段锁提供线程安全。在Java 8中,两者都通过红黑树优化了性能,...

    HashMap与ConcurrentHashMap面试要点.pdf

    ### HashMap和ConcurrentHashMap...综上所述,面试时通常会针对HashMap和ConcurrentHashMap的实现原理、它们的差异、以及在并发环境下如何保证数据的一致性和线程安全进行提问。了解这些知识点有助于在面试中脱颖而出。

    详谈HashMap和ConcurrentHashMap的区别(HashMap的底层源码)

    HashMap和ConcurrentHashMap都是Java语言中常用的哈希表实现,但是它们之间存在着一些关键的区别,HashMap是非线程安全的,而ConcurrentHashMap是线程安全的,HashMap适用于单线程环境,而ConcurrentHashMap适用于多...

    深入Java集合学习系列:HashMap的实现原理

    在Java编程语言中,集合框架是开发者日常工作中不可或缺的一部分,HashMap作为其中的重要成员,它的实现原理对于理解Java性能优化和数据结构有深远的意义。HashMap是一个基于哈希表的数据结构,它实现了Map接口,...

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

    总的来说,`ConcurrentHashMap`的设计是为了解决多线程环境下HashMap的线程安全问题,通过分段锁和CAS操作实现了高效并发的哈希表。从JDK1.7到1.8的演变,反映了并发编程中对性能和线程安全的不断追求。

    HashMap底层实现原理共6页.pdf.zip

    《HashMap底层实现原理》 HashMap是Java编程语言中常用的数据结构之一,它是基于哈希表实现的,提供了高效的键值对存储和查找功能。HashMap在Java集合框架中扮演着重要的角色,广泛应用于各种数据存储和处理场景。...

    HashMap底层原理

    HashMap的底层原理主要依赖于哈希表,这是一种数据结构,它通过计算键的哈希码来实现高效的查找操作。 在HashMap中,每个元素都是一个键值对,存储在一个Entry对象中。当向HashMap添加键值对时,首先会计算键的哈希...

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

    HashMap通过链表或者自版本1.8起引入的红黑树来解决这种冲突,确保在冲突发生时仍然能够高效地查找和插入元素。 HashMap与HashTable的主要区别在于线程安全性和对null值的支持。HashMap是非同步的,意味着在多线程...

    HashMap的实现原理

    总结来说,HashMap的实现原理结合了哈希表、链表和红黑树,通过高效的哈希算法和扩容策略来确保高效的数据存取。然而,由于HashMap的非线程安全特性,开发者在多线程环境中应考虑使用`ConcurrentHashMap`等线程安全...

    【面试普通人VS高手系列】ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?.doc

    ConcurrentHashMap 本质上是一个 HashMap,因此功能和 HashMap 一样,但 ConcurrentHashMap 在 HashMap 的基础上,提供了并发安全的实现。并发安全的主要实现是通过对指定的 Node 节点加锁,来保证数据更新的安全性...

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

    8. **冲突处理**:哈希冲突是不可避免的,当两个键经过哈希函数后得到相同的索引时,HashMap 使用链地址法来解决冲突,即将这些键值对链接到同一个桶中。 9. **哈希函数**:HashMap 的性能高度依赖于哈希函数。好的...

    HashMap原理.rar

    总的来说,HashMap通过哈希表实现了高效的数据存储和检索,但需要注意其线程安全性和潜在的哈希冲突问题。在实际开发中,根据具体需求选择合适的数据结构是非常重要的。了解HashMap的工作原理,可以帮助我们更好地...

    面试必考之HashMap源码分析与实现

    当两个键的哈希值相同,导致冲突时,HashMap使用链地址法来解决,即将冲突的键值对链接成一个链表。 3. **哈希冲突处理** 在HashMap中,如果两个键的哈希值相同但并不相等,它们会在同一个桶(数组索引位置)形成...

    HashMap底层原理.pdf

    本文将详细介绍HashMap的底层原理,包括其内部实现结构、关键字段的作用、以及JDK 1.7和JDK 1.8版本的主要区别。 首先,HashMap是基于哈希表的Map接口非同步实现,它允许使用null值和null键,这意味着HashMap在设计...

    hashmap面试题_hashmap_

    总结,HashMap是Java编程中的基础工具,掌握其工作原理和常见面试题,不仅能帮助我们应对面试,更能提升在实际开发中的问题解决能力。深入理解HashMap,有助于我们更好地利用数据结构,提高代码的执行效率。

    ConcurrentHashMap之实现细节

    本文将深入探讨`ConcurrentHashMap`的关键实现细节,包括其核心设计思想——锁分离(Lock Stripping),以及如何通过不可变性和易变性的巧妙结合来优化读取性能。 #### 二、锁分离技术 锁分离是`ConcurrentHashMap...

    05.HashMap相关面试题

    HashMap 和 HashTable都是基于散列表的集合框架,但它们有着不同的设计目标和实现方式。 * HashMap 是非线程安全的,意味着它不能在多线程环境下使用。 * HashTable 是线程安全的,使用 synchronized 关键字来实现...

    HashMap的工作原理Java开发Java经验技巧共4页

    深入理解HashMap的工作原理对于提升Java开发的效率和写出高效的代码至关重要。以下是对HashMap工作原理的详细解析。 HashMap基于哈希表(也称为散列表)实现,它的核心思想是通过对象的哈希值来快速定位数据。当向...

    自定义map实现java的hashmap

    HashMap基于哈希表(也称为散列表)原理,通过键对象的哈希码来定位元素,进而实现O(1)的平均时间复杂度。下面我们将深入探讨如何使用数据结构的思想自定义一个类似HashMap的实现。 1. 基本概念 - 键(Key):...

Global site tag (gtag.js) - Google Analytics