1、Java中有哪些集合类
主要由Collection和Map两个几口派生而出,其中Collection
接口又派生出3个子接口:List、Set和Queue。所有Java的集合类都是List、Stack、Queue和Map这四个接口的实现类。
1.Set:无序,元素不可重复
2.List:有序可重复
3.Queue:先进先出(FIFO)
4.Map:映射(key-value)
这些接口的常见实现类:HashSet、TreeSet、ArrayList、LinkList、ArrayDeque、HashMap、TreeMap等。
Collection体系的继承树:
Map体系的继承树:
注:紫色框体代表接口,其中加粗的是代表四类集合的接口。蓝色框体代表实现类,其中有阴影的是常用实现类。
2、Java容器中线程安全和不安全的有:
java.util包下的集合类大多数时线程不安全的,例如常见的HashSet、TreeSet、ArrayList、LinkedList、ArrayDeque、HashMap、TreeMap。这些集合类虽然线程不安全,但是优点是性能好。同时可以使用Collection工具类提供的synchronizedXxx()方法将非线程安全的类包装成线程安全的集合类。此外,java.util包下也有线程安全的集合类,例如Vector、HashTable。这些集合类线程安全,但是Api古老性能差。
java5开始,java.util.concurrent包下提供了大量即线程安全又具有良好访问性能的集合类。可以分为2类:
1.以Concurrent开头的集合类:
该类集合是支持并发访问的集合,可以多个线程并发写入访问,这些读写操作是线程安全的,但读取操作不必锁定。该类集合类采用更复杂的算法保证永远不会锁住整个集合,因此在并发写入时有更好的性能
2.以CopyOnWrite开头的集合类:
该类集合类采用复制底层数组的方法来实现写操作。当线程对此类集合进行读取时,直接读取集合本身,无需加塞和阻塞;当执行写入操作时,集合会在底层复制一份数组,对数组进行写入,由于是对副本执行操作,因此线程安全。
java.util.concurrent包下线程安全的集合类的体系结构:
3、Map接口有哪些实现类
常见的有:HashMap、TreeMap、LinkedHashMap、ConcurrentHashMap
HashMap:无需排序优先考虑使用
ConcurrentHashMap:需要线程安全使用,性能优于HashTable
LinkHashMap:需要排序
TreeMap:需要将key自然排序或自定义排序
4、Map put 的过程
1.首次扩容:
先判断数组是否为空,若数组为空则进行第一次扩容(resize)
2.计算索引:
通过hash算法,计算当前键值在数组中的索引
3.插入数据:
1)若当前数组为空,直接插入数据
2)若当前数组非空且key不存在,则将数据链到链表末端
3)若当前数组非空且key存在,则覆盖其value
4)若链表长度达到8,则将链表转化成红黑树,并将数据加入树中
4.再次扩容
数组个数超过threshold
5、如何得到一个线程安全的Map
1.使用Collection工具类,将Map包装成线程安全的Map;
2.使用Concurrent下的Map,如ConcurrentHashMap
6、HashMAp有什么特点
1.线程不安全
2.可以使用null作为key和value
7、JDK7和JDK8中的HashMap有什么区别
JDK7中的HashMap是基于数组+链表实现的,底层维护一个Enty数组。该方案有一个缺陷,当Hash冲突严重时,在桶上形成的链表会越来越长,使得查询效率越来越低。
JDK8中的HashMap是基于数组+链表+红黑树实现的,底层维护一个Node数组。当链表存储的数据个数大于8个时,改用红黑树存储结构。查询效率提高为O(logN)
8、HashMap的底层实现原理
底层是基于hash算法的,通过get和put方法存储和获取对象
存储对象时,我们将K/V传给put方法,在调用K的hashCode计算hash从而得到buket位置,进一步存储对象时,hashmap会根据当前buket占用情况自动给调整容量。
获取对象时,将K传给get方法,在调用HashCode计算hash从而得到buket位置,并进一步equals方法确定键值对。
如果发送碰撞,HashMap通过链表将产生碰撞的元素组织起来。在Java 8中,如果一个buket中碰撞冲突的元素8,则使用红黑树替代链表
9、HashMap扩容机制
1. 数组的初始容量为16,而容量以2的次方扩容。一是为了提高性能使用足够大的数组,二是为了能够使用位运算代替取模运算
2. 数组是否需要扩容通过负载因子判断,如果当前数组元素个数达到数组容量的0.75时,就会进行扩容,这个0.75就是负载因子。负载因子可以通过构造器传入。
3. 为了解决碰撞,数组中的元素时单向链表类型。当链表长度达到一个阈值(7或8)时,会使用红黑树代替链表以提高性能,当链表长度缩小到另一个阈值(6)时,又会将红黑树转化成链表。
4. 在检查链表长度转化成红黑树之前,还会先检查当前数组的长度是否达到一个阈值(64),如果没有达到,会放弃转化,先扩容数组。
10、HashMap中的循环链表是如何产生的
在多线程的情况下,当需要重新调整HashMap的大小时,就会存在条件竞争,如果两个线程同时进行对HashMap大小调整,在调整过程中,存储在链表中的元素的次序会反过来,因为在移动到新的buket时,HashMap会把元素放在链表的头部而不是尾部,以避免尾部遍历。当条件竞争发生,就会出现循环链表
11、HashMap为什么线程不安全
在HashMap并发进行put时,可能会产生条件竞争,导致形成循环链表,从而导致死循环。
12、HashMap为什么使用红黑树不使用B树
HashMap本来是数组+链表的形式,为了解决链表查询效率低的缺点引入红黑树。如果使用B/B+树,在数据量不大的情况下,树会“挤在”一个节点中,从而让效率退化成链表级别。
13、Hashmap如何实现线程安全
1. 使用HashTable
2. 使用ConcurrentHashMap
3 使用Collections工具类将HashMap包装长线程安全的Map
14、HashMap如何解决Hash冲突
为了解决碰撞,当链表长度达到一个阈值时,会将链表转化成红黑树,当链表长度缩小到另一个阈值时,又会将红黑树转化成单向链表。
15、HashMap和HashTable的区别
1. HashMap线程不安全,性能优于HashTable;HashTable线程安全。
2.HashTable不能使用Null作为K/V的值,会引发空指针异常;HashMap可以
16、HashMap和ConcurrentHashMap的区别
HashMap是非线程安全的。可以使用Collections工具类将HashMap包装成线程安全的Map,底层是基于互斥锁的,性能与吞吐量比较低。
ConcurrentHashMap的实现细节要复杂得多,性能也要更好。
17、ConcurrentHashMap是怎么实现的
JDK 1.7中的实现:
在 jdk 1.7 中,ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成,采取分段锁来保证安全性。Segment 是 ReentrantLock 重入锁,在 ConcurrentHashMap 中扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 类似,是一个数组和链表结构。
JDK 1.8中的实现:
JDK1.8 的实现已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操作,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
18、ConcurrentHashMap是怎么实现分段分组的
get操作:
Segment的get操作实现非常简单和高效,先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment,再通过散列算法定位到元素。get操作的高效之处在于整个get过程都不需要加锁,除非读到空的值才会加锁重读。原因就是将使用的共享变量定义成 volatile 类型。
put操作:
当执行put操作时,会经历两个步骤:
- 判断是否需要扩容;
- 定位到添加元素的位置,将其放入 HashEntry 数组中。
插入过程会进行第一次 key 的 hash 来定位 Segment 的位置,如果该 Segment 还没有初始化,即通过 CAS 操作进行赋值,然后进行第二次 hash 操作,找到相应的 HashEntry 的位置,这里会利用继承过来的锁的特性,在将数据插入指定的 HashEntry 位置时(尾插法),会通过继承 ReentrantLock 的 tryLock() 方法尝试去获取锁,如果获取成功就直接插入相应的位置,如果已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用 tryLock() 方法去获取锁,超过指定次数就挂起,等待唤醒。
19、LinkedHashMap的理解
LinkedHashMap使用双向链表来维护K/V对的顺序,该链表负责维护Map的迭代顺序,迭代顺序与K/V对插入顺序一致。
LinkedHashMap可以避免对HashMap、HashTable里的K/V对进行排序,同时又可以避免使用TreeMap所增加的成本
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能。但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将会有较好的性能。
20、LinkedHashMap的底层原理
LinkedHashMap继承于HashMap,在HashMap的基础上维护了一条双向链表,解决了HashMap不能随时保持遍历顺序和插入顺序一致的问题。在实现上,很多方法直接继承HashMap,仅为维护双向链表重写了部分方法。
21、TreeMap的底层原理
TreeMap基于红黑树实现。映射根据其K的自然排序,或者根据创建时提供的Comparator进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey、put、get、remove方法,他的时间复杂度都是O(logN)
22、ArrayList和LinkedList的区别
1. ArrayList的实现基于数组,LinkedList实现基于双向链表
2. 对于随访问,ArrayList性能由于LinkedList
3. 对于插入删除操作,LinkedList性能优于arrayList
4. LinkedList更占内存
23、有哪些线程安全的List
1. Vector:比较古老的API,线程安全但是效率低
2. Collections.SynchronizedList:使用Collections工具类将线程不安全的List包装成线程安全的List,其所有方法都带有同步锁,亦不是性能最优的List
3. CopyOnWriteArrayList:在java.util.concurrent包下增加的类,它采用复制底层数组的方式来实现写操作。由于对集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。在所有线程安全的List中,它是性能最优的方案。
24、ArrayList的数据结构
ArrayList的底层原理是数组,默认初始数组大小为10,超出限制时会增加50%的容量,并以System.arrayCopy()方法复制数据到新的数组。
按照数组下标访问的性能很高,进行末端插入的性能也很高,但是进行下标插入,删除元素时,需要用System.arrayCopy()复制数组,性能较低。
25、CopyOnWriteArrayList的原理
CopyOnWriteArrayList是一个线程安全且读操作无锁的ArrayList。在写操作时,会复制一份新的ArrayList作为副本,在副本上进行写操作,再将原引用指向新的ArrayList,整个写操作是上锁的。CopyOnWriteArrayList允许并发进行读操作,读操作没有加锁限制。在写操作执行过程中,如果需要读操作,会作用在原ArrayList上。
优点:读操作性能很高,因为无需任何同步措施,比较适合读多写少的并发场景。
缺点:1. 内存占用问题,因为每次执行写操作都会复制一份ArrayList,但数据量大时,对内存压力较大。2. 实时性无法保证。CopyOnWriteArrayList读写分离的策略,使得在写操作执行时,不会阻塞但是读到的是老容器的数据。
26、TreeSet和HashSet的区别
TreeSet和HashSet都是元素不可重复,线程不安全的Set,不同之处有:
1. HashSet元素可以为null,TreeSet不可以。
2. HashSet元素是无序的,TreeSet支持自然排序和自定义排序
3. HashSet底层是HashMap,TreeSet底层是红黑树
27、HashSet的底层原理
HashSet是基于HashMap实现的,默认构造函数构建一个初始容量为16,负载因子为0.75的HashMap。所有放入HashSet的元素实际上由HashMap的Key来存储,而HashMap的Value存储了一个PRESENT(静态Object对象)。
28、 BlockingQueue中有哪些方法,为什么这样设计?