HashMap的工作原理是什么?

HashMap在JDK1.8之前的实现方式 数组+链表,

但是在JDK1.8后对HashMap进行了底层优化,改为了由 数组+链表+红黑树实现,主要的目的是提高查找效率。

JDK版本实现方式节点数>=8节点数<=6
1.8以前数组+单向链表数组+单向链表数组+单向链表
1.8以后数组+单向链表+红黑树数组+红黑树数组+单向链表

img
HashTable是锁整个Map对象 ConcurrentHashMap是锁Map的部分结构

基于Map接口

table 是一个 Node 类型的数组,默认长度为 16,在第一次执行 resize() 方法的时候初始化

Node 是 HashMap 的一个内部类,实现了 Map.Entry 接口,本质上是一个键值对。 Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。 初始值为空数组{},主干数组的长度一定是2的次幂,

HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null 建和null 值, 因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。HashMap是线程不安全的。

//默认初始容量为16,0000 0001 右移4位 0001 0000为16,主干数组的初始容量为16,而且这个数组
//必须是2的倍数(后面说为什么是2的倍数)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量为int的最大值除2
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认加载因子为0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//阈值,如果主干数组上的链表的长度大于8,链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//hash表扩容后,如果发现某一个红黑树的长度小于6,则会重新退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//当hashmap容量大于64时,链表才能转成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//临界值=主干数组容量*负载因子
int threshold;
数组 数组在堆中是一块连续的存储空间 数组遍历的时间复杂度为O(1) 增删慢是因为,当在中间插入或删除元素时,会造成该元素后面所有元素地址的改变,所以增删慢(增删的时间复杂度为O(n) )。

链表具有增删快,遍历慢的特点。链表中各元素的内存空间是不连续的,一个节点至少包含节点数据与后继节点的引用,所以在插入删除时,只需修改该位置的前驱节点与后继节点即可,链表在插入删除时的时间复杂度为O(1)。但是在遍历时,get(n)元素时,需要从第一个开始,依次拿到后面元素的地址,进行遍历,直到遍历到第n个元素(时间复杂度为O(n) ),所以效率极低。hash碰撞:

hash是指,两个元素通过hash函数计算出的值是一样的,是同一个存储地址。当后面的元素要插入到这个地址时,发现已经被占用了,这时候就产生了hash冲突 链表过长,效率就会大大降低,查找和添加操作的时间复杂度都为O(n); 但是在jdk1.8中如果链表长度大于8,链表就会转化为红黑树,时间复杂度也降为了O(logn),性能得到了很大的优化。 首先,hashMap的主干是一个Node数组(jdk1.7及之前为Entry数组)每一个Node包含一个key与value的键值对,与一个next next指向下一个node,hashMap由多个Node对象组成。
一次put经历的过程:
将key传入hash方法,计算其对应的hash值:

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

此处如果传入的int类型的值:①向一个Object类型赋值一个int的值时,会将int值自动封箱为Integer。②integer类型的hashcode都是他自身的值,即h=key;h >>> 16为无符号右移16位,低位挤走,高位补0;^ 为按位异或,即转成二进制后,相异为1,相同为0,由此可发现,当传入的值小于 2的16次方-1 时,调用这个方法返回的值,都是自身的值。
然后再执行putVal方法:

static final int TREEIFY_THRESHOLD = 8;
//根据key生成hash值。
//根据key的hash和Node数组长度生成下标,找到对应数组元素。  
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  boolean evict) {
      //HashMap的Node数组
      //要操作的Node
      //n为当前HashMap的Node数组长度  i为操作的数组下标
      Node<K,V>[] tab; Node<K,V> p; int n, i;
       //判断tab数组是不是空
        if ((tab = table) == null || (n = tab.length) == 0)
            //如果数组是空就初始化数组,n是数组长度初始化默认是16
            n = (tab = resize()).length;
      // & hash这是为了计算坑位当n为2的m次幂的时候(n - 1) & hash与hash%n的结果是一样的
       // 根据键的哈希值计算索引位置,如果位置上没有节点,把键值对封装成一个Node节点放在索引位置上
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {//否则坑位有人
            Node<K,V> e; K k;
            //判断hash相等key相等说明坑位是自己占了,替换value就可以了
            //先判断hash是否一致(hash不相同也可以映射到同一个位置)再判断值是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)//判断是不是红黑树
                // 如果索引位置节点是TreeNode,说明是一棵红黑树,利用红黑树的putTreeVal方法添加键值对
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {//否则就是链表
                for (int binCount = 0; ; ++binCount) {//循环链表
                    // 如果节点为空,那么说明当前链表上没有这个键,封装一个Node插入到链表尾部即可
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);//就生成子节点挂到父节点
                        //如果链表长度 >= 7
                        //遍历次数,就是链表节点的数量,如果数量到达默认的阈值
                        //调用HashMap的treeifyBin方法,判断是否有必要转换为红黑树/
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);//转成红黑树
                        break;//跳出循环
                    }
                    //判断hash相等key相等说明坑位是自己占了,替换value就可以了
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            如果e不为空说明有找到hash相等key相等的节点
            //表示键存在索引位置的数据结构中(节点、链表、红黑树)
            if (e != null) { // existing mapping for key
                V oldValue = e.value; //节点的旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;//替换新的值
                afterNodeAccess(e);//空方法
                return oldValue;//返回旧的指
            }
        }
        ++modCount;// 修改次数+1
        if (++size > threshold)//如果大于了阙值就扩容
            resize();
        afterNodeInsertion(evict);//空方法 用于子类覆写
        return null;
    }
    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
  final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; //临时变量储存table数组
        Node<K,V> first, e;//临时变量获取第一个元素
        int n; K k; //n为table的长度
        //如果table已经被初始化切table数组的长度大于0,在已知元素的查找位置上有元素则进入if判断
        //现判断HashMap的数组是否为空 且 根据hash值判断数组对应的位置是否为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //first元素存在,切first元素即锁需要查找的元素,直接返回first.
            // 不为空则进行判断数组上的第一个节点是否是传入的key对应的节点
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //e为临时变量储存在first元素不是所需元素的下一个元素
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    //当链表被树化后的查询
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //如果链表没有被树化,则使用链表的方式查询.
                do {
                    //循环判断当前的临时变量e是否与所需元素相同
                    //相同则返回e元素,不相同则返回null
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

重写equals方法的时候,一定要重写hashCode方法

在重写equals()方法时,也有必要对hashCode()方法进行重写,尤其是当我们自定义一个类,想把该类的实例存储在集合中时。

hashCode方法的常规约定为:值相同的对象必须有相同的hashCode,也就是equals()结果为相同,那么hashcode也要相同,equals()结果为不相同,那么hashcode也不相同;

当我们使用equals方法比较说明对象相同,但hashCode不同时,就会出现两个hashcode值,比如在HashMap中,就会认为这是两个对象,因此会出现矛盾,说明equals方法和hashCode方法应该成对出现,当我们对equals方法进行重写时,也要对hashCode方法进行重写。

hashCode()方法和equal()方法的作用其实一样,在Java里都是用来对比两个对象是否相等一致,那么equal()既然已经能实现对比的功能了,为什么还要hashCode()呢?

因为重写的equal()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对比,则只要生成一个hash值进行比较就可以了,效率很高,那么hashCode()既然效率这么高为什么还要equal()呢?

​ 因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出:

​ 1.equal()相等的两个对象他们的hashCode()肯定相等,也就是用equal()对比是绝对可靠的。

​ 2.hashCode()相等的两个对象他们的equal()不一定相等,也就是hashCode()不是绝对可靠的。

        Phone4 phone1 = new Phone4();
        Phone4 phone2 = new Phone4();
        System.out.println(phone1.hashCode());//237061348
        System.out.println(phone2.hashCode());//1685538367

HashMap中的“死锁”是怎么回事?

HashMap在并发使用时可能发生死循环,导致cpu100%

在内部,HashMap使用一个Entry数组保存key、value数据,当一对key、value被加入时,会通过一个hash算法得到数组的下标index,算法很简单,根据key的hash值,对数组的大小取模 hash & (length-1),并把结果插入数组该位置 当插入一个新的节点时,如果不存在相同的key,则会判断当前内部元素是否已经达到阈值(默认是数组大小的0.75),如果已经达到阈值,会对数组进行扩容,也会对链表中的元素进行rehash。 并把原来的元素移动过去。 这里会新建一个更大的数组,并通过transfer方法,移动元素。 移动的逻辑也很清晰,遍历原来table中每个位置的链表,并对每个元素进行重新hash,在新的newTable找到归宿,并插入。 假设HashMap初始化大小为4,插入个3节点,不巧的是,这3个节点都hash到同一个位置,如果按照默认的负载因子的话,插入第3个节点就会扩容,为了验证效果,假设负载因子是1. 插入第4个节点时,发生rehash,假设现在有两个线程同时进行,线程1和线程2,两个线程都会新建新的数组。

假设 线程2 在执行到Entry next = e.next;之后,cpu时间片用完了,这时变量e指向节点a,变量next指向节点b。

线程1继续执行,很不巧,a、b、c节点rehash之后又是在同一个位置7,开始移动节点

节点a和b互相引用,形成了一个环,当在数组该位置get寻找对应的key时,就发生了死循环。

所以在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

HashMap中能put两个相同key吗?为什么?如何实现?

key(value)比较的时候只比较两个key是否引用同一个对象,比较的是对象的地址

将字符串"key"直接赋值给str1和str2,因为字符串是放在常量池中的,所以str1和str2实际上还是同一个对象,所以它们的key值是相同的,会被覆盖;

       HashMap<Object, Object> hashMap = new HashMap<>();
        String s1="123";
        String s2="123";
        hashMap.put(s1,1);
        hashMap.put(s2,2);
        System.out.println("s1="+s1.hashCode()); //48690
        System.out.println("s2="+s2.hashCode()); //48690
        System.out.println(s1==s2); //true
        System.out.println(s1.equals(s2));//true
        // {123=2}
        System.out.println(hashMap);//默认调用toString

str1和str2是通过new的方式创建出来的,属于不同对象,所以它们的引用不同,key值也就不同,所以put的时候不会被覆盖;

   public static void main(String[] args) {
        HashMap<Object, Object> hashMap = new HashMap<>();
        ArrayListTest a1 = new ArrayListTest();
        ArrayListTest a2 = new ArrayListTest();
        hashMap.put(a1,1);
        hashMap.put(a2,2);
        // {com.Test.ArrayListTest@e2144e4=1, com.Test.ArrayListTest@6477463f=2}
        System.out.println(hashMap);//默认调用toString
      /*  Set<Map.Entry> ms =hm.entrySet();
        for (Map.Entry entry : ms) {
            System.out.print(entry.getKey()+"="+entry.getValue());
        }
            for(Map.Entry<Object,Object> entry:hashMap.entrySet()){
            System.out.println(entry.getKey()+"  "+entry.getValue());
        }
        */
    }

有一个特殊的map—>IdentityHashMap可以实现一个key保存多个value

注意:此类并不是通用的Map实现!此类再实现Map接口的时候违反了Map的常规协定,Map的常规协议在
比较对象强制使用了equals()方法,但此类设计仅用于其中需要引用相等性语义的情况

(IdentityhashMap类利用哈希表实现Map接口,比较键(和值)时使用引用相等性代替对象相等性,
也就是说做key(value)比较的时候只比较两个key是否引用同一个对象)

只要重写了key的hashCode()和map的put()方法,应该就可以实现对于相同key下多个value的存储。

总结:要实现开头的需求

1、如果是类似String这种,已经重写了hashCode和equals的。则只需要创建一个自己的HashMap类,重写put即可。

2、如果是自定义的类,那就必须重写了hashCode和equals的,然后在使用自定义的HashMap类了。

具体做法是:由于判断key是否存在的时候是先比较key的hashCode,再比较相等或equals的,所以重写hashCode()和equals()方法即可实现添加重复元素。重写这两个方法之后就可以覆盖重复的键值对,如果需要对value进行叠加,调用put()方法之前用containsKey()方法判断是否有重复的键值,如果有,则用get()方法获取原有的value,再加上新加入的value即可。

 if (   e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))    )

HashMap如何计算数组下标

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

如果key对象为null的话,就返回一个0,当然String对象为null的话,强行计算hashCode()是会抛出空指针异常的。如果key对象不是为null,那么获取key的hashCode,然后与右移位16位后的hashCode进行异或操作(关于异或操作,总结:不同,结果为1,相同,结果为0),异或的结果就位hash()函数返回值。我们以String的hashCode()为例

总结一下: HashMap中数组下标值的计算过程,大致分为如下几步:

  • 获取key.hashCode(),然后将hashCode高16位和低16位异或(^)操作,
  • 与当前数组长度-1结果进行与(&)操作,最终结果就是数组的下标值。

至于由于当前结点(键值对)的加入,导致当前HashMap中容量超过了阈值而扩容2倍,扩容后导致每个结点重新计算下标值(称为新下标值),新下标值只有两种可能:

  • key的hash()返回值新加入与(&)操作计算的一位为0,则下标值与原下标值相同。
  • key的hash()返回值新加入与(&)操作计算的一位为1,则计算出来的下标值=原下标值+原数组长度。
    如果key对象为Integer,他的hashCode()直接返回的value。

HashMap中的键值可以为null吗?原理?

可以,数组第一个值

HashMap扩容机制?

当map中包含的Entry的数量大于等于threshold = loadFactor * capacity的时候,且新建的Entry刚好落在一个非空的桶上,此刻触发扩容机制,将其容量扩大为2倍。(为什么2倍,而不是1.5倍,3倍,10倍;解释见最后的补充)

当size大于等于threshold的时候,并不一定会触发扩容机制,但是会很可能就触发扩容机制,只要有一个新建的Entry出现哈希冲突,则立刻resize。
resize(),为什么容量需要时2倍这样扩张

HashMap的容量必须是2的幂,那么为什么要这么设计呢?答案当然是为了性能。在HashMap通过键的哈希值进行定位桶位置的时候,调用了一个indexFor(hash, table.length);方法。

static int indexFor(int h, int length) {
        return h & (length-1);
    }

这里是将哈希值h与桶数组的length-1(实际上也是map的容量-1)进行了一个与操作得出了对应的桶的位置,h & (length-1)。 Java%/操作比&慢10倍左右,因此采用&运算会提高性能。 **通过限制length是一个2的幂数,h & (length-1)h % length结果是一致的。**这就是为什么要限制容量必须是一个2`的幂的原因。

Java8中扩容只需要满足一个条件:当前存放新值(注意不是替换已有元素位置时)的时候已有元素的个数大于等于阈值(已有元素等于阈值,下一个存放后必然触发扩容机制)

注:

(1)扩容一定是放入新值的时候,该新值不是替换以前位置的情况下

(2)扩容发生在存放后,即是数据存放后(先存放后扩容),判断当前存入对象的个数,如果大于阈值则进行扩容。

(1)Java 8 在新增数据存入成功后进行扩容

(2)扩容会发生在两种情况下(满足任意一种条件即发生扩容):

a 当前存入数据大于阈值即发生扩容

b 存入数据到某一条链表时,此时该链表数据个数大于8,且数组长度小于64即发生扩容

(3)此外需要注意一点java7是在存入数据前进行判断是否扩容,而java8是在存入数据后再进行扩容的判断。

这里补充一下JDK8关于红黑树和链表的知识:

第一次添加元素的时候,默认初期长度为16,当往map中继续添加元素的时候,通过hash值跟数组长度取“与”来决定放在数组的哪个位置,如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了8个(代码是>=7,从0开始,及第8个开始判断是否转化成红黑树),如果数组的长度还小于64的时候,则会扩容数组。如果数组的长度大于等于64的话,才会将该节点的链表转换成树。在扩容完成之后,如果某个节点的是树,同时现在该节点的个数又小于等于6个了,则会将该树转为链表。

HashMap 与HashTable的区别

1.两者最主要的区别在于Hashtable是线程安全,而HashMap则非线程安全

Hashtable的实现方法里面都添加了synchronized关键字来确保线程同步,因此相对而言HashMap性能会高一些,我们平时使用时若无特殊需求建议使用HashMap,在多线程环境下若使用HashMap需要使用Collections.synchronizedMap()方法来获取一个线程安全的集合。

2.HashMap可以使用null作为key,而Hashtable则不允许null作为key
虽说HashMap支持null值作为key,不过建议还是尽量避免这样使用,因为一旦不小心使用了,若因此引发一些问题,排查起来很是费事

3.HashMap是对Map接口的实现,HashTable实现了Map接口和Dictionary抽象类

HashMap的初始容量为16,Hashtable初始容量为11,两者的填充因子默认都是0.75

4.HashMap扩容时是当前容量翻倍即:capacity2,Hashtable扩容时是容量翻倍+1即:capacity2+1

5.两者计算hash的方法不同
Hashtable计算hash是直接使用key的hashcode对table数组的长度直接进行取模

int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取摸

static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

 static int indexFor(int h, int length) {
        return h & (length-1);
    }

hashmap与concurrenthashmap原理和区别


HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

HashMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
  • 插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
  • 当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
  • 计算index方法:index = hash & (tab.length – 1)

HashMap的初始值还要考虑加载因子:

  • 哈希冲突:若干Key的哈希值按数组大小取模后,如果落在同一个数组下标上,将组成一条Entry链,对Key的查找需要遍历Entry链上的每个元素执行equals()比较。
  • 加载因子:为了降低哈希冲突的概率,默认当HashMap中的键值对达到数组大小的75%时,即会触发扩容。因此,如果预估容量是100,即需要设定100/0.75=134的数组大小。
  • 空间换时间:如果希望加快Key查找的时间,还可以进一步降低加载因子,加大初始大小,以降低哈希冲突的概率。

HashMap和Hashtable都是用hash算法来决定其元素的存储,因此HashMap和Hashtable的hash表包含如下属性:

  • 容量(capacity):hash表中桶的数量
  • 初始化容量(initial capacity):创建hash表时桶的数量,HashMap允许在构造器中指定初始化容量
  • 尺寸(size):当前hash表中记录的数量
  • 负载因子(load factor):负载因子等于“size/capacity”。负载因子为0,表示空的hash表,0.5表示半满的散列表,依此类推。轻负载的散列表具有冲突少、适宜插入与查询的特点(但是使用Iterator迭代元素时比较慢)

除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing。

HashMap和Hashtable的构造器允许指定一个负载极限,HashMap和Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:

  • 较高的“负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与put()方法都要用到查询)
  • 较低的“负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销

程序猿可以根据实际情况来调整“负载极限”值。

ConcurrentHashMap

  • 底层采用分段的数组+链表实现,线程安全
  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

Hashtable和HashMap都实现了Map接口,但是Hashtable的实现是基于Dictionary抽象类的。Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。

HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,然后找到bucket位置来存储值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞时,对象将会储存在链表的下一个节点中。HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD,8),链表就会被改造为树形结构。

在HashMap中,null可以作为键,这样的键只有一个,但可以有一个或多个键所对应的值为null。当get()方法返回null值时,即可以表示HashMap中没有该key,也可以表示该key所对应的value为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个key,应该用**containsKey()**方法来判断。而在Hashtable中,无论是key还是value都不能为null。

Hashtable是线程安全的,它的方法是同步的,可以直接用在多线程环境中。而HashMap则不是线程安全的,在多线程环境中,需要手动实现同步机制。

Hashtable与HashMap另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。

来在存储结构中ConcurrentHashMap比HashMap多出了一个类Segment,而Segment是一个可重入锁。

ConcurrentHashMap是使用了锁分段技术来保证线程安全的。

锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

ConcurrentHashMap提供了与Hashtable和SynchronizedMap不同的锁机制。Hashtable中采用的锁机制是一次锁住整个hash表,从而在同一时刻只能由一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。

ConcurrentHashMap默认将hash表分为16个桶,诸如get、put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

Hash如何避免内存泄漏?

Java中怎么可以产生内存泄露?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值