对线面试官 - Java 基础面试题【一】
面试官:能简单说说 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,备注- 技术交流 。或关注微信公众号【码上遇见你】。
版权声明: 本文为 InfoQ 作者【派大星】的原创文章。
原文链接:【http://xie.infoq.cn/article/5cbf6ab5b6192f7624dbdd19c】。
本文遵守【CC BY-NC-ND】协议,转载请保留原文出处及本版权声明。
评论