面试官:能简单说说String、StringBuffer、StringBuilder的区别吗?
派大星:可以,
- 首先
String是不可变的,如果尝试修改会新生成一个字符串对象,StringBuffer和StringBuilder是可变的。 StringBuffer是线程安全的,StringBuilder是线程不安全的。所以在单线程环境下StringBuilder效率会更高一些。
面试官:不错,那ArrayList和LinkedList有哪些区别知道吗?
派大星:
- 首先从数据结构方面讲的话,两者是不同的,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
- 其次从应用场景来讲的话,两者也是不同的,ArrayList更适合随机查找,LinkedList更适合删除和添加。查询、添加、删除的时间复杂度也是不同的。
- 从接口角度来说,两者都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做队列来使用
面试官:可以,但是上述List并非是线程安全的。如果要保证线程安全应该怎么办?
派大星:可以尝试使用CopyOnWriteArrayList,它可以保证线程安全。
面试官:那你能简单讲一讲它的底层实现原理吗?
派大星:可以,
- 首先CopyOnWriteArrayList内部也是通过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作再原数组上进行
- 并且写操作的时候会加锁,防止出现并发写入丢失数据的问题
- 写操作完成之后会把原数组指向新数组
- CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占用内存,同时可能会产生读到的数据不是最新的。所以不适合实时性要求很强的场景。
面试官:不错,了解HashMap吗?知道它的扩容原理吗?
派大星:了解一些:但是在不同JDK版本上有所不同。 JDK1.7版本:
- 会先生成新数组,
- 然后遍历老数组中的每个位置上的链表上的每个元素
- 接着取每个元素的key,并基于新数组长度就,计算每个元素在新数组中的下标
- 再然后会将元素添加到新数组中去。
- 最后当所有元素都转移完了之后,将新数组赋值个HashMap对象的table属性即可
JDK1.8版本:
- 会先生成新数组
- 接着会遍历老数组中每个位置上的链表或红黑树
- 然后会进行判断如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
- 如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
- 统计每个下标位置的元素个数
- 如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点添加到新数组对应的位置
- 如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置上
- 最后当所有元素转移完了之后,会将新数组赋值给HashMap对象的table属性
面试官:不错,HashMap是线程安全的吗?如果不安全体现在哪里?在实际应用中如果要保证线程安全又几种解决方案呢?
派大星:HashMap不是线程安全的。如果在实际使用过程中想要保证其线程安全,主要表现在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。在jdk1.8中,在多线程环境下,会发生数据覆盖的情况
可以采用HashTable或者是ConcurrentHashMap。但是HashTable效率过于低下,多数情况下可以采用ConcurrentHashMap即可。
面试官:那你简单说说ConcurrentHashMap是如何保证线程安全的呢?
派大星:在JDK1.7它的ConcurrentHashMap的解决思想是将散列表分为多个段,进而使用分段锁来降低多的粒度,因为锁的粒度越小事务的并行度越高。 概括来讲:
- 在JDK1.7中ConcurrentHashMap中使用了分段锁,Segment继承与ReentranLock,并将每个Segment对线作为锁,每个Segment对象中有一个HashEntry数组。
- Segment数组经初始化后便不再扩容,HashEntry数组可以扩容
- 使用预创建的思想,当线程想要进行put操作而获取锁时发现锁被占用,会先进行对节点的创建,以避免线程处于空闲状态
- 扩容是在HashEntry中的put方法进行的,而当前HashEntry已经使用了Segment对象作为锁来保证线程安全,进而保证了扩容的线程安全
JDK1.8中:
- 引入了红黑树的数据结构,且不在使用分段锁,改用Node数组
- 直接在散列表的每个头节点上
使用CAS进行创建头节点或者使用Synchronized关键字加锁
面试官:不错,那你可以简单聊聊ConcurrentHashMap的扩容过程吗?
派大星: JDK1.7:
- 1.7版本中的ConcurrentHashMap是基于Segment分段实现的,每个Segment相当于一个小型的HashMap
- 每个Segment内部会进行扩容(注意这里是Segment内部不是Segment本身),和HashMap的扩容逻辑类似,先生成新的数组,然后转移元素到新数组中
- 扩中的判断也是每个Segment内单独判断的。判断是否超过阈值
JDK1.8:
- 1.8版本的ConcurrentHashMap不再基于Segment实现,
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
- 如果当某个线程put时,发现没有正在扩容,则将key-value天假到ConcurrentHashMap中,然后判断是否超过了阈值,超过了则进行扩容
- ConcurrentHashMap是支持多个线程同时扩容,扩容之前也先生成一个新的数组
- 在转移数组时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作。
如有问题,欢迎加微信交流:w714771310,备注- 技术交流 。或关注微信公众号【码上遇见你】。