集合(一)
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举例:
- HashMap是懒惰创建数据,初次使用才创建数组
- 根据key计算索引(桶下标)
- 如果桶下标没有人占用,就会创建一个链表Node然后返回
- 如果桶下标被占用 分两种情况 ①如果是TreeNode就走红黑树的更新或添加逻辑 ②如果是链表Node就走链表的添加更新逻辑,如果链表长度超过树化阈值就走树化逻辑
- 返回前检查元素个数是否超过阈值,一旦超过就进行扩容
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的扩容机制
- 
数组的初始容量为16,而容量是以2的次方扩充的,一是为了提高性能使用足够大的数组,二是为了能使用位运算代替取模预算(据说提升了5~8倍)。 
- 
数组是否需要扩充是通过负载因子判断的,如果当前元素个数为数组容量的0.75时,就会扩充数组。大于0.75的话空间节省了,但链表长度太长影响性能,小于这个值的话。链表长度变短了,但扩容会变得十分频繁 
- 
为了解决碰撞,数组中的元素是单向链表类型。当链表长度到达一个阈值时(7或8),会将链表转换成红黑树提高性能。而当链表长度缩小到另一个阈值时(6),又会将红黑树转换回单向链表提高性能。 
- 
对于第三点补充说明,检查链表长度转换成红黑树之前,还会先检测当前数组数组是否到达一个阈值(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如何实现线程安全?
- 
直接使用Hashtable类; 
- 
直接使用ConcurrentHashMap; 
- 
使用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就扩容
- 扩容单位:以链表为单位从后向前迁移链表,
2.18 ConcurrentHashMap是怎么分段分组的?
get操作:
Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。
put操作:
当执行put操作时,会经历两个步骤:
- 
判断是否需要扩容; 
- 
定位到添加元素的位置,将其放入 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,仅为维护双向链表重写了部分方法。
 
                    
                     
                    
                 
                    
                

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号