1. Java的集合有哪些?
- 主要有两大类集合都继承自Iterable接口,分别是Collection与Map。
- 其中Collection下又分为List和Set两个接口,List有序可以重复,Set无序不可重复;
- List的具体实现类有:ArrayList,LinkedList,Vector;Set的具体实现类有:HashSet,TreeSet;
- Map接口主要有三个实现类:HashMap,HashTable(线程安全),TreeMap
使不安全的集合线程安全:Collections.synchronizedList();
2. ArrayList是怎么扩容的?
如果不指定ArrayList的容量,默认初始化一个容量为10的数组,当插入第11个元素时就会触发扩容,创建一个长度为原来1.5倍的数组,并将原来的数据拷贝到新数组中,这就是扩容机制。
3. 说一下LinkedList和ArrayList
-
线程安全:都是线程不安全;
-
底层数据结构:ArrayList底层采用的是Object数组,LinkedList底层是双向链表;
-
是否支持随机访问:ArrayList支持,LinkedList不支持。
-
插入删除元素时:ArrayList采用数组存储,插入和删除元素的时间复杂度受位置影响,时间复杂度最坏为O(n),LinkedList插入或删除的时间复杂度仅为O(1)。
-
当ArrayList存储的数据过多时,会进行扩容,而LinkeList不需要扩容。
4. 链表和哈希表的区别?
-
链表是一种物理存储单元上非连续、非顺序的存储结构,链表由一个个节点组成,单向链表中每个节点存放值和下一个节点的地址;
-
哈希表是根据key值直接访问的数据结构,通过关键码值映射到表中的一个位置来访问记录,以加快查找速度。
5. 说说HashMap的底层
-
JDK1.7之前HashMap底层数据结构是数组+链表,首先通过计算key值的hash值,然后对数组长度取模,判断当前元素应该存放的位置,如果该位置没有元素就直接存入,如果有元素,判断存入元素的hash值是否与该元素相等,相等就覆盖,不相等就采用拉链法解决冲突;
-
JDK1.8后,底层是数组+红黑树, 当拉链的链表长度大于8时,先判断数组长度,如果长度小于64,对数组扩容,如果大于64,就将链表转化为红黑树。
6. hashmap是否为线程安全的,为什么不安全?
线程不安全,在JDK1.7的时候会出现死循环、数据丢失的情况,JDK1.8会出现数据覆盖的情况。
(这几个问题出现的具体原因请参考别的博客,结合源码看才能记得清楚,下面只是我根据源码的简单总结)
-
在1.7中,插入元素采用的是头插法,HashMap的线程不安全主要发生在扩容时,假设两个线程同时执行扩容,一个进入阻塞状态,另一个线程完成了扩容后,阻塞线程被唤醒,该线程还会继续执行扩容,此时扩容的过程中有可能出现一种情况,比如我们扩容后要插入的元素是A和B,他们原来的hash值就相等,且A指向B,迁移到新散列表的时候A先插入,根据头插法原理B插入后B会指向A,这样就出现死循环;
-
1.8后摒弃了transfer函数,直接在resize函数中完成数据的迁移,使用的是尾插法,解决了死循环的问题,但是会出现数据覆盖问题,在put操作中有段代码是检查哈希碰撞,如果没有哈希碰撞就直接插入元素,假设此时有两个线程A和B都在执行put操作,并且通过hash函数计算的结果是相同的,线程A执行完检查哈希碰撞操作后因时间片耗尽被挂起,线程B得到时间片后在该处下标进行插入元素,然后线程A获得了时间片,他会直接进行插入操作,这样就把线程B刚插入的数据覆盖了。
7. HashMap扩容
JDK1.8后,底层是数组+红黑树, HashMap 默认的初始化大小为 16,之后每次扩充,容量变为原来的 2 倍。如果初始化时指定容量,HashMap 会将其扩充为 2 的幂次方大小。插入元素时,首先计算hash值……
4.8 为什么每次扩容是2的n次幂
因为为了让HashMap高效,必须减少碰撞,所以必须把数据均匀分布,Java中的hash值范围非常大,远远超过内存,虽然保证了难以出现碰撞,但是内存放不下。所以需要对这个散列值关于数组长度做取模运算,即hash%n,如果n为2的幂次方,那么hash%n=hash&(n-1),与操作的运算效率高于取模运算,所以每次扩容都是2的幂次方。
8. 为什么HashMap的加载因子是0.75?
HashMap的初始容量是16,所以当HashMap的数组长度达到一个临界值的时候才会触发扩容,这个临界值是16*0.75=12;至于0.75这个具体值的由来,这是从统计学中的泊松分布推出来的,取值0.75既可以是为了提高利用率和又可以减少查询成本。
9. HashMap的rehash过程
rehash发生在扩容时,将数组扩容后,对于原来数组中的每个元素,重新计算key的哈希值,然后通过(n-1)&hash判断当前元素存放的位置,如果没有元素就直接插入,如果存在元素用拉链法存入,当链表长度超过8且数组长度超过64时,链表转换成红黑树。
10. Hashmap 发生hash冲突怎么办?
遇到哈希冲突将冲突值首先判断对应的value是否相同,相同就覆盖,不同就加到对应的链表或者红黑树中。
11. 为什么用红黑树
红黑树是一种近似平衡树,虽然查找效率略低于平衡树,但其插入、删除、查找各种操作性能比较稳定。
12. 讲讲HashMap的get操作?
首先根据对象的Hash值进行数组方面的寻找,然后找到这个数组之后,判断key是不是唯一,如果key唯一,则直接返回,如果不唯一,则使用equals进行值的判断,最后返回数据。
13. 说一下hashmap和hashtable
-
线程安全上:hashmap是线程不安全的,hashtable线程安全
-
执行效率上:hashmap效率高于hashtable,现在hashtable已经被ConcurrentHashMap取代
-
扩容机制上:如果创建时不指定容量,hashmap初始容量为16,每次扩容容量变为原来的2倍,hashtable初始容量为11,每次扩容为原来的2倍加1;如果创建时指定了容量初始值,HashMap会将宿主扩充为2的n次幂,而HashTable会使用指定大小。
-
底层数据结构:JDK1.8后,HashMap底层机构是数组+红黑树,HashTable底层是数组+链表
14. 自定义一个类,需要存放在HashSet/HashMap中,需要注意什么?
需要重写equals方法,也要重写hashCode方法
15. ConcurrentHashMap和Hashtable区别
底层数据结构不同,实现线程安全的方式上不同。
底层:1.7时ConcurrentHashMap
是分段数据+链表,1.8时是数据+链表(红黑树),HashTable
在1.8之前是数组+链表的形式;
实现线程安全:对于ConcurrentHashMap
:1.7的时候对整个桶数组进行分割分段,每把锁只锁住其中一段数据,多线程访问不同数据段的数据,这就不会存在锁竞争,保证了线程安全;在1.8的时候,抛弃了分段的概念,底层也发生了改变,并发控制采用synchronized
和CAS操作,例如一个在put的过程中,计算到插入的位置然后只将该位置锁住,但是其他线程能在别的位置插入元素。
对于Hashtable
:也使用synchronized
来保证线程安全,但效率非常低,因为多个线程访问同步方法时可能会进入阻塞或者轮询状态,例如put元素时,另一个线程不能put也不能get。
16. ConcurrentHashMap的put过程
- 1.7的时候
ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成,其实就是将HashMap分为多个小HashMap,每个Segment元素维护一个小HashMap,目的是锁分离,给每个Segment进行加锁。当⼀个线程占⽤锁访问其中⼀个段数据时,其他段的数据也能被其他线程访问 。
对于插入操作,需要两次Hash映射去定位数据存储位置。首先通过第一次hash过程,定位Segment位置,然后通过第二次hash过程定位HashEntry位置。
因为Segment继承ReentrantLock,所以在数据插入指定HashEntry过程的时候,会尝试调用ReentrantLock的tryLock方法获取锁,如果获取成功就直接插入相应位置,无法获取,当前线程就会以自旋方式去继续调用tryLock方法去获取锁,超过指定次数就挂起,等待唤醒。
- 1.8的时候
底层是数组加红黑树,最一开始得检查key或者value是否有空值,如果有就抛出异常,然后根据key计算hash值,初始化数组,计算插入的数组中的位置。
判断该位置有没有元素,没有元素的话,那么就用CAS的方式添加元素,当然有可能失败,有可能其他线程已经抢占先添加,这个过程在一个死循环里,失败了再从头来一次,插入成功后返回;
如果此时对应位置有元素了,那么判断插入位置的第一元素的hash值是否为MOVED,如果是说明正在扩容,那么此线程就会帮助扩容。如果没有在扩容,那么就对第一个元素synchronized上锁,上锁之后还要对桶内第一个元素判断是否发生了改变,发生变化了就再从头再来,如果插入位置的链表长度超过阈值就转换成红黑树。
最后还要判断是否需要进行扩容。
我对再次判断桶内第一个元素的理解:如果在加锁的时候,此时有可能其他线程删除了第一个元素,那么就产生了错误。经历了重重困难终于能插入数据了,此时就和HashMap有点像了,如果第一元素是链表节点,那么遍历链表查询是否key相等,若查到把value更改为我们要put的value值,没找到的话新建节点插入到链表尾部。若第一个节点是树节点,那么就以树的方式插入。
17. 如何计算ConcurrentHashMap的size?
size是调用sumCount函数获取的元素数量,元素数量 = baseCount+ counterCells各个元素值
baseCount是一个volatile类型的变量,用来记录元素的个数,当插入新数据put()或则删除数据remove()时,会通过addCount()方法更新baseCount。
counterCells是个全局的变量,表示的是CounterCell类数组。CounterCell是ConcurrentHashmap的内部类,它就是存储一个值。初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会使用 counterCells记录元素个数的变化。
所以需要baseCount加上counterCells中所有元素的值,就得到了size。
(继续执行方法体中的逻辑,执行fullAddCount(x, uncontended)方法,这个方法其实就是初始化counterCells,并将x的值插入到counterCell类中,而x值一般也就是1或-1,这可以从put()方法中得知。)
注意:
ConcurrentHashMap如果设置了初始容量
,那就是大于该容量的2的最小次幂,比如new ConcurrentHashMap(32)
,默认容量是64