本文为 Java 面试题系列文章第四篇,之前文章可点击访问:
一、 「BAT常问」40道Java基础面试题及详细答案整理汇总「开篇」
二、 「BAT常问」40道Java基础面试题及详细答案整理汇总「1—8」
三、 「BAT常问」40道Java基础面试题及详细答案整理汇总「9—16」
17.ConcurrentHashMap可以代替 Hashtable 吗?
hashTable虽然性能上比不上ConcurrentHashMap,但并不能完全被取代,两者的 迭代器 的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。
ConcurrentHashMap的get,clear,iterator 都是弱一致性的。 Doug Lea(大神 Doug Lea是JCP 中的一员) 也将这个判断留给用户自己决定是否使用ConcurrentHashMap。
ConcurrentHashMap与HashTable都可以用于 多线程 的环境,但是当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。因为ConcurrentHashMap引入了分割( Segment ation),不论它变得多么大,仅仅需要锁定map的某个部分,而其它的线程不需要等到迭代完成才能访问map。简而言之,在迭代的过程中,ConcurrentHashMap仅仅锁定map的某个部分,而Hashtable则会锁定整个map。
那么既然ConcurrentHashMap那么优秀,为什么还要有Hashtable的存在呢?ConcurrentHashMap能完全替代HashTable吗?
HashTable虽然性能上不如ConcurrentHashMap,但并不能完全被取代, 两者的迭代器的一致性不同的,HashTable的迭代器是强一致性的,而ConcurrentHashMap是弱一致的 。
ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
那么什么是强一致性和弱一致性呢?
get方法是弱一致的,是什么含义?可能你期望往Concurrent HashMap 底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。
我们结合java内存模型相关内容来分析下put/get方法。put方法我们只需关注Segment#put,get方法只需关注Segment#get,在继续之前,先要说明一下Segment里有两个volatile变量:count和table;HashEntry里有一个volatile变量:value。
总结
ConcurrentHashMap的弱一致性主要是为了提升效率,是一致性与效率之间的一种权衡。要成为强一致性,就得到处使用锁,甚至是全局锁,这就与Hashtable和同步的HashMap一样了。
18.为什么说HashMap是线程不安全的呢?
HashMap 在并发执行 put 操作时会引起死循环,导致 CPU 利用率接近100%。因为多线程会导致 HashMap 的 Node 链表形成环形数据结构,一旦形成环形数据结构,Node 的 next 节点永远不为空,就会在获取 Node 时产生死循环。
19.在使用HashMap时,如何做到 线程安全 ?
了解了 HashMap 为什么线程不安全,那现在看看如何线程安全的使用 HashMap。
这个无非就是以下三种方式:
Hashtable ConcurrentHashMap Synchronized Map
Hashtable例子代码:
先稍微吐槽一下,为啥命名不是 HashTable 啊,看着好难受不管了就装作它叫HashTable 吧。因为已经不常用了,就简单说说吧。HashTable 源码中是使用synchronized来保证线程安全的,比如下面的 get 方法和 put 方法:
所以当一个线程访问 HashTable 的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。举个例子,当一个线程使用 put 方法时,另一个线程不但不可以使用 put 方法,连 get 方法都不可以,这样效率很低,现在基本不会使用它了。
ConcurrentHashMap
ConcurrentHashMap 于 Java 7 的,和8有区别,在8中 CHM 摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用 CAS 算法。
SynchronizedMap
synchronizedMap() 方法后会返回一个 SynchronizedMap 类的对象,而在 SynchronizedMap 类中使用了 synchronized 同步关键字来保证对 Map 的操作是线程安全的。
性能对比
这是要靠数据说话的时代,所以不能只靠嘴说 CHM 快,它就快了。写个测试用例,实际的比较一下这三种方式的效率(源码来源),下面的代码分别通过三种方式创建 Map 对象,使用 ExecutorService 来并发运行5个线程,每个线程添加/获取500K个元素。
ConcurrentHashMap 性能是明显优于 Hashtable 和 SynchronizedMap 的,CHM 花费的时间比前两个的一半还少。
20.多并发情况下HashMap是否还会产生死循环?
看下了ConcurrentHashMap的源码,ConcurrentHashMap是Java 5中支持高并发、高吞吐量的线程安全HashMap实现。
在看很多博客在介绍ConcurrentHashMap之前,都说HashMap适用于单线程访问,这是因为HashMap的所有方法都没有进行锁同步,因此是线程不安全的,不仅如此,当多线程访问的时候还容易产生死循环。
建议参考博客文章《Java困惑》:多并发情况下HashMap是否还会产生死循环。讲的很详细,百度很好找到。
既然会产生死循环,为什么并发情况下,还是用ConcurrentHashMap。 jdk 好像有,但是Jdk8 已经修复了这个问题。
21.讲一讲TreeMap、HashMap、LindedHashMap的区别
LinkedHashMap 可以保证HashMap集合有序,存入的顺序和取出的顺序一致。
TreeMap 实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
HashMap 不保证顺序,即为无序的,具有很快的访问速度。 HashMap 最多只允许一条记录的键为Null;允许多条记录的值为 Null。 HashMap 不支持线程的同步。
我们在开发的过程中 使用HashMap比较多,在Map中在Map 中插入、删除和定位元素,HashMap 是最好的选择 。
但如果您要 按自然顺序或自定义顺序遍历键 ,那么 TreeMap会更好 。
如果 需要输出的顺序和输入的相同 ,那么 用LinkedHashMap 可以实现,它还可以按读取顺序来排列 。
22.说说 Collection 包的结构,和Collections的区别在哪里?
Collection 是集合类的上级接口,子接口主要有Set、List 、Map。
Collecions 是针对集合类的一个帮助类, 提供了操作集合的工具方法,一系列静态方法实现对各种集合的搜索、排序线性、线程安全化等操作。
Collection
Collection 是单列集合
List
元素是有序的、可重复。
有序的 collection,可以对列表中每个元素的插入位置进行精确地控制。
可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。
可存放重复元素,元素存取是有序的。
List接口中常用类
Vector:线程安全,但速度慢,已被ArrayList替代。底层数据结构是数组结构。
ArrayList:线程不安全,查询速度快。底层数据结构是数组结构。
LinkedList:线程不安全。增删速度快。底层数据结构是列表结构。
Set
Set接口中常用的类
Set(集) 元素无序的、不可重复。
取出元素的方法只有迭代器。不可以存放重复元素,元素存取是无序的。
HashSet:线程不安全,存取速度快。它是如何保证元素唯一性的呢?依赖的是元素的hashCode方法和euqals方法。
TreeSet:线程不安全,可以对Set集合中的元素进行排序。它的排序是如何进行的呢?通过compareTo或者compare方法中的来保证元素的唯一性。元素是以二叉树的形式存放的。
Map
map是一个双列集合
Hashtable :线程安全,速度快。底层是 哈希表 数据结构。是同步的。不允许null作为键,null作为值。
Properties :用于配置文件的定义和操作,使用频率非常高,同时键和值都是字符串。是集合中可以和IO技术相结合的对象。
HashMap :线程不安全,速度慢。底层也是哈希表数据结构。是不同步的。允许null作为键,null作为值,替代了Hashtable。
LinkedHashMap : 可以保证HashMap集合有序。存入的顺序和取出的顺序一致。
TreeMap :可以用来对Map集合中的键进行排序
23.用到try catch finally时,如果try里有return,finally里边的代码还执行么?
肯定会执行。finally{}块的代码。
只有在try{}块中包含遇到 System.exit(0) 。
之类的导致Java虚拟机直接退出的语句才会不执行。
当程序执行try{}遇到return时,程序会先执行return语句,但并不会立即返回——也就是把return语句要做的一切事情都准备好,也就是在将要返回、但并未返回的时候,程序把执行流程转去执行finally块,当finally块执行完成后就直接返回刚才return语句已经准备好的结果。
24.说说Excption与Error包结构,OOM你遇到过哪些情况?SOF你遇到过哪些情况?
Throwable是 Java 语言中所有错误或异常的超类。
Throwable包含两个子类: Error 和 Exception 。它们通常用于指示发生了异常情况。
Throwable包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()等接口用于获取堆栈跟踪数据等信息。
Java将可抛出(Throwable)的结构分为三种类型:
被检查的异常(Checked Exception)。
运行时异常(RuntimeException)。
错误(Error)。
运行时异常RuntimeException
定义 : RuntimeException及其子类都被称为运行时异常。
特点 : Java编译器不会检查它 也就是说,当程序中可能出现这类异常时,倘若既”没有通过throws声明抛出它”,也”没有用try-catch语句捕获它”,还是会编译通过。
例如,除数为零时产生的ArithmeticException异常,数组越界时产生的IndexOutOfBoundsException异常,fail-fail机制产生的ConcurrentModificationException异常等,都属于运行时异常。
堆内存溢出 OutOfMemoryError(OOM)
除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError(OOM)异常的可能。
Java Heap 溢出。
一般的异常信息:java.lang.OutOfMemoryError:Java heap spacess。
java堆用于存储对象实例,我们只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,就会在对象数量达到最大堆容量限制后产生内存溢出异常。
堆栈溢出 StackOverflow (SOF)
StackOverflowError 的定义:
当应用程序递归太深而发生堆栈溢出时,抛出该错误。
因为栈一般默认为1-2m,一旦出现死循环或者是大量的递归调用,在不断的压栈过程中,造成栈容量超过1m而导致溢出。
栈溢出的原因 :
1、递归调用
2、大量循环或死循环
3、全局变量是否过多
4、数组、List、map数据过大