集合(一)

2.1 Java中有哪些容器(集合类)?

java中的集合主要由Collection 和Map两个接口派生而出,Collection又派生出List,Set两个子接口。java中所有集合类都是List ,Set,Map的实现类

  • List集合 有序 可重复,有索引
  • Set集合 无序 不重复,无索引
  • Map集合 Key-Value类集合

 

 

 

 

 

 

2.2 Java中的容器,线程安全和线程不安全的分别有哪些?

  • java.util下的集合类大部分都是线程不安全的,如常见的ArrayList ,LinkedList等都是线程不安全的,但它们的性能通常比较高
  • java.util下也有线程安全的集合类,如Vector,HashTable等,都是比较古老的API,虽然线程安全但性能很低

JDK1.5后在java.util.concurrent包下提供了大量线程安全且性能较好的集合类

  • 以Concurrent开头的集合类:

    以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。

  • 以CopyOnWrite开头的集合类

          以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程            对此类集合执行写入操作时,集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线            程安全的。

2.3 Map接口有哪些实现类?

HaseMap LinkedHashMap ConcurrentHashMap

2.4 描述一下Map put的过程

HashMap举例:

  1. HashMap是懒惰创建数据,初次使用才创建数组
  2. 根据key计算索引(桶下标)
  3. 如果桶下标没有人占用,就会创建一个链表Node然后返回
  4. 如果桶下标被占用 分两种情况 ①如果是TreeNode就走红黑树的更新或添加逻辑 ②如果是链表Node就走链表的添加更新逻辑,如果链表长度超过树化阈值就走树化逻辑
  5. 返回前检查元素个数是否超过阈值,一旦超过就进行扩容

2.5 如何得到一个线程安全的Map?

  • 使用java.util.concurrent下的集合,如ConcurrentHashMap
  • 使用Collections工具类,将线程不安全的Map包装成线程安全的Map

2.6 HashMap有什么特点?

  • HashMap是线程不安全的
  • HashMap可以使用null作为key或value(如果key为null,会直接将索引设置为0,不为null就走hashcode())

2.7 JDK7和JDK8中的HashMap有什么区别?

  • 底层数据结构  1.7是数组+链表    1.8是数组+链表+红黑树
  • 链表的插入 1.7是头插法 1.8是尾插法
  • 扩容 1.7是元素个数大于等于阈值且没有空位时才扩容 1.8元素个数大于阈值就会扩容(扩容这方面个人感觉1.7好)

2.8 介绍一下HashMap底层的实现原理

它基于hash算法,通过put()和get()方法存储和获取对象

存储对象时,将Key-Value传给put方法,调用key的hashcode方法确定数组下标索引进一步存储,如果元素个数超过阈值会进行扩容,为原来的两倍。获取对象时,将key传给get,调用key的hashcode方法确定索引下标,进一步调用equals确定

如果发生hash冲突,hashmap会通过拉链法解决,当链表长度超过阈值8时,链表会转化为红黑树

2.9 介绍一下HashMap的扩容机制

  1. 数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。

  2. 数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。大于0.75的话空间节省了,但链表长度太长影响性能,小于这个值的话。链表长度变短了,但扩容会变得十分频繁

  3. 为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。

  4. 对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(64),如果没有到达这个容量,会放弃转换,先去扩充数组。

2.10 HashMap中的循环链表是如何产生的?

(1.7存在,由于头插法引起)

在多线程的情况下,当重新调整HashMap大小的时候,就会存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历。如果条件竞争发生了,那么就会产生死循环了。

2.11 HashMap为什么用红黑树而不用B树?

B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。

HashMap本来是数组+链表的形式,链表由于其查找慢的特点,所以需要被查找效率更高的树结构来替换。如果用B/B+树的话,在数据量不是很多的情况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。

2.12 HashMap为什么线程不安全?

HashMap在并发执行put操作时,可能会导致形成循环链表,从而引起死循环。

2.13 HashMap如何实现线程安全?

  1. 直接使用Hashtable类;

  2. 直接使用ConcurrentHashMap;

  3. 使用Collections将HashMap包装成线程安全的Map。

2.14 HashMap是如何解决哈希冲突的?

为了解决碰撞,数组中的元素是单向链表类型。当链表长度超过阈值,且数组容量>=64会将链表转换成红黑树提高性能(否则扩容数组)。而当数组扩容拆分链表时,链表长度<=6时,又会将红黑树转换回单向链表提高性能。

2.15 说一说HashMap和HashTable的区别

  • hashtable时线程安全类集合,但性能没有hashmap优秀
  • hashtable不允许key或value为null值,因为它直接使用hashcode方法计算索引,如果为null会引发空指针异常
  • hashmap运行key或value为null,计算索引时如果key为null会直接将索引设为0,不为null就调用hashcode,key为null只能存在一个,value为null可以存在多个

2.16 HashMap与ConcurrentHashMap有什么区别?

参考答案

HashMap是非线程安全的,这意味着不应该在多线程中对这些Map进行修改操作,否则会产生数据不一致的问题,甚至还会因为并发插入元素而导致链表成环,这样在查找时就会发生死循环,影响到整个应用程序。

Collections工具类可以将一个Map转换成线程安全的实现,其实也就是通过一个包装类,然后把所有功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。

ConcurrentHashMap的实现细节远没有这么简单,因此性能也要高上许多。它没有使用一个全局锁来锁住自己,而是采用了减少锁粒度的方法,尽量减少因为竞争锁而导致的阻塞与冲突,而且ConcurrentHashMap的检索操作是不需要锁的。

2.17 介绍一下ConcurrentHashMap是怎么实现的?

jdk1.7:

  • 数据结构:segment 大数组 + HsahEntry 小数组 +链表 每个segment对应一把锁,如果多个线程访问不同的segment不会冲突
  • 并发度:segment数组大小即并发度,决定了同一时刻能多少线程并发访问,segment数组不会扩容,创建时决定并发度
  • 扩容:每个小数组的扩容相对独立,小数组在超过扩容因子会触发扩容,每次扩容翻倍
  • segment[0] 首次创建其他小数组时,会以此原型为依据

 

 

jdk1.8:

  • 数据结构:Node数组 + 链表 +红黑树, 数组每个头节点作为锁,如果多个线程访问头节点不同则不会冲突。首次生成头节点时如果发生竞争,利用cas
  • 并发度:Node数组有多大并发度就多大,可以扩容
  • 扩容条件 Node数组满3/4就扩容
  • 扩容单位:以链表为单位从后向前迁移链表,迁移完成的将旧数组头节点替换为 ForwardingNode

 

 

2.18 ConcurrentHashMap是怎么分段分组的?

get操作:

Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。

put操作:

当执行put操作时,会经历两个步骤:

  1. 判断是否需要扩容;

  2. 定位到添加元素的位置,将其放入 HashEntry 数组中。

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

2.19 说一说你对LinkedHashMap的理解

  • LinkedHashMap使用双向链表来维护key-value对的顺序(其实只需要考虑key的顺序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致。
  • LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
  • LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

2.20 请介绍LinkedHashMap的底层原理

  • LinkedHashMap继承于HashMap,它在HashMap的基础上,通过维护一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,LinkedHashMap很多方法直接继承自HashMap,仅为维护双向链表重写了部分方法。
posted @ 2022-04-13 18:14  yxhhhhhh  阅读(46)  评论(0)    收藏  举报