敖丙思维导图-集合

敖丙思维导图系列目录

这些知识整理都是自己查阅帅丙资料(当然还有其他渠道)加以总结滴~ 每周都会更新知识进去。
如有不全或错误还请大家在评论中指出~


  1. 敖丙思维导图-集合
  2. 敖丙思维导图-多线程之synchronized\ThreadLocal\Lock\Volatitle\线程池
  3. 敖丙思维导图-JVM知识整理
  4. 敖丙思维导图-Spring
  5. 敖丙思维导图-Redis
  6. 敖丙思维导图-RocketMQ+Zookeeper
  7. 敖丙思维导图-Mysql数据库

本文章目录

  • 敖丙思维导图系列目录
  • HashMap
    • 初始化长度
    • 红黑树
    • 迭代器
    • 扩容机制
    • 为什么重写equals方法的时候需要重写hashCode方法呢?
    • 线程安全的实现
  • ConcurrentHashMap
    • jdk1.7
    • jdk1.8
    • put操作
  • ArrayLists和LinkedList
    • ArrayList遍历性能更快
    • 使用`Vertor、Collections.synchronizedList、CopyOnWriteArrayList` 确保线程安全。
  • 其它基础知识


今天开始准备系统的复习下Java集合体系啦,以敖 丙的复习脑图走啦~(ง •_•)ง

敖丙思维导图-集合_第1张图片

HashMap

数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry在Java8中叫Node
1.8插入数据时判断链表长度是否大于 8并且数组长度大于64, 大于的话链表转换为红黑树;当删除小于六时重新变为链表

根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。

初始化长度

默认初始化长度(1<<4就是16),因为位与运算比算数计算的效率高了很多。因为Length-1的值是所有二进制位全为1,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。2的幂实现均匀分布

红黑树

红黑树是一种自平衡的二叉查找树

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色的空节点(NIL节点)。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
    这些规则确保 从根到叶子节点的最长路径不超过最短路径的2倍。
    平衡时使用:变色 + 旋转

左旋转: 逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。

迭代器

HashMap 中的 Iterator 迭代器是 fail-fast

  • 快速失败(fail—fast)是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。在遍历过程中使用一个 modCount 变量,遍历下一个元素之前都会去监测这个值。
  • 安全失败(fail—safe)遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。(java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。)

扩容机制

数组容量是有限的,数据多次插入的,到达一定的数量就会进行扩容(resize)。

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。
  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
    (长度扩大以后,Hash的规则变化HashCode(Key) & (Length - 1)) HashCode(Key)就是:hashcode的高16位和低16位异或,用之前需要对数组的长度取模运算(因为数组的初始大小才16放不下哦),把散列值和数组长度-1做一个"与"操作)

扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8 不用重新hash就可以直接定位原节点在新数据的位置;(扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,重新hash数值比原来大16(旧数组的容量)

jdk1.7头插法(新元素总会被放在链表的头部位置)在resize时多线程会形成环形链表
1.8使用尾插法避免(使用头插会改变链表的上的顺序,但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。)但是它没有加同步锁,多线程情况最容易出现的就是:线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,A会把B的数据覆盖

为什么重写equals方法的时候需要重写hashCode方法呢?

在java中,所有的对象都是继承于Object类。在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样。

  • 对于值对象,==比较的是两个对象的值
  • 对于引用对象,比较的是两个对象的地址

对hashCode方法重写,以保证相同的对象返回相同的hash值。不然链表咋找对象??

线程安全的实现

Java中有HashTable、Collections.synchronizedMap、以及ConcurrentHashMap可以实现线程安全的Map。

  1. HashTable是直接在操作方法上加synchronized关键字,锁住整个数组,粒度比较大。

HahTable对象的key、value值均不可为null。Hashtable使用的是安全失败机制fail-safe),如果你使用null值,就会使得其无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。HashMap 的键值则都可以为 null

  1. Collections.synchronizedMap是使用Collections集合工具的内部类,通过传入Map封装出一个SynchronizedMap对象,内部定义了一个对象锁Map,方法内通过对象锁(Map)实现【还有排斥锁mutex,如果你传入了mutex参数,则将对象排斥锁赋值为传入的对象。】;
  2. ConcurrentHashMap使用CAS + synchronized,让并发度大大提高。

LinkedHashMap内部维护了一个单链表,有头尾节点,同时LinkedHashMap节点Entry内部除了继承HashMap的Node属性,还有before 和 after用于标识前置节点和后置节点。可以实现按插入的顺序或访问顺序排序。
TreeMap是按照Key的自然顺序或者Comprator的顺序进行排序,内部是通过红黑树来实现。所以要么key所属的类实现Comparable接口,或者自定义一个实现了Comparator接口的比较器,传给TreeMap用于key的比较。

ConcurrentHashMap

jdk1.7

jdk1.7时,由 Segment 数组( 继承于 ReentrantLock)-》里面存放数据使用的是
HashEntry(HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了数据Value还有下一个节点next) 组成。分段锁技术:当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。set需要获取Segment 锁;而 get 方法由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,因为不需要加锁。

jdk1.8

jdk1.8时,采用了 CAS + synchronized 来保证并发安全性。抛弃了原有的 Segment 分段锁,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰。采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized。

put操作

ConcurrentHashMap在进行put操作的还是比较复杂的,大致可以分为以下步骤:

  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
CAS 带来的ABA问题加个版本号、时间戳就能解决。

ArrayLists和LinkedList

ArrayList是实现了基于动态数组(jdk1.8后对arraylist进行优化,默认为0,初次添加元素时扩容为10,为的是避免无用内存占用。每次扩容1.5倍)的数据结构,随机访问占优,空间浪费主要体现在在list列表的结尾预留一定的容量空间;

  • 当使用ArrayList(int initialCapacity)的时候,不会初始化数组大小,只是指定了elementData缓冲区数组的大小, 跟数组的大小size并没有什么关系。(elementData用transient来修饰,因为elementData里面有一些元素是空的,这种是没有必要序列化的。)
    LinkedList基于链表的数据结构,新增和删除占优,空间花费则体现在它的每一个元素都需要消耗相当的空间。

ArrayList遍历性能更快

ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。

Arrays.asList()的坑
Arrays.asList()方法返回的是的Arrays内部的ArrayList,用的时候需要注意。它体现的是适配器模式,后台的数据仍然是数组,所以不能使用修改集合的方法(add/remove/clear)

使用Vertor、Collections.synchronizedList、CopyOnWriteArrayList 确保线程安全。

CopyOnWrite并发容器用于读多写少的并发场景( 只是在增删改上加锁,但是读不加锁)。

  • 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,
  • 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。

CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证同步,避免多线程写的时候会 copy 出多个副本。

虽然同步容器(例如Vertor)的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性。需要客户端通过主动加锁来保证。
由于同步容器存在的并发度低问题,从Java5开始,java.util.concurent包下,提供了大量支持高效并发的访问的集合类–并发容器。但是,作为代替Vector的CopyOnWriteArrayList并没有解决同步容器的复合操作的线程安全性问题。

ConcurrentHashMap中增加了对常用复合操作的支持,比如putIfAbsent()、replace()

其它基础知识

String,StringBuild,StringBuff的区别
执行效率: stringbuild>stringbuff>string
String类是不可变类,任何对String的改变都会引发新的String对象的生成;
StringBuffer是可变类,任何对它所指代的字符串的改变都不会产生新的对象,线程安全的。
StringBuilder是可变类,线性不安全的,不支持并发操作,不适合多线程中使用,但其在单线程中的性能比StringBuffer高。

你可能感兴趣的:(面试复习,java,数据结构)