Map结构面试题总结

目录

1.hashMap得底层原理?(代补充)

2.ConcurrentHashMap 1.8得底层原理?

  2.1 前置知识

        2.1.1是什么?

        2.1.2结构

        2.1.3为什么使用红黑树

        2.1.4初始化参数

        2.1.5链表什么时候转红黑树?

         2.1.6Node源码分析:

  2.2 ConcurrentHashMap的DCL操作

  2.3 sizeCtl属性

  2.4 ConcurrentHashMap的散列算法

   2.5 ConcurrentHashMap的线程安全?

  2.6 计数器和size

  2.7 put方法分析

  2.8 扩容原理

        2.8.1 扩容时机

        2.8.2核心属性sizeCtl

        2.8.3扩容前的准备

3.总结

  1.ConcurrentHashMap如何保证线程安全的?

   2. ConcurrentHashMap和HashMap区别?


1.hashMap得底层原理?(代补充)


2.ConcurrentHashMap 1.8得底层原理?

  2.1 前置知识

        2.1.1是什么?

               ConcurrentHashMap解决了HashMap得线程不安全问题,如果出现并发读写操作时候,HashMap可能会出现丢失数据,为了保证安全得同时还能保证效率,采用ConcurrentHashMap。 

        2.1.2结构

                ConcurrentHashMap得存储结构和HashMap结构式一样得,都是数组+链表+红黑树

        2.1.3为什么使用红黑树

                红黑树更多是解决链表长度太长,查询性能o(n)的问题,而红黑树的时间复杂度是o(log n),写入性能会变慢因为会有树的左旋和右旋。维护红黑树的成本会很高,转红黑树概率为0.00000006,概率非常低。

        2.1.4初始化参数

                  数组初始化大小:16

                  负载因子:0.75 扩容的触发点。ConcurrentHashMap是不允许修改的,HashMap是可以修改的。

                 为什么负载因子是0.75?

                  (1)是基于泊松分布概率学决定的

                  (2)当0.5的情况下,hash冲突概率笑了,但是数组空间利用不足,当为1的情况下,数组空间利用率高了,但是hash冲突概率高了,并且红黑树概率也变高了

                  (3)0.75更符合二进制位运算


        2.1.5链表什么时候转红黑树?

                  数组长度大于64 and 链表长度大于8;当红黑树长度小于6情况转换链表。

                为什么要满足数组长度大于64?因为链表转红黑树是为了提升查询效率,但是当数组长度短的情况下,hash冲突就可能增加,所以当数组没达到这个限制的时候,先进行扩容操作。

                 什么时候扩容?其实是元素个数,当元素个数>数组大小*加载因子。

         2.1.6Node源码分析:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        //传入得key
        final K key;
        //传入得value
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }
        省略部分代码
       ....
    }

               通过源码中分析得出,我们在map.put(key,value),在底层实现就是使用Node节点进行存储得,此处注意点就是hash,key使用final进行修饰,为什么hash值和key使用final修饰,而val和next不用final修饰呢?

              解答:使用final域确保初始化安全性,初始化安全性,让不可变对象不需要同步就能自由的被访问和共享。

                  使用volatile修饰来保证某个变量内存的改变对其他线程即时可见。可以配合CAS实现不加锁对并发操作的支持。
                ConcurrentHashMap的get操作可以无锁,由于Node的元素val和指针next是使用volatile修饰的,在多线程环境下,A线程修改节点val或者新增节点对B线程都是即时可见的,保证了数据的一致性。

         个人看来,key和hash不牵扯到线程安全的问题,因为key如果变了在map中也就使用链表进行存储;为什么value和next需要呢,更多是因为value,next更新场景,保证可见性是防止其他线程修改时候要让其他线程可见。

  2.2 ConcurrentHashMap的DCL操作

             懒加载,初始化数组的时候使用了DCL保证线程安全。

private final Node<K,V>[] initTable() {
        //声明局部变量
        Node<K,V>[] tab; int sc;
        //数组没有初始化,才会进行while循环
        while ((tab = table) == null || tab.length == 0) {
            //正在初始化或者扩容 处理并发场景下同时初始化场景。
            if ((sc = sizeCtl) < 0)
                //线程等待,让出cpu时间片
                Thread.yield(); 
            //这里时>=0情况下,进行CAS操作,修改为-1,表示正在初始化
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    //双重检查的地方
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

  2.3 sizeCtl属性

             是控制数组的初始化和扩容的标识

               sizeCtl==-1:当前数组正在初始化

               sizeCtl<-1 当前数组正在扩容

               sizeCtl==0:刚刚new ConcurrentHashMap

               sizeCtl>0:代表下次扩容时的阙值

                                在new ConcurrentHashMap,指定数组初始化的长度

  2.4 ConcurrentHashMap的散列算法

                基于key的hashCode和数组长度做运算出来的。

                

tabAt(tab, i = (n - 1) & hash))

     上面代码:可以简略成 Node f=table[(n-1)&hash]

       可以计算出数组的索引位置上。

        hash值又怎么求出来的呢?

            

int hash = spread(key.hashCode()); 
return (h ^ (h >>> 16)) & HASH_BITS;

       其实将key的hashCode值,为了降低hash冲突,hash散列,使用高16位和低16位进行异或(相等为1,不能为0)操作。

   为什么spred还要有一个与运算?

    保证线程安全。

static final int MOVED     = -1; // hash for forwarding nodes 正在迁移数据
static final int TREEBIN   = -2; // hash for roots of trees 当前索引位置为红黑树
static final int RESERVED  = -3; // hash for transient reservations 当前索引位置被占用了,但是还没有值
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

   2.5 ConcurrentHashMap的线程安全?

               只保证写写操作。不保证读写操作的一致性和安全些。

               1.要插入到数组上时,基于CAS保证线程安全

               2.要挂到链表或者红黑树时,基于synchronized保证线程安全

  2.6 计数器和size

              记录的当前元素个数,需要保证效率。

              synchronized :太重

              cas或者Atomic:当并发过大,线程cas失败会重试,以自旋的方式不断浪费cpu资源。

             LongAdder,基于分段的形式,不单使用BaseCount,还基于一个CounterCell数组存储

            基于CPU内核数决定CounterCell数组的长度。

            就是将BaseCount+CounterCell数组的值累加。

private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

  2.7 put方法分析

                1.首先判断key,value是否为空,为空抛出异常

                2.计算key hash值,key.hashCode(),利用高16位异或低16位运算并保证是正数

                 2.判断是否需要初始化,如果需要进行初始化操作。利用DCL+CAS保证初始化线程安全

                 3.计算出数组的索引值,判断数组索引位置是否为空,为空使用cas进行赋值操作

                  4.判断当前是否在迁移操作,如果在迁移帮助迁移

                  5.进行上锁操作,上锁为第一个索引位置上的元素,使用synchronized保证线程安全

                  6.判断链表情况下,从头开始遍历,如果找到相同的key,进行值替换,否则追加到最后一个位置上

                7.判断是否是红黑树,进行红黑树元素替换或者新增

                8.判断链表长度是否符合转红黑树条件,符合转红黑树,在这里会有一种情况,当链表长度大于等于8数组长度不满足64的情况下会触发扩容操作。

               9.进行计数操作,利用LangAdder保证线程安全,如果数量达到阙值,会进行扩容操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
		//1.进行判断key和value是否为空,为空抛出异常
        if (key == null || value == null) throw new NullPointerException();
        //2.计算hash值,主要通过高16位异或低16位,并且保证hash值为正数
		int hash = spread(key.hashCode());
        int binCount = 0;
		
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
			//3.判断数组是否需要初始化,如果需要初始化,进行初始化;使用DCL+CAS保证线程安全
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
			//4.判断数组索引位置是否为空,为空进行赋值操作,使用CAS保证线程安全
            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
            }
			//5.如果当前正在迁移,当前线程帮助扩容操作,提高扩容性能,主要是通过步长进行计算
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
			//6.获取当前数组索引位置,进行上锁操作。锁的是数组索引第一个位置使用的是synchronized。
                V oldVal = null;
                synchronized (f) {
					
                    if (tabAt(tab, i) == f) {
					    //7.如果是链表,从头查找hash相同,key相同的,如果找到了,进行值替换,否则在最后一个元素插入新数据
                        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;
                                }
                            }
                        }
						//8.如果是红黑树,进行红黑树处理
                        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;
                            }
                        }
                    }
                }
				//9.判断是否转红黑树,数组长度大于64and 链表长度大于等于8,在这里有种特殊情况,就是链表长度大于8数组长度不满足64情况下,也会进行扩容操作。
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
		//10.将size加1,利用LongAdder原理保证计数器安全,如果元素个数大于阙值,进行扩容操作。
        addCount(1L, binCount);
        return null;
    }

  2.8 扩容原理

        2.8.1 扩容时机

                  (1)数据数量达到阙值  

                  (2)链表长度大于等于8and数组长度大于64情况

                      

                  (3)putAll时,传入的map的长度经过运算大于当前阙值

                

        2.8.2核心属性sizeCtl

                sizeCtl<-1:代表正在扩容

                       高16位:代表当前扩容的标识戳,跟oldTable长度有关。

                      低16位:代码当前参与扩容的线程个数有关 -1.

                 sizeCtl>0:下次扩容的阙值或者初始化的长度

        2.8.3扩容前的准备

            

//n代表旧数组的长度 sc代表sizeCtl
//在基于n计算扩容标识戳
int rs = resizeStamp(n);
/**resizeStamp 方法内容 return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
* numberOfLeadingZeros 计算出当前到前面0的个数
  1 << (RESIZE_STAMP_BITS - 1)  保证左移16位,符合位是1,为负数
**/
if (sc < 0) {
     if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
         sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
           transferIndex <= 0)
             break;
              if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
       }
       else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                            (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
              

            计算出来扩容2倍,为了帮助扩容,其他线程也会帮助扩容

3.总结

  1.ConcurrentHashMap如何保证线程安全的?

        1.初始化数组的时候,使用DCL 双重检查锁,利用CAS的方式

         2.使用volatile修饰一些参数,保证线程可见性,有序性。

         3.要插入到数组上时,基于CAS保证线程安全

          4.要挂到链表或者红黑树时,基于synchronized保证线程安全

          5.计数器基于longAdder,BaseCount+CountCell数组

   2. ConcurrentHashMap和HashMap区别?

     (1)ConcurrentHashMap不允许key和value都为空,hashMap时允许的

ConcurrentHashMap:
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
}

        2.hash计算不同

ConcurrentHashMap:
int hash = spread(key.hashCode());
static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
HASH_BITS:01111111 11111111 11111111 11111111 
与运算:必须保证hash值为正数。
HashMap:
 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

      

         3.ConcurrentHashMap线程安全,HashMap线程不安全

            参考1.ConcurrentHashMap如何保证线程安全的?

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值