• 常用集合


    常用集合

    一、HashMap

    1、hash方法效率

    1.7时hash方法分为三步:获取hashcode,位运算和异或扰动、取模。1.8优化为获取hashcode、位运算和异或扰动、用位运算取模,1.8时扰动方法直接优化成了高16位异或低16位,且通过位运算取模hash&(n-1)替代取模,提高了计算效率。

    2、链表的插入顺序

    hashMap在1.7时采用头插法,发生碰撞时新元素被添加在对应位置的头部,在rehash时也是如此,所以在1.7中rehash一次元素和原来比就会发生倒置。1.7也正因此有可能在多线程下因为两个线程同时扩容导致死循环的问题。如:

    扩容时新数组先生成好,然后每个entry逐个移动到新的数组中。移动过程中分为三步,原链表的头entry指向新数组对应位置的新entry2,然后把原链表的头entry放到新数组的对应位置替代entry2,最后把要处理的entry更新为旧链表中的下一个entry。

    现在有这样一个场景原来链表有1->2->3->4要把它移动到新的数组,假如线程1要执行步骤1还没执行,此时被线程2抢走执行权,线程2一口气挪动了两个元素,旧链表就变成了3->4,此时新链表就是2->1,然后线程1抢过执行权,执行步骤1,也就是它要调整的元素1指向现在数组的新entry2,所以这样就形成了1和2之间循环指针。

    1.8改进成了尾插法,避免了这个问题

    3、rehash算法改进

    hashMap1.8改进了rehash的算法,之前是要把每个key重新计算一遍然后确定桶的位置然后再更新,现在只需要确定新增位是0还是1就可以了,如果是1,该key的位置就是原位置+原来容量,如果是0,就是原位置不用移动。

    如数组从16扩容到32,原来key1的hash1,key2的hash2,计算桶的位置(n-1)&hash:

    也就是1111逐位与hash1,和hash2计算各自位置,扩容后变为11111逐位与hash1和hash2,后四位结果一致,关键就在第5位hash是不是1,如果是1,那就要原位置+16得到新位置。

    4、红黑树

    阈值为8。

    5、put方法:首先找到对应的桶,然后如果是空的就包装成entry放入其中,如果已经存在就判断是否是树节点,如果是就按照树节点方法插入,否则就判断是否链表长度大于8,如果是就转换为红黑树然后插入,否则就按照链表插入方法,在头或尾插入。最后判断是否需要扩容。

    6、装填因子:默认为0.75

    7、初始容量:16

    二、HashTable

    线程安全效率低,不可以存null键,底层是数组+链表。

    线程安全的实现通过synchronized的全表锁,一个线程添加元素另一个线程不能做任何操作。

    初始容量为11,每次扩容为2n+1

    三、ConcurrentHashMap

    concurrentHashMap是又多个segment组成的,每个segment就是一个子hash表,每一个数据单元就是一个entry,entry有一个字段next,形成一条链表。分段锁是它最大的特点,不像hashtable一把全表锁,这种分段锁可以同时支持多个线程操作不同的段。所有的操作都是先定位到segment然后再定位到具体的桶然后进行搜索的。

    1、remove方法:

    需要先获取分段锁,因为每个entry链表的next都是final的,这就导致链表不能从中间开始改变,每次只能在链表的头部更新,所以整个的remove操作实际上是找到要删除的点,然后从头部开始遍历整个链表,把除了该点意外的所有entry都移动到桶的新头部上。

    2、get方法:

    不会加锁。先判断count是否为0,如果对应entry的value不为空就返回,如果为空就加锁重读。这是因为其他修改的方法有可能造成指令重排导致读到空结果。get方法用到的共享变量都是volatile,包括segment的count、entry的value(volatile可以保证读写同时到来时先写,读的线程就能返回修改过的值)。对链表的遍历不需要加锁的原因就是next是final的,但是有可能返回过时的头结点,get方法是有可能返回错误结果的。get可能返回非最新的数据。

    3、put方法:

    首先定位到segment,然后判断是否需要扩容,扩容这个动作只在本segment内完成,然后再插入对应位置entry链表的头部,访问共享变量都要加锁。

    4、size方法:

    先尝试两次不锁住segment来统计集合的大小,就是把segment中的count字段相加,如果统计的过程中元素个数发生变化就锁住后再统计其大小,用变量segment的modCount来确定统计过程中元素到底发生没发生变化,发生put、remove和clean时都会修改modCount。

    四、ArrayList

    arrayList总是在插入数据前检查是否需要扩容。它有一种有参构造可以直接指定初始容量。

    如果不指定初始容量。1.6会设置初始容量为10。1.7会设置初始容量为0,插入后扩充为10,两个版本都是扩容时变成原来的1.5倍。

    Arraylist实现了randomAccess接口,这是一个标识接口标识其具有随机访问特性,当使用binarySearch的时候,会根据是否可以随机访问用不同方法实现

    五、LinkedList

    1.6和1.7都是双向链表,1.6时需要一个空节点作为链表的起始点,是循环链表。1.7则不需要,仅仅在头和尾处设置了一个first和一个last指针。

    这样,1.7的改进在于:

    1、节省了一个空节点的开销

    2、在1.6中涉及链表头和尾的插入,由于循环链表的原因必须与空节点交互完成插入,需要修改4次指针。而1.7中由于不是循环链表只需要修改2次就可以完成。

    六、CopyOnWriteArrayList和CopyOnWriteArraySet

    这两个集合是线程安全的,它们保证线程安全的方式都是写时复制,用另外开辟空间的方法来解决线程安全问题。

    每次add时都会拷贝原有数组,然后在拷贝数组上进行添加,最后把拷贝数组的引用赋值给原数组。添加元素时原来的数组是没有变化的。add时要加互斥锁。

    读的时候是不会加锁的。

    要使用这个集合,首先数组不能太大,否则可能产生oom,且读到的值可能是旧的,不能用于实时读,最后它适合读多写少的情况。

    fail-fast和 fail-safe 的区别

    fail-fast机制:当单线程环境下,在遍历集合的同时对集合中内容进行修改,就会抛出Concurrent Modification Exception,此外在多线程下一个线程遍历集合,另一个集合修改时也会抛出异常。java.util包下的集合都是fail-fast的。

    实现:集合内部有一个变量,每次修改时都会修改这个变量的值,然后遍历时调用hasnext或者next方法时,会检查这个值是否被改变,如果改变了就抛出异常。

    这个特性不能用来检验是否并发修改,因为不能保证及时抛出,而且会有ABA问题。

    fail-safe机制:这类集合在遍历时都是遍历的复制体,在拷贝的副本上进行遍历。所以这种方式并发修改不会引起异常,但是在遍历期间发生的修改是检查不到的。java.util.concurrent包下集合都是这类的。

  • 相关阅读:
    (转)搜索Maven仓库 获取 groupid artifactId
    idea自用快捷键(非常实用)
    (2)一起来看下使用mybatis框架的insert语句的源码执行流程吧
    (1)复习jdbc操作,编译mybatis源码,准备为你的简历加分吧
    关于CPU核心,线程,进程,并发,并行,及java线程之间的关系
    数组排序
    泛型类、泛型方法、类型通配符的使用
    数组的三种声明方式总结、多维数组的遍历、Arrays类的常用方法总结
    Java基本数据类型总结、类型转换、常量的声明规范,final关键字的用法
    JAVA基础语法——标识符、修饰符、关键字(个人整理总结)
  • 原文地址:https://www.cnblogs.com/yinyunmoyi/p/11556728.html
Copyright © 2020-2023  润新知