【面试进行时】大厂常考面试题一览(一)

917 阅读11分钟

这几天在整理之前的面试资料,又偶在在群里和各位同学一起探讨些奇妙的问题,心有所感,便准备将这些题目整理起来,一来是分析给大家,二来也以备日后之需。

一面二面主要是基础面,问的都是一些基础知识,难度不是很高,这里我就简单的分享一下我所遇到的题目。

一、面试者你好,我们开门见山,能跟我介绍一下spring的核心特征吗?包括Spring bean的生命周期也可以讲一下。

对于java工程师来说,几乎没有多少不了解Spring了,Spring Boot 、Spring Cloud等等也是信手拈来。但是很多和我一样的新人对于Spring的源码还是了解的不深,但是看过几遍之后,觉得还是有意义的。

众所周知,Spring的核心特性就是IOC和AOP

IOC(控制反转)是依赖倒置原则的一种代码设计思路。就是把原先在代码里面需要实现的对象创建、对象之间的依赖,反转给容器来帮忙实现。IOC容器主要通过注解和XML的形式来配置类之间的依赖关系,降低了类之间的耦合关系。IOC

AOP(面向切面编程)则是Spring的另一个核心特征,它把一些与业务无关,却为业务模块所共同调用的逻辑封装起来,比如日志记录,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。它主要由JDK动态代理和CGLIB代理实现。

至于Spring bean的声命周期,我们先看下图,然后再慢慢讲。 因为是面试,我们不用讲的非常细,心中有图然后翻译出来就行了,具体的内容,大家可以自己打开编译工具,然后找到这个类ClassPathXmlApplicationContext,找到这个方法往下看就行

Bean的生命周期:

createBeanInstance() -> 创建实例

populateBean() -> 属性赋值

initializeBean() -> 初始化

1、在doCreateBean方法中调用createBeanInstance进行实例化

2、还是在同一个地方,赋值并初始化我们的bean

但是Bean生命周期内的拓展点非常的多,远远不是上文这一点。 我们可以这样总结一下:

1、实例化Bean对象
2、设置Bean属性
3、如果通过各种Aware接口声明了依赖关系,则会注入Bean对容器基础设施层面的依赖。
Aware接口集体包括BeanNameAware、BeanFactoryAware和ApplicationContextAware
分别注入Bean ID、Bean Factory 和ApplicationContext
4、如果实现了BeanPostProcesser,调用BeanPostProcesser的前置初始化方法postProcessBeforeInitialization
5、如果实现了InitializingBean接口,则会调用afterPropertiesSet方法
6、调用Bean自身定义的init方法
7、调用BeanPostProcesser的后置方法postProcessAfterInitialization
创建完毕
如果我们要销毁的销毁话
8、容器关闭前调用DisposableBean的destroy方法和自身的destroy方法

二、你讲的很精彩,那么能不能顺便讲一讲AOP的实现方式?

AOP的实现主要是通过JDK动态代理和CGLIB代理来实现,这两种方法随着java的发展,在效率上已经没有多少区别,主要的不同点在于一个是JRE提供给我的,一个是需要依赖第三方jar包实现。

不,这不是最重要的区别。

java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,被JDK动态代理的类必须实现接口

而CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。因为才用的方法是继承,所以该类或者方法最好不要声明成final的。

三、可以讲一讲HashMap的底层原理吗?

这道题真的可以说是必考。。。或许每个面试官都回答过这个问题?

首先,我们知道HashMap是一个集合,键值对的集合,源码中每个节点用Node<K,V>表示。

其次,底层通过数组+链表/红黑树来实现。

HashMap的初始容量是16,负载因子是0 75,扩容方式为newsize = oldsize*2,size大小为2的n次幂 扩容后一定会变成2的n次幂 每次扩容时,原数组中元素依次重新计算存放位置,并重新插入。

HashMap在JDK1.7版本的头插法实现元素插入,到了JDK1.8版本升级成尾插法,为什么这样?因为采用头插法可能出现死链。同时,1.8还是先插入后扩容。

讲到了红黑树,我们再来讲讲为什么用红黑树?

红黑树性质
性质1:每个节点要么是黑色,要么是红色。
性质2:根节点是黑色。
性质3:每个叶子节点(NIL)是黑色。
性质4:每个红色结点的两个子结点一定都是黑色。
性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

因为比平衡树快:

1AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

四、不错,不知道你有没有听过G1垃圾回收器?

G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。

G1和CMS一样,采用的是分代回收机制。

**Young GC ** 当Eden区的空间占满之后,会触发Young GC,G1将Eden和Survivor中存活的对象拷贝到Survivor,或者直接晋升到Old Region中。Young GC的执行是多线程并发的,期间会停顿所有的用户线程(stop-the-world)。

Old GC / 并发标记周期

  • 1、初始标记:这个阶段会stop-the-world,它伴随着一次Young GC,然后对Survivor区的对象进行标记
  • 2、扫描根引用区:从上一阶段标记的根区域中,标记所有拥有老年代对象引用的存活对象,这是一个并发的过程,而且必须在进行下一次Young GC之前完成
  • 3、并发标记:寻找整个堆的存活对象。这个阶段是并发执行的,中间可以发生多次 Young GC,Young GC 会中断标记过程
  • 4、重新标记:这个阶段会stop-the-world,同时完成最后的存活对象标记。它使用了比 CMS 收集器更加高效的 snapshot-at-the-beginning (SATB) 算法。这个阶段会回收完全空闲的区块
  • 5、复制/清除:G1统计存活对象和完全空闲的区域,完全空闲区域将被重置回收。

五、你对消息中间件了解多少

在工作中,比较常见的是rabbitmq、kafka、Rocketmq这几种消息中间件,我所在的公司现在用的是rabbitmq。

面试官问我们这个问题,其实是希望我们讲讲工作中的业务场景,这个业务场景中为什么要使用消息中间件。

消息中间件看起来很复杂,其实核心说起来就是:解耦、异步、削峰。

解耦:RPC的调用是强依赖的,系统之间的调用会存在耦合,解耦就是把本来耦合在一起的几个系统给分开,通过消息中间件可以将业务上弱依赖、系统调用上的强依赖关系进行解耦。

异步:其实还是上一步解耦的延生,原先业务上是同步代码采用中间件进行异步操作以后,系统的响应耗时大大降低。

削峰:MQ会提供两种模式消息获取方式:PUSH推模式,PULL拉模式,消息中间件根据自己的处理能力,每隔一定时间,或者每次拉取若干条消息,实施流控,达到保护自身的效果。

但是,引入了消息中间将势必会增加系统的复杂性,需要考虑更多的问题:如何保证消息没有重复消费?如何处理消息丢失的情况?如何保证消息传递的顺序性。

六、哦?那你就讲讲如何保证消息没有重复消费?如何处理消息丢失的情况?如何保证消息传递的顺序性?

诱引面试官问你擅长的问题,但是很多时候面试官都会一笑而过,假设今天没有一笑而过

如何保证消息没有重复消费?

有时候重启系统、重启应用等突发事件发生时可能会无可避免的产生MQ的消息重复推送。

但是重复消费并不可怕,只要我们考虑到消息的幂等性就行了。

如何保证幂等性? 比如我们可以在写数据库的时候,根据主键查询一下,如果有,那么就说明已经消费过了。 或者可以用redis来写操作,redis的set有天然的幂等性。 比较常用的就是生成一个全局唯一的订单ID,到了消费者这里,可以根据这个ID进行判断。

如何处理消息丢失的情况?

消息丢失主要有三种情况,第一是生产者丢失了数据,第二是MQ丢失了数据,第三是消费者弄丢了数据,这次我就以rabbitmq来举例子讲讲如何处理消息丢失的情况。

一般来说。如果我们要确保生产者消息不丢失,可以开启confirm模式,在生产者那里设置开启confirm模式之后,每次写的消息都会分配一个唯一的id,然后如果写入了rabbitmq中,rabbitmq会给我们回传一个ack消息,告诉我们这个消息ok。如果rabbitmq没能处理这个消息,会回调我们一个nack接口,告诉我们这个消息接收失败,我们可以重试。而且我们可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么我们就可以重发。

面对rabbitmq自己弄丢数据这个问题,我们必须开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失,但是这个概率较小。

如果消费者丢失了消息,这个时候可以用rabbitmq提供的ack机制,简单来说,就是我们关闭rabbitmq自动ack,可以通过一个api来调用就行,然后每次我们自己代码里面确保处理完的时候,再程序里ack一次。这样的话,如果我们还没处理完就没有ack了。这个时候rabbitmq就会认为我们还没处理完,这个时候rabbitmq就会把这个消息分配给别的消费者去处理,消息是不会丢失的。

如何保证消息传递的顺序性

如果我们有一个queue,多 consumer。生产者向 RabbitMQ 里发送了三条数据,顺序依次是 ABC,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ中消费这三条数据中的一条,结果消费者2先执行完操作,把B存入数据库,这样就造成了消费顺序混乱。

我们可以拆分多个 queue,每个queue对应一个 consumer。或者可以一个queue只对应一个 consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理。

时间也不早了,今天的面试就到这里吧。

关于面试题这个专题,我会慢慢夹杂在java专题内更新,一次不会更新很多题,但是都是一些我或者其他小伙伴遇到的实际真题,面试题的内容也不会写的很底层,很专业,只是尽量在短时间内说清楚问题就好了。如果大家有什么遇的高频题目,也可以给我留言,我会一起写到这个专题里面。

我的微信: