zoukankan      html  css  js  c++  java
  • Java八股文——集合

    1. HashMap

      基本信息:

    • 数据结构:数据+链表,数组+链表+红黑树
      • jdk1.8中,当链表大小超过8时,就会转换为红黑树,当小于6时变回链表,主要是根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候转换,小于等于6转为链表
    • 默认大小:16
    • 负载因子:0.75(原因:0.75的负载因子,能让随机hash更加满足0.5的泊松分布

      默认容量为什么是16?

    • 当我们想要往一个hashmap中put元素的时候,需要通过hash方法计算出放到哪个桶中,hash方法是根据key来定位这个K-V在链表数组中的位置的,hash的公式:HashCode(key) & (length-1),其实就是取模.
      用位运算来代替取模,主要就是因为位运算的效率较高。例如:X % 2^n = X & (2^n -1),假设n为3,则2^3 = 8 ,表示为二进制就是1000,2^3 - 1 = 7,即0111,此时 X & (2^n -1)就相当于取X的二进制的最后三位数,从二进制角度来看,X / 8相当于 X >> 3 ,此时得到了X / 8 的商,而被移位的部分(后三位)就是 X % 8,也就是余数
      因此,如果保证map的长度是 2^n的话,就可以实现取模运算了
      那么为什么一定是16呢?
      关于这个默认容量的选择,官方没有给出相关解释,应该是一个经验值,需要在效率和内存使用上权衡,不能太小也不能太大
    • 并且,Hashmap在两个可能改变容量的地方做了兼容处理,一个是扩容,一个是初始化。
    • 当我们初始化Map且设置了容量时,HashMap不一定会采用传入的值,而是经过计算,得到一个新值,以提高hash效率,源码中的算法就是根据用户传入的容量值,得到第一个比他大的二次幂返回

      扩容:

    ·  扩容的阈值是负载因子 * 当前容量。

    1. 创建一个新的Entry数组,长度是原数组的两倍
    2. rehash:便利原Entry数组,将所有的Entry重新hash到新数组(重新hash是因为长度扩大之后,hash的值可能不同)

      Hashmap是怎么放入数据的?(put方法)

    • 首先判断key是否是null,是的话hash值就是0,获得hash值后进行扰动,1.7版本是5次异或4次位移,1.8是一次异或一次位移,然后根据计算出的新hash值找到对应的index,然后找到对应Node/Entity,遍历链表/红黑树,遇到hash值相同且equals相同的,则覆盖,不是则新增,如果节点数大于8就树化。put完成后,判断当前长度是否大于阈值,是就扩容
    • 对于链表插入,1.7之前是头插法,从1.8开始变成尾插法,主要是为了解决rehash出现的死循环问题,并且1.7的时候是先扩容再插入,而1.8是先插入后扩容。正常来说,如果先插入,就可能节点需要树化,会多一次损耗,个人猜测,是由于读写问题,hashmap并不是线程安全的,如果先扩容后插入,那么扩容期间是访问不到新放入的值的,所以先插入,在扩容期间是可以访问到值的

      为什么需要从头插法变成尾插法?

    • 在多线程的时候,如果不同的线程同时插入一个map,当达到扩容阈值时,两个线程同时触发扩容,而头插法在循环中会导致某个节点发生循环指向,后续查找元素过程中就会发生死循环

      HashMap线程不安全主要体现在:

    1. 1.7多线程环境下,扩容会造成环形链表或数据丢失
    2. 1.8多线程环境下,put方法会发生数据覆盖的情况

      如何处理HashMap的线程不安全的情况?

    1. 使用Collection.synchronizedMap()创建线程安全的map
      • 实现线程安全的原理:
        SynchronizedMap内部维护了一个普通对象map和排斥锁mutex,通过该方法创建出map之后,操作map就会以mutex对方法进行加锁,mutex默认是SynchronizedMap,可以通过构造参数传入
    2. HashTable
      • 所有数据操作方法都加锁,效率低不考虑
      • 不允许键值为null,HashMap可以
      • 使用安全失败机制(fail-safe)而HashMap使用快速失败机制(fail-fast),在安全失败机制下,如果使用null,会使得无法判断对应的key是不存在还是为空
    3. ConcurrentHashMap
      • 结构与HashMap相同,但是采用了CAS + synchronized来保证安全性
      • 最大容量:1 << 30
      • PUT操作:
        1. 根据key计算出hashcode
        2. 判断是否需要初始化
        3. 根据当前key定位的node,如果为空则可以写入,利用CAS尝试写入,失败则自旋保证成功
        4. 如果当前位置的hashcode == MOVED == -1,则需要扩容
        5. 如果都不满足,则利用synchronized写入数据
        6. 如果大于TREEIFY_THRESHOLD则需要转换为红黑树
      • GET操作
        • 直接根据计算出来的hashcode寻址,如果在桶上直接返回,如果是红黑树则按照树的方式获取,如果不满足则用链表方式遍历获取
      • 扩容
        • 1.7版本是基于segment,segment内部维护了HashEntity数组,所以扩容方式是在这个基础上的,类似HashMap扩容
        • 1.8版本扩容较为复杂,利用了ForwardingNode,先根据机器内核数来分配每个线程能分到的busket数(最小是16),这样可以做到多线程协助迁移,提升速度,然后根据自己分配的busket数来进行节点迁移,如果为空就放置ForwardingNode,代表迁移完成,如果是非空节点(判断是不是ForwardingNode,是就结束了),加锁,链路循环进行迁移
      • 为什么在Java8放弃了segment?
        • 由于在创建一个ConcurrentHashMap的时候,segment的数量就已经固定了,当需要进行扩容的时候,变化的是segment的大小,segment继承了ReentrantLock,如果segment变得很大了,那么锁的粒度就会变得很大,分段锁就没有意义了,每一个段就相当于一个同步的Map
        • 在java8中替换成了Node数组链表红黑树,并且因为ReentrantLock需要节点继承AQS来获得同步支持,会增大内存开销,因此java8不使用ReentrantLock,改为synchronized,synchronized是jvm直接支持的,能够在运行的时候进行相应的优化措施,比如锁粗化,自旋等

    2.List

    1. ArrayList

      • 底层使用数组实现,查找与访问较快,新增和删除较慢
      • 实现了RandomAccess接口,可以随机访问
      • 默认初始容量 10
      • 以1.5倍扩容
      • 调用构造函数时只会初始化数组大小,而size这个变量不会初始化,此时如果调用set方法指定下标设置数据会抛出数组越界异常,因为set方法中是根据size来判断是否可以设置当前下标的数据
      • 构造方法中,如果设置初始化大小为0,则数组扩容时,大小由0变为1
      • 无参构造方法中,数组默认大小也是0,但扩容时,大小由0变为10
    2. LinkedList

      • 底层使用链表实现,新增与删除较快,查找较慢
      • 内部维护了链表长度,以及头尾节点,获取长度不需要遍历
      • 实现了队列接口,具有队列的先进先出的功能
  • 相关阅读:
    Construct Binary Tree from Preorder and Inorder Traversal
    Construct Binary Tree from Inorder and Postorder Traversal
    Maximum Depth of Binary Tree
    Sharepoint 2013 创建TimeJob 自动发送邮件
    IE8 不能够在Sharepoint平台上在线打开Office文档解决方案
    TFS安装与管理
    局域网通过IP查看对方计算机名,通过计算机名查看对方IP以及查看在线所有电脑IP
    JS 隐藏Sharepoint中List Item View页面的某一个字段
    SharePoint Calculated Column Formulas & Functions
    JS 两个一组数组转二维数组
  • 原文地址:https://www.cnblogs.com/gtblog/p/13668103.html
Copyright © 2011-2022 走看看