20道Java高频面试题-题解
作者丨安琪拉的博客
来源丨安琪拉的博客
这里是更新的20道题题解:
自我介绍一下
重点介绍自己最满意的技术,言简意赅的说最近一段工作经历和项目,突出项目的贡献。
2. 项目中用到的技术栈介绍一下
3. 项目中做的认为比较满意的部分讲一讲?
提前梳理好,重点介绍自己熟悉和深入研究过的,特别注意,选择介绍在项目中有真实应用场景的技术,有实际落地的案例是加分项。有的人可能学了很多东西,但是具体一问都没动手实践过。
4. 如果这个技术方案让你重新设计,你怎么做?
这个需要在平时工作中就想好,不浮在表面,思考要有深度,你是否对自己负责的系统做过完整的梳理,把整个链路、模块之间的关系都摸清楚了。例如面试官可能会问你,你的系统能承受多大的QPS、一次业务请求在系统中的完整链路,某个模块的实现逻辑,让你重新设计有没有可能做到更好。
5. Java集合类都有哪些?平常用到哪些?
安琪拉画了如下这张图,我建议大家有时间看看 java.util
的源码,可以自己动手画一画,印象会更深刻。
上面这个图梳理一下,为了方便对照上图看,图都放在最后
Java中的集合类可以分为两大类:一类是实现[Map接口];另一类是实现[Collection接口]。
实现Collection 接口的分为List 和Set,所以Java 集合类是由Map、List、Set构成。
Map 提供了键(key)值(value)对,定义了映射关系,如 图5.1所示,这个接口定义了Map需要的一些基本的方法,例如:size()、get()、put()。
AbstractMap 是抽象类(接口和抽象类统一用虚线框),实现了一些基础的操作。类定义如下:
public abstract class AbstractMap<K,V> implements Map<K,V>
K是key的泛型,V是value的泛型知识点: 经常面试被拿来比较的是HashMap 和HashTable。区别是HashTable 是线程安全的,实现方式是HashTable 会在一些需要操作hash表的地方加锁,具体方法就是在函数方法声明上加了’synchronized‘ 关键字,例如:put 和 get方法。
知识点: WeakHashMap 基于WeakReference,
强引用,例如 Object angela = new Object(), angela 就是强引用,对象只要还被引用,直到内存不足也不会被回收。
WeakReference,当一个对象被WeakReference引用, 而没有任何其他strong reference指向的时候, 如果GC运行, 那么这个对象就会被回收;
SoftReference和WeakReference一样, 但被GC回收的时候需要多一个条件: 当系统内存不足时才会回收SoftReference 引用的对象。
TreeMap 实现了SortedMap(实际上实现的是NavigableMap,NavigableMap继承了SortedMap),底层数据结构是红黑树,存放的元素都是按照key 来排序的,排序规则可以传入
Comparator
来定义。WeakHashMap 有个特点,它里面的元素随时可能会变成null,它是基于WeakReference实现,举个例子:
WeakHashMap hashMap = new WeakHashMap();
for (int i = 0; ; i++) {
//一直往里面加数据, 内存会爆炸
hashMap.put(i, new String("angela"));
//每隔一千次判断一下有没有对象被回收
if (i % 10000 == 0) {
//遍历一遍
for (int j = 0; j < i; j++) {
//
if (hashMap.get(j) == null) {
System.out.println("key为" + j + "的对象已经为null, GC会回收");
return;
}
}
}打印:
key为54808的对象已经为null, GC会回收
HashMap 想必大家都知道是面试的重中之重,基本常规面试很容易被问
List 下有ArrayList 和LinkedList,Vector,Stack。
知识点: ArrayList 和 LinkedList 区别,
最大的区别: ArrayList 底层是基于数组的数据结构,LinkedList基于链表的数据结构;
Vector 和 ArrayList 区别是Vector 是线程安全的。
Stack 是栈,继承自 Vector,和Vector一样,基础数据结构都是数组,Stack使用数组实现先进后出,基本原理是push: 向数组尾部追加元素,pop是让数组尾部置空,设置元素为null。
想详细了解Stack,可以参考我写的这篇: Java知识体系-Java集合栈
数组和链表区别很明显,数组随机访问速度快,链表查找需要从表头开始找。理解什么是随机访问,就是给定一个随机的index(下标),希望找到元素,数组直接array[index] 访问,时间复杂度0(1), 链表是O(n),因为链表需要从头开始找。
新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
Set 下有HashSet、TreeSet。Set最大的特点是key是唯一的,不允许重复。
HashSet 也是基于HashMap实现的,只不过value 是固定值。
TreeSet 是基于TreeMap实现,当然初始化的时候也可以指定基于自定义的NavigableMap实现。存放的元素是基于key排序好的。
所以实际上搞懂HashMap 和 TreeMap,这二个Set 基本也清楚了。
6. ArrayList 和 LinkedList 区别?
最大的区别: ArrayList 底层是基于动态数组的数据结构,LinkedList基于链表的数据结构;
数组和链表区别很明显,数组随机访问速度快,链表查找需要从表头开始找。理解什么是随机访问,就是给定一个随机的index(下标),希望找到元素,数组直接array[index] 访问,时间复杂度0(1), 链表是O(n)。
新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
7. HashMap 实现的数据结构和扩容过程?
数据结构是数据+链表
8. ArrayList 和 LinkedList 区别?你平常怎么选择?
比较多的随机访问,比如用下标index 访问集合,用ArrayList;
如果有比较多的在集合中间插入和删除,用LinkedList,因为ArrayList中间每次插入、删除需要移动数组元素。
9. 异常类都有哪些?Exception 和 Error什么区别?
Error是错误,对于所有的编译时期的错误以及系统错误都是通过Error抛出的。这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如Java虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java中,错误通过Error的子类描述。
Exception 规定的异常是程序本身可以处理的异常。异常和错误的区别是,异常是可以被处理的,而错误是没法处理的。其中Exception 又有二类:
Checked Exception
可检查的异常,这是编码时非常常用的,所有checked exception都是需要在代码中处理的。它们的发生是可以预测的,正常的一种情况,可以合理的处理。比如IOException,或者一些自定义的异常。除了RuntimeException及其子类以外,都是checked exception
Unchecked Exception
RuntimeException及其子类都是unchecked exception。比如NPE空指针异常,除数为0的算数异常ArithmeticException等等,这种异常是运行时发生,无法预先捕捉处理的。Error也是unchecked exception,也是无法预先处理的。
10. Synchronized 原理,锁膨胀过程 ?
11. synchronized 和 Reentrantlock 区别?
大家看下面这段,重在理解
两者的共同点:
1. ReentrantLock 显示的获得、释放锁,synchronized 隐式获得释放锁
2. ReentrantLock 可响应中断、可轮回,synchronized 是不可以响应中断的,为处理锁的
不可用性提供了更高的灵活性
3. ReentrantLock 是 API 级别的,synchronized 是 JVM 级别的
二者的不同点:
1. ReentrantLock 可以实现公平锁
2. ReentrantLock 通过 Condition 可以绑定多个条件
3. 底层实现不一样, synchronized 是同步阻塞,使用的是悲观并发策略,lock 是同步非阻塞,采用的是乐观并发策略
4. Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现。
5. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。
6. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
7. Lock 可以提高多个线程进行读操作的效率,既就是实现读写锁等
作用:都用来协调多线程对共享资源的访问
可重入性质:都是可重入锁,同一线程可以多次获得同一个锁
锁性质:都保证了可见性和互斥性
这道题总结解答出处:synchronized 和 ReentrantLock 区别是什么?
线程池原理是怎样的?
分布式事务一致性怎么实现?
这个大家可以优先看自己系统的实现。
我先来介绍一下分布式事务一致性的需求背景,我们经常使用支付工具转账,比如以经典的小明给小红转账100元为例,如果是单机系统,我们可以用本机事务轻松解决,但是分布式系统,转账可能是跨系统的调用,我们要保证数据的一致性就有些额外工作量了。
现在普遍互联网分布式系统都不会维护实习强一致性,而是做最终一致性,允许有短暂的不一致。
我这里只介绍用的很普遍的事务消息,其他的比如二阶段提交、本地消息表做数据一致性比对&补偿都是实现一致性的方式。
事务消息发送步骤如下:
事务消息回查步骤如下:
以上内容参考: RocketMQ事务消息
在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。
发送方将半事务消息(类似prepare消息),消息内容是将小红账号加100元,发送至消息队列RocketMQ版服务端。
消息队列RocketMQ版服务端将消息持久化成功之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事务消息。
发送方开始执行本地事务逻辑,讲小明账号扣减100块。
发送方根据本地事务执行结果向服务端提交二次确认(100块扣成功则Commit,100块扣失败则发Rollback),服务端收到Commit状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到Rollback状态则删除半事务消息,订阅方将不会接受该消息。
消息乱序遇到过吗?怎么解决的?
消息重复业务系统幂等解决,消息乱序,如果是业务系统,一般是MQ尽量保障同一用户路由到同一消息分区,但这个只是尽量,一般消息乱序都是由下游消费方来处理,处理方法是消息中增加版本号、occurTime(业务时间发生时间)来判断消息的先后顺序,然后做对应的业务逻辑,例如,同一业务流水号,从库里面的数据的版本号或occurTime和新消息的版本号和occurTime 比较,版本号更大,时间更靠后的为最新消息,可以做更新操作。
一般消息中间件都会遇到以下几个问题:
消息重复
消息并发
消息乱序
消息延迟
消息积压
ThreadLocal 用过吗?实现机制?
发现很多博客关于ThreadLocal的说明写错了,ThreadLocal不是维护了key为Thread对象的Map,而是Thread对象维护了一个key为ThreadLocal 对象的Map。
参考之前写的:一文了解ThreadLocal用法
wait、sleep区别?
1、sleep是线程中的方法,但是wait是Object中的方法。
2、sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。
3、sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。
4、sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。
反射用过吗?什么原理?
动态代理了解吗?
单例模式了解吗?实现一个线程安全的单例模式?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
实现代码(只列举双重检查锁的写法,其他写法参考):
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
无界队列和有界队列?
先说概念:
有界队列
有界队列:就是有固定大小的队列。比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列
无界队列:指的是没有设置固定大小的队列。这些队列的特点是可以直接入列,直到溢出。当然现实使用中,几乎不会有到这么大的容量(超过 Integer.MAX_VALUE),所以从使用者的体验上,就相当于 “无界”。比如没有设定固定大小的 LinkedBlockingQueue。
无界队列的特性:所以无界队列的特点就是可以一直入列,不存在队列满负荷的现象。这个特性,在我们自定义线程池的使用中非常容易出错。而出错的根本原因是对线程池内部原理的不了解。
使用无界队列创建了一个线程池如下:
ExecutorService executor = new ThreadPoolExecutor(2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
这里的队列都指线程池使用的阻塞队列BlockingQueue的实现,使用的最多的应该是LinkedBlockingQueue,注意一般情况下要配置一下队列大小,设置成有界队列,否则JVM内存会被撑爆!
来一张队列全体照
1.常见的有界队列
ArrayBlockingQueue:基于数组实现的阻塞队列;
LinkedBlockingQueue:基于链表实现的阻塞队列,该有界队列不设置大小时就是Integer.MAX_VALUE;
SynchronousQueue:内部容量为零,适用于元素数量少的场景,尤其特别适合做交换数据用,内部使用队列来实现公平性的调度,使用栈来实现非公平的调度,在Java6时使用CAS代替了原来的锁逻辑;
上面三个队列的共性:
put和take 操作都是阻塞的
offer和poll 操作不是阻塞的,offer操作时,若队列满了会返回false,不会阻塞;poll操作时,若队列为空会返回null,不会阻塞;
并不是在所有场景下,非阻塞都是好的,阻塞代表着不占用CPU,在有些场景也是需要阻塞的,put和take操作存在必有其存在的必然性;
ArrayBlockingQueue 与 LinkedBlockingQueue 对比:
ArrayBlockingQueue 实现简单,表现稳定,添加和删除操作使用同一个锁,通常性能不如后者;
LinkedBlockingQueue 添加和删除两把锁是分开的,所以竞争会小一些;
2.常见的无界队列
ConcurrentLinkedQueue:无锁队列,底层使用CAS操作,通常具有较高吞吐量,但是具有读性能的不确定性,弱一致性——不存在如ArrayList等集合类的并发修改异常,通俗的说就是遍历时修改不会抛异常;
PriorityBlockingQueue:具有优先级的阻塞队列;
DelayedQueue:延时队列,使用场景
- 缓存:清掉缓存中超时的缓存数据;
任务超时处理;
内部实现其实是采用带时间的优先队列,可重入锁,优化阻塞通知的线程元素leader
LinkedTransferQueue:简单的说也是进行线程间数据交换的利器
无界队列的共性:
put 操作永远都不会阻塞,空间限制来源于系统资源的限制;
底层都使用CAS无锁编程;
此题题解参数:
https://www.yuque.com/tiankongyiwusuoyouweihegeiwoanwei/kb/ud3nbr
-End-
最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!
面试题
】即可获取
在看点这里好文分享给更多人↓↓