HashMap、ConcurretnHashMap面试题详解,源码分析

附上关于hashMap的几篇优质文章

1.HashMap与Hash表详解

2.HashMap内部原理及实现

3.Comparable和Comparator的区别?

面试题

HashMap、LinkedHashMap和TreeMap的区别是什么?

在这里插入图片描述

HashMap:

        HashMap实现Map接口,其中key的值没有顺序,HashMap无序,且不可重复

LinkedHashMap:

        LinkedHashMap继承于HashMap,它比 HashMap 多维护了一个双向链表,因此可以按照插入的顺序从头部或者从尾部迭代,是有序的。不过因为比 HashMap 多维护了一个双向链表,它的内存相比而言要比 HashMap 大,并且性能会差一些,但是如果需要考虑到元素插入的顺序的话, LinkedHashMap 不失为一种好的选择。LinkedHashMap保证的顺序分为插入顺序访问顺序

  • 插入顺序:按照插入的顺序把数据放在链表尾部,插入的是什么顺序,读出来的就是什么顺序
        linkedHashMap.put("name1", "josan1");
        linkedHashMap.put("name2", "josan2");
        linkedHashMap.put("name3", "josan3");

在这里插入图片描述
插入排序的场景:

       mybatis的xml文件中使用java.util.hashMap去映射返回的数据,这些数据是无序存放在map中的,当使用poi进行excel数据导出时,要求严格按照select 后边的字段顺序去映射到map结构,那么java.util.hashMap就无法做到,需要使用java.util.LinkedHashMap按照插入顺序去保证一个有序性!

  • . 访问顺序 :就是说你访问了一个key,这个key就跑到了最后面。LinkedHashMap提供了可以设置accessOrder=true的构造方法来改变插入顺序为访问顺序
        // 第三个参数用于指定accessOrder值
        Map<String, String> linkedHashMap = new LinkedHashMap<>(16, 0.75f, true);
        linkedHashMap.put("name1", "josan1");
        linkedHashMap.put("name2", "josan2");
        linkedHashMap.put("name3", "josan3");
        
        System.out.println("通过get方法,导致key为name1对应的Entry到表尾");
        linkedHashMap.get("name1");

在这里插入图片描述
TreeMap:

        与 HashMap 不同, TreeMap 的底层就是一颗红黑树,它的 containsKey , get , put 和 remove 方法的时间复杂度是 log(n) ,并且它是按照 key 的自然顺序(或者指定排序)排列。TreeMapkey的默认的排序为升序,如果要改变其排序可以自己写一个类实现Comparator接口,并重写compare方法

结论:

  • 当我们希望快速存取<Key, Value>键值对时,我们可以使用HashMap
  • 当我们希望遍历时,元素是按插入顺序或者访问顺序排好序的,我们可以使用LinkedHashMap
  • 当我们希望自由定制元素的排序规则,或者按照key的自然顺序对Map中的元素进行排序时,我们可以使用TreeMap

但是上述Map类型都是线程不安全的!那么在多线程场景下如何保证线程安全呢?

  • 当我们希望在多线程并发存取<Key, Value>键值对时,我们会选择ConcurrentHashMap
  • 当我们需要多线程并发存取<Key, Value>数据并且希望保证数据有序时,不好意思,JDK没有提供ConcurrentTreeMap这么好的数据结构给我们。我们可以使用JDK1.6里面引入的ConcurrentSkipListMap跳表来解决,它相当于TreeMap的多线程版本。点击查看ConcurrentSkipListMap跳表原理!
            
            
            

①:为什么hashmap每次扩容大小为2的n次方?

        因为hashmap在put元素时,会先根据entry的key的hash值 和 数组的长度-1做一个&与运算,得到一个数组下标,目的是为了确定这个entry存储在数组的哪个位置上。

        但前提是保证数组下标不超过数组的长度,比如hashmap的初始长度是16,在put第一个元素时,下标应该在0-15之间取值。
就以16的数组长度为例, 看一下二进制&运算的过程:

15和key的hash进行运算:

					 高四位 	   低四位
				
				150000		1111	
        	  hash:  0101		0101   (随机hash值)	
	   15和hash &0000		0101    = 6  (15放在数组中索引为6的位置)

16和key的hash进行运算:

					 高四位 	   低四位
					 
				160001		0000
        	  hash:  0101		0101   (随机hash值)	
	   15和hash &0001		0000    = 1615只能放在索引为016的位置)

        可以看到如果是16的二进制和key的hash进行运算后,得到的是永远是0001 0000 = 16 的下标,已经超出(>=)数组的下标了,明显不合理。
所以hashmap源码中有两个关键点来解决这个下标越界问题!

①:使用了 hash & (length -1)的操作,(length-1)使低四位全是 1111,这样与hash进行与运算时,即可保证存储位置的下标在数组的最大长度之内。

②:如何保证(length-1)使低四位全是 1111…的呢?那就必须保证数组的大小是2ⁿ,对应的二进制数为 0001 0000 ,2 ⁿ - 1 对应的二进制数为 0000 1111,所以hashmap的数组大小一般为2ⁿ


面试题:为什么扩容因子会是0.75,为什么不是0.5或1?

        针对这个问题,在HashMap的源码注释中已经做出了解释:
在这里插入图片描述

  1. 如果扩容因子为0.5:那么每使用一半的容量就要进行扩容,但这也意味着存储元素时,hash碰撞机会较少,一定程度上减少了链表的长度,我们知道遍历链表获取元素的时间复杂度是O(n),所以扩容因子为0.5可以增加查找效率。但如果有4G容量的话,0.5的扩容因子会导致一般空间不可用,造成了空间浪费!以空间换时间!
  2. 如果扩容因子为1:那么当数组长度满了才会进行扩容。由于hash碰撞是随机的,那此时的链表长度可能非常大,这样就会影响hashmap的查找效率!但节省了空间。以时间换空间!

所以,扩容因子为0.75作为一个折中方案使用在java的hashmap中,这个0.75是由stackoverFlower上的一个牛人使用牛顿二项式计算得到,每种语言的map的扩容因子不尽相同,比如.net的扩容因子就是0.68!

jdk1.8中:当链表长度大于8且数组容量大于64的时候才会把链表转为红黑树,否则优先扩容
在这里插入图片描述



## ②:hashMap进行put操作时,key的hash值是怎么生成的?

①:调用key的hashcode() 方法
②:对hash的二进制数进行一些右移以及异或运算,使hash值分布更加分散,提升get的效率,减少hash碰撞
在这里插入图片描述
jdk1.7的hashMap源码
在这里插入图片描述


③:jdk1.7的hashmap的扩容操作是在元素插入之前还是之后?

答:之后
在这里插入图片描述


④:hashmap可以存入key为null的元素吗?如果能是怎么存储的?

答:可以存储。如果key为null,源码中会做一次判断,key为null的时候,hash值为0,存储在数组中的固定位置,当传入第二个key为null的数据时,会替换原来的null数据,不会产生链表,因为key为null的hash值固定!


⑤:hashmap为什么要扩容,jdk1.7和1.8的扩容流程是什么样的?

        扩容是为了增加底层的数组长度,避免每个节点的数据链表过长,造成查找效率低下。当 数据长度 > 数组长度 x 负载因子 时会进行扩容。

        jdk1.7过程是:先创建一个长度为原来数组的2倍的 新数组。再把原来数组的数据挪到新数组中,方法是遍历每一个老数组的链表节点,重新计算老数组中的key的hash与新数组长度-1做&运算,确定在新数组中的位置,利用头插法以此放置节点中的链表数据。由于老数组中的所有数据要与新数组的legth-1重新做hash运算来决定在新数组中的位置,当数据过大时,扩容效率低下

        jdk1.8过程是:把原数组中的链表元素的hash值的二进制 与 原数组容量(16)的二进制做 &运算,这个运算只有两个结果:要么为0,要么为原数组容量值(16),根据这两种结果把原数组中的值分为高位和低位两个链表。低位在新数组中的位置和原数组一致,高位在新数组中的位置等于原数组的位置+原数组容量。比如原数组中下标为3的链表经过&运算,分为高位和低位,低位存放在新数组中3的位置,高位存放在新数组中16+3=19的位置!


⑥:JDK1.8 和 JDK1.7的hashmap为什么线程不安全(重点)?

        首先,在HashMap设计之初,目标是简单高效,就没有采取任何措施保证put、remove 操作的多线程安全。所以无论1.8还是1.7的hashMap都无法保证线程安全!

        为什么线程不安全呢?比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算所要落到数组的哪个位置,然后执行插入,但在插入之前,此时线程A的时间片用完了,而此时线程B被调度得以执行。和线程A一样执行,只不过线程B成功将记录插到了桶里面。假设线程A插入的记录计算出来的索引位置和线程B要插入的记录计算出来的索引位置是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然认为它要插入,对线程B已经插入的事情一无所知,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

        jdk1.7的hashMap在多线程环境下,自动扩容时还会出现链表闭环(死循环)。
jdk1.7死循环的核心代码如下:

//jdk1.7中的hashMap扩容伪代码
void transfer(Entry[] newTable, boolean rehash) {

    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
    	// e 代表链表上的当前节点 
        while(null != e) {
            //第一行:取出链表上当前节点的next节点
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //第二行:对e重新进行hash运算,得到e在新数组上的位置
            int i = indexFor(e.hash, newCapacity);
            //第三行:e:当前节点 断掉原数组的next指针,并指向新数组的位置,第一次执行时newTable[i] = null
            e.next = newTable[i];
            //第四行:把e 放在新数组上的位置newTable[i]上
            newTable[i] = e;
            //第五行:原数组链表上第一个节点头插完毕
            // 把 这个节点的next节点,也就是上述第一行代码取到的next节点,赋值给e,重新进行下一轮头插!
            e = next;
        }
    }
}

        单线程模式下,遍历某个数组位置上的链表,通过头插法(头插法同样适用于jdk1.7中的hash碰撞),插入新的数组中去,老数组中链表的头部变为新数组的尾部,这个过程在单线程中没有任何问题!头插法过程置换了链表中每个节点的next属性,原数组中的链表头节点在新数组中的next为null
在这里插入图片描述

        但是在多线程模式下,如果线程A在执行完核心代码第一行时,cpu被线程B抢去,此时线程A的e和next属性的指向的都是原数组。等到线程B执行完毕,链表已经经过头插法挪进了新的数组中,此时线程A拿到cpu执行权,但线程A的e和next属性的指向的已经是新数组中经过倒置的链表元素,如果线程A继续执行的话,根据代码逻辑会把尾节点的next属性指向头节点,造成闭环,当下一个元素新增时,e 不可能为null,就造成了死循环。
在这里插入图片描述
        jdk1.8扩容时针对闭环问题作出了优化,所以jdk1.8已经不存在闭环导致的死循环问题了!部分代码如下:

        else { // preserve order
            HashMap.Node<K,V> loHead = null, loTail = null;
            HashMap.Node<K,V> hiHead = null, hiTail = null;
            HashMap.Node<K,V> next;
            do {
                next = e.next;
                if ((e.hash & oldCap) == 0) {
                    if (loTail == null)
                        loHead = e;
                    else
                        loTail.next = e;
                    loTail = e;
                }
                else {
                    if (hiTail == null)
                        hiHead = e;
                    else
                        hiTail.next = e;
                    hiTail = e;
                }
            } while ((e = next) != null);
            if (loTail != null) {
                loTail.next = null;
                newTab[j] = loHead;
            }
            if (hiTail != null) {
                hiTail.next = null;
                newTab[j + oldCap] = hiHead;
            }
        }

        jdk1.8与1.7不同的是不在让原数组的元素与新数组的length-1做rehash()运算,而是采用了高低位的思想,低位在新数组中的位置和原数组一致,高位在新数组中的位置等于原数组的位置+原数组容量。不仅节省了大量hash计算的时间,还巧妙的避过了jdk1.7存在的死循环问题。

        具体做法是见上一题!

完全线程安全的map集合是concurrentHashMap!


⑦:JDK1.8 和 JDK1.7的hashmap有什么不同?

①:jdk1.8把entry对象换成Node,但是Node本身还是继承的Entry。
②:jdk1.8在链表长度大于等于8时,会变链表为红黑树。
③:jdk1.8如果在链表中put元素时,是遍历链表所有数据,在尾部插入新数据,称为尾插法。jdk1.7在链表中put元素时,是通过头插法,整体链表下移来插入元素的。
④:jdk1.8先增加元素,再判断是否需要扩容。jdk1.7先判断是否扩容,扩容完成,才新增元素
⑤:jdk1.7在多线程扩容、迁移数据时会出现链表闭环,造成死循环。jdk1.8利用高低位的思想,不会出现死循环!

⑧: jdk1.8 hashMap底层原理,结合源码分析

先说HashMap的Put⽅法的⼤体流程:

  1. 根据Key通过哈希算法与与运算得出数组下标
  2. 如果数组下标位置元素为空,则将key和value封装为Entry对象(JDK1.7中是Entry对象,JDK1.8中是 Node对象)并放⼊该位置
  3. 如果数组下标位置元素不为空,则比较hashequals方法,如果相等,直接替换掉该元素,如果不相等,则要分情况讨论
    1. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进⾏扩容,如果不⽤扩容就⽣成Entry对 象,并使⽤头插法添加到当前位置的链表中
    2. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红⿊树Node,还是链表Node
      1. 如果是红⿊树Node,则将key和value封装为⼀个红⿊树节点并添加到红⿊树中去,在这个过 程中会判断红⿊树中是否存在当前key,如果存在则更新value
      2. 如果此位置上的Node对象是链表节点,则将key和value封装为⼀个链表Node并通过尾插法插 ⼊到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插⼊到链表中,插⼊ 到链表后,会看当前链表的节点个数,如果⼤于等于8,那么则会将该链表转成红⿊树
      3. 将key和value封装为Node插⼊到链表或红⿊树中后,再判断是否需要进⾏扩容,如果需要就 扩容,如果不需要就结束PUT⽅法

jdk1.8的源码如下:

  public V put(K key, V value) {
  		//通过hash计算得到key的hash值 传入putVal()
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
                   
        //hashmap的数组变为Node数组
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断数组是否为空,如果为空初始化数组为16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //通过数组长度和hash值的&运算,决定Node在数组中的位置
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//如果tab[i]位置上没值,直接把传入的Node放到这个位置上
            tab[i] = newNode(hash, key, value, null);
        else {
        	
            Node<K,V> e; K k;
            
            //如果tab[i]位置上有值,比较两者hash和equals()方法是否相等,如果相等,把传进来的元素赋给临时变量e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
            	//如果当前节点数据结构是红黑树,把传进来的元素放进红黑树中,并返回临时变量e
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            	//上边都不成立的话,就是链表结构。遍历链表
                for (int binCount = 0; ; ++binCount) {
                	//如果链表只有一个元素,则p.next为null.
                    if ((e = p.next) == null) {
                    	//把传进来的元素通过尾插法插入链表尾部
                        p.next = newNode(hash, key, value, null);
                        //判断链表长度是否大于8
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        	//如果大于8,创建红黑树
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果链表有多个元素,就把传进来的key的hash与每一个链表上的key的hash和equals()方法进行比较,
                   // 如果相等,把传进来的元素赋给临时变量e
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //收集上边的临时变量e,如果e不为null,就说明传进来的key的hash值和原来数组位置上的key的hash值一样,
            //那么传进来的Node,就会替代原来位置的Node,并返回原来位置上key的value!!
            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)
        	//扩容,使用高低位思想,解决1.7线程不安全问题
            resize();
        afterNodeInsertion(evict);
        return null;
    }


⑨:ConcurrentHashMap是怎么保证线程安全的?

1. ConcurrentHashMap和HashMap的区别?

操作ConcurrentHashMapHashMap
无锁无锁
加入了同步机制(分段锁)无锁
扩容多线程扩容,效率更高单线程扩容


2. jdk1.7的ConcurrentHashMap

ConcurrentHashMap是在put、remove操作的时候才会加锁(synchronized),get操作是不会加锁的

在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组(Segment[k,v]类似于 entry[k,v] 和 Node[k,v])和多个HashEntry组成,利用分段锁技术来(基于ReentrantLock实现)保证线程安全,如下图所示:
在这里插入图片描述
        Segment数组的意义:就是通过构建一个Segment数组,将HashMap的一个大的table分割成多个小的table来进行分段加锁,当多个线程竞争时,不会锁住整个table,而是锁住对应segment,当进行put操作时,不同的segment之间因为不是一把锁,所以其他的segment操作不受影响,粒度相对于锁整个table较小(锁住整个table是HashTable做的事,效率非常低),但相对于jdk1.8的ConcurrentHashMap锁的粒度又稍大了一些,因为在这里锁的是多个链表,而jdk1.8的ConcurrentHashMap锁的是单个链表!

jdk1.7中ConcurrentHashMap的put操作:

        对于ConcurrentHashMap的数据插入,这里要进行两次Hash去定位数据的存储位置

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

        从Segment的继承体系可以看出,Segment实现了ReentrantLock,也就带有锁的功能,当执行put操作时,会进行第一次key的hash来定位Segment的位置,如果该Segment还没有初始化,即通过CAS操作进行赋值。然后进行第二次hash操作,找到相应的HashEntry的位置,这里会利用继承过来的锁的特性,在将数据插入指定的HashEntry位置时(链表的尾端),会通过继承ReentrantLock的tryLock()方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用tryLock()方法去获取锁,超过指定次数就挂起,等待唤醒。

jdk1.7中ConcurrentHashMap的get操作:

        ConcurrentHashMap的get操作跟HashMap类似,只是ConcurrentHashMap第一次需要经过一次hash定位到Segment的位置,然后再hash定位到指定的HashEntry,遍历该HashEntry下的链表进行对比,成功就返回,不成功就返回null。

jdk1.7 的 ConcurrentHashMap 使用分段锁保证并发安全
在这里插入图片描述


3. jdk1.8的ConcurrentHashMap

        JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。数据结构是数组+链表+红黑树
在这里插入图片描述

ConcurrentHashMap拥有出色的性能, 在真正掌握内部结构时, 先要掌握比较重要的成员:

  • MIN_TRANSFER_STRIDE: 默认16, table扩容时, 每个线程最少迁移table的槽位个数。
  • MOVED: 值为-1, 当Node.hash为MOVED时, 代表着table正在扩容
  • TREEBIN:值为-2, 代表此元素后接红黑树。
  • nextTable: table迁移过程临时变量, 在迁移过程中将元素全部迁移到nextTable上。
  • SIZECTL: 用来标志table初始化和扩容的,不同的取值代表着不同的含义
  • SIZECTL = 0:代表table还没有被初始化
  • SIZECTL = -1:代表table正在初始化
  • SIZECTL = 0:代表table正在扩容
  • SIZECTL = 0:代表table初始化完成

其他的和HashMap的成员变量差不多!

ConcurrentHashMap添加元素流程:

  1. 检查map是否初始化:因为Map的初始化都是在put方法中,所以首先要检查Map是否被初始化。ConcurrentHashMapHashMap不同的是:ConcurrentHashMap考虑到在初始化时也可能发生多线程竞争,所以ConcurrentHashMapinitTable方法中采用了CAS来保证只有一个线程对map进行初始化!

  2. 如果要存储的位置上为null,使用CAS解决HashMap的线程安全问题,上面说过,当发生hash冲突时,HashMap会存在线程安全问题,表现是:A线程要存储的元素被其他线程的元素顶掉。

    • jdk1.7的 ConcurrentHashMap通过ReentrantLock加锁的方法,保证同时只有一个线程进行存储
    • jdk1.8的 ConcurrentHashMap通过CAS无锁自旋的方式,同样保证只有一个线程进行存储,其他线程自旋等待下次执行,下次存储时发现要存的位置上已有元素,则通过链表追加到后边。这种CAS算法比jdk1.7的 ConcurrentHashMap更加轻量!
  3. 如果要存储的位置上已有元素,则通过加synchronized锁,锁的对象是当前位置的第一个元素。

    • 然后通过hashequals()判断链表或红黑树上的元素是否相等,如果相等,则替换、覆盖。
    • 如果不相等,则通过尾插法,向链表尾部插入新的元素!
    • synchronized锁防止多线程下,造成链表或树上数据的丢失、覆盖!这个synchronized锁的粒度同样比1.7的reentrantLock小,因为1.7的reentrantLock锁的是多个链表
  4. 如果插入数据时刚好触发扩容,也就是说当前table的状态正好等于MOVED,说明正在扩容。那么此时插入数据的put线程会暂停put操作,并帮助扩容线程一起对数组进行扩容,但扩容线程数和每次迁移的槽位个数都有限制。 ConcurrentHashMap的多线程扩容也是比普通的hashMap单线程扩容效率要高很多!

put添加元素的源码如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //生成key的hash
        int hash = spread(key.hashCode());
        int binCount = 0;
        //因为下边用了CAS,所以此处有死循环CAS重试!! hashMap没有这个循环
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //如果第一个线程进来发现table未初始化,执行table的初始化方法
            if (tab == null || (n = tab.length) == 0)
            	//第一个线程对table初始化:使用CAS无锁算法,修改SIZECTL = -1。其他线程修改失败自旋!
                tab = initTable();
            //table初始化完成后,通过&运算找到存放位置后,分三种情况
            //①:如果位置上没有其他元素
            //则通过CAS无锁算法放入新元素,CAS竞争失败的线程循环重试,此时该位置已有元素,则不会进入esle if 判断!
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            //②:该数组正在进行扩容 
            // MOVED, 代表着table正在扩容
            else if ((fh = f.hash) == MOVED)
            	//当前线程发现数组正在扩容,会帮助扩容线程一起扩容!
            	//具体做法是:每个线程分配16个槽位,分工合作,一起迁移数据
                tab = helpTransfer(tab, f);
            //③:该位置已有其他元素
            else {
                V oldVal = null;
                //加synchronized锁,锁的对象是当前位置的第一个元素
                //加锁防止多线程下,造成链表或树上数据的丢失,可能被覆盖!
                //锁的粒度比1.7的reentrantLock小,因为1.7的reentrantLock锁的是多个链表
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    	//如果是链表,走下面的逻辑
                        if (fh >= 0) {
                            binCount = 1;
                            //当前线程拿到锁之后,遍历数组当前下标位置的链表,比较hash值
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //如果hash值和equals()方法都相等,则替换掉原来的value
                                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;
                                //直到遍历到最后一个元素(next==null),发现都不相等
                                //通过尾插法把传进来的数据放入链表尾部
                                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;
    }

⑩:不同版本ConcurrentHashMap的总结和对比

        可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,相对而言,总结如下:

  1. 1.8的锁的粒度更细。JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry。
  2. 1.7使用ReentrantLock实现分段锁,1.8基于synchronized关键字实现分段锁+CAS保证线程安全

JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?

  1. jdk1.8锁的粒度变低,相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差。在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了。
  2. 基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然。
  3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也可能是一个选择依据


⑩:set.contains() 和 list.contains()方法的注意点

set.contains() 遇到的问题:

	import java.util.HashSet;
	
	class Dog{
	    String color;
	
	    public Dog(String s){
	        color = s;
	    }   
	}
	
	public class SetAndHashCode {
	    public static void main(String[] args) {
	        HashSet<Dog> dogSet = new HashSet<Dog>();
	       
	        dogSet.add(new Dog("white"));
	
	        System.out.println("We have " + dogSet.size() + " white dogs!");
	
	        if(dogSet.contains(new Dog("white"))){
	            System.out.println("We have a white dog!");
	        }else{
	            System.out.println("No white dog!");
	        }   
	    }
	}

输出为:

We have 1 white dogs!
No white dog!

程序中添加了一只白色的小狗到集合dogSet中. 且 size()方法显示有1只白色的小狗.但为什么用 contains()方法来判断时却提示没有白色的小狗呢?为什么呢?

        Java的API文档指出: 当且仅当 本set包含一个元素 e,并且满足(o==null ? e==null : o.equals(e)) 条件时,contains()方法才返回true. 因此 contains()方法 必定使用equals方法来检查是否相等. 但是仅重写equals方法contains()还是无法判断的,问题的关键在于 Java中hashCode与equals方法的紧密联系. hashCode() 是Object类定义的另一个基础方法

        equals()与hashCode()方法之间的设计实现原则为:如果两个对象相等(使用equals()方法),那么必须拥有相同的哈希码(使用hashCode()方法). hashCode()的默认实现是为不同的对象返回不同的整数,在上面的例子中,因为未定义自己的hashCode()实现,因此默认实现对两个对象返回两个不同的整数,这种情况破坏了约定原则

解决办法:Dog类需要重写hashCode() 和 equals() 方法

list.contains() 遇到的问题:

在这里插入图片描述
        UserVo是一个类,userlist是一个ArrayList,我在每次往userlis中add数据的时候,我都要先判断一下,userlis中是不是已经存在同样的数据,如果存在,则不插入,如果不存在,我就add就去。起初我没有重写equals()和hashCode()方法,每次的判断都是false,就是都能插进去。以下是在UserVo中重写了这两个方法,问题完美解决。

在这里插入图片描述


⑾:hashMap与hashTable的区别?

        HashTable是较为远古的使用Hash算法的容器结构了,现在基本已被淘汰,单线程转为使用HashMap,多线程使用ConcurrentHashMap。 而TreeMap能够把它保存的记录根据键排序,默认是按升序排序

        HashTable的操作几乎和HashMap一致,主要的区别在于HashTable为了实现多线程安全,在几乎所有的方法上都加上了synchronized锁,而加锁的结果就是HashTable操作的效率十分低下。

HashTable与HashMap对比

  • (1)线程安全:HashMap是线程不安全的类,多线程下会造成并发冲突,但单线程下运行效率较高;HashTable是线程安全的类,很多方法都是用synchronized修饰,但同时因为加锁导致并发效率低下,单线程环境效率也十分低;
  • (2)插入null:HashMap允许有一个键为null,允许多个值为null;但HashTable不允许键或值为null;
  • (3)容量:HashMap底层数组长度必须为2的幂,这样做是为了hash准备,默认为16;而HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;
  • (4)Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行运算;
  • (5)扩容机制:HashMap创建一个为原先2倍的数组,然后对原数组进行遍历以及rehash;HashTable扩容将创建一个原长度2倍的数组,再使用头插法将链表进行反序;
  • (6)结构区别:HashMap是由数组+链表形成,在JDK1.8之后链表长度大于8时转化为红黑树;而HashTable一直都是数组+链表;
  • (7)继承关系:HashTable继承自Dictionary类;而HashMap继承自AbstractMap类;
  • (8)迭代器:HashMap是fail-fast(快速失败机制);而HashTable不是。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值