不容错过的Java高级面试题
又到跨年之际,想必在这一年技术成长颇多的猿友们为备战金三银四而蠢蠢欲动了吧。工欲善其事必先利其器。停止无病呻吟和眼高手低,脚踏实地地狂刷面试题,offer拿到手软不再是空谈。帝都的雁为大家汇总本人在今次找工作中遇到的面试题,希望可以帮到猿友。
(PS:博主本次找工作参加面试的知名企业有:有快手/字节/阿里/滴滴/boss直聘/携程/猎聘/好未来/京东/美团/当当,最终也如愿进入其中一家大厂;面试题基于Java全栈,参照个人简历的技术栈由浅至深询问。故建议猿友们在简历上写自己hold得住的技能,切莫画蛇添足。以本人技术栈为例,答案均为本人的理解,仅供参考。)
一、设计模式
列举常被问到的设计模式。
策略(阿里/快手/京东/猎聘)
问:在项目哪些地方使用到了策略设计模式。
答:重构订单状态变更逻辑。系统中订单的状态有好多个,每一个状态对应一种业务逻辑,以前的代码按照if() else if()分支去处理,代码臃肿且冗余。我定义订单策略的抽象类抽取共同行为和属性,再将每个订单状态的业务逻辑封装为订单策略实现类,并放入spring的IOC中;定义一个枚举,将订单的状态和订单策略实现类的beanID进行映射。最后定义策略上下文对象,用于交互即可。
单例(快手/boss直聘/滴滴)
问:手写一个单例。
答:我一般为了简单,会直接写饿汉式。但会向面试官阐述单例的一些实现方式和注意事项。
单例是指一个类在一个JVM中仅有一个实例对象。常见的实现方式有饿汉式、懒汉式、线程安全的懒汉式、双重检验锁+volatile、静态内部类、枚举等方式。反射和反序列化可以破坏单例,所以需要在单例的构造中再次判断实例对象是否已创建,进而抛出异常进行规避。
代理(猎聘/字节/阿里)
问:动态代理的实现方式。
答:继承目标类或实现目标类接口。
JDK动态代理基于实现目标类接口,写一个方法增强器实现invocationHandler接口,重写其invoke方法,然后通过Proxy.newInstance的方式传入目标类的类加载器、目标类实现的接口,以及自定义的方法增强器,然后创建代理对象。本质上是动态拼接了一个实现了目标类接口的代理类的字符串,将这个字符串输出至本地的一个Java文件中,再通过Java编译器编译,变为class文件,类加载器将其加载至JVM内存,以反射的方式进行实例化使用。
CGLIB动态代理基于继承目标类。写一个MethodIntercepter的实现类,重写其invokeSuper方法,然后通过Enhancer的方式进行创建使用。底层通过ASM字节码技术生成了三个文件:代理类字节码、目标类索引文件、代理类索引文件。索引文件中将当前类中方法名称和参数列表类型组成签名,按照hash生成一个下标,然后通过switch case的方式列举,故而访问起来会比反射的方式更快。
观察者(boss直聘/携程/滴滴/美团/当当)
问:观察者的使用场景。
答:订单发货后需要给商家发短信和邮件。由于我们系统前期没有引入MQ,所以采用异步线程去进行解耦。发货可以看作一个事件,短信和邮件可以看作两个观察者。采用spring的Event事件通知去实现此功能。
模板方法(阿里/快手/京东)
问:项目中怎么使用的模板方法。
答:系统中很多批量导入的业务代码冗余,且其大致操作流程相似,为了便于维护,故而将批量导入的流程抽取至抽象类中,具体的行为抽象交给不同的业务代码实现即可。
题外话(京东/猎聘/好未来/滴滴/当当)
问:目前对于设计模式的使用分为截然不同的两个派系,大力拥护和强烈反对。如何看待设计模式?
答:设计模式有时可能不直观,非本人编写的代码,阅读起来有可能有些绕,不利于新手修改。但它方便扩展,且可以使业务代码之间很好的解耦,对于源码的阅读也有很大帮助。
二、JAVA基础
Java基础包括JDK和spring体系、mybatis。
线程池(快手/字节/阿里/滴滴/boss直聘/携程/猎聘/好未来/京东/美团/当当)
问:线程池的核心组件有哪些?为什么不推荐使用JDK自带线程池?如何手写一个线程池。
答:线程池核心参数有核心线程数(线程池启动后,一直处于活跃状态的线程)、队列长度(核心线程都在处理任务时,新来的任务会放入此队列中)、最大线程数(队列满了后,会再开启一定数量的线程,即线程池最多创建的线程数量)、拒绝策略(队列满了,最大线程数的线程均在处理任务,此时还有任务投递,则进行的动作)。
JDK自带的线程池有四种:固定长度(队列*)、可缓存(最大线程数*)、可定时(最大线程数*)、单例(队列*)。由于其*(队列或最大线程数),可能会造成线程太多或任务太多带来的OOM(out of memory)问题。我研究过定时线程池,内部维护了一个延时工作队列(底层采用小堆顶的方式实现),任务携带时间间隔的参数,每当任务要执行时,会把当前系统时间+时间间隔后,把这个任务再次投递至延时工作队列中。
手写线程池思路:定义阻塞队列。创建核心线程数的线程,使其一直处于活跃状态(死循环即可),当有任务需要执行时,从队列中获取任务执行。
并发包(快手/字节/阿里/滴滴/boss直聘/携程/猎聘/好未来/京东/美团/当当)
问:sync和lock的区别。公平锁与非公平锁的区别。CAS有什么问题?如何解决?
答:我个人认为lock就是仿照sync的底层原理以Java代码的方式实现了一次。
Sync内部维护了一个monitor对象,用于管理锁的开销。其内部有锁池(抢锁失败的线程)、等待池(调用锁wait方法的线程)、锁持有线程、重入次数、竞争逻辑。
Sync存在锁的膨胀(粗化)。偏向锁:即一把锁在一段时间内只被同一个线程持有,那么当这个线程来访问锁时,不会对其进行加锁操作,而是在锁对象的对象头markword中存放偏向锁的信息,记录当前线程ID,用于比较。轻量级锁:一个线程持有一把锁的时间非常短,那么抢锁失败的线程不会直接阻塞,而是自旋等待(将锁对象头中偏向锁信息拷贝至自己线程的工作内存中,以CAS的方式自旋)。重量级锁:如果多次自旋失败,则不会继续这种消耗CPU资源的行为,而是直接将其阻塞,等待锁的释放。
Lock锁本质上是AQS的一层封装。其内存也有状态(记录锁重入次数)、阻塞双向链表(抢锁失败的线程)、锁持有线程、condition的单向等待链表(调用condtion.await方法的线程)。
AQS默认创建为非公平锁,当线程获取锁失败时,会短暂自旋一次,然后将当前线程通过LockSupport的方式进行阻塞,然后封装为NODE节点,放入阻塞链表的末尾。当锁释放后,从阻塞链表的头部取出一个节点,将线程唤醒,继续抢夺锁资源。由于期间可能出现非阻塞链表的线程参与锁资源的争夺,对排队的线程不公平,所以为非公平锁。
公平锁在非公平锁的基础上进行判断,抢到锁的线程如果不是阻塞链表头部的线程,则将其阻塞,放入阻塞链表末尾排队等待。
CAS自旋消耗CPU资源,且存在ABA问题,可以通过版本号去解决。
参考:Synchronized的花花肠子和AQS的傀儡之Lock锁
集合源码(快手/字节/阿里/boss直聘/携程/猎聘/京东)
问:arrayList底层实现。HashMap1.7和1.8有什么区别。ConcurrentHashMap1.7和1.8有什么区别。
答:arrayList底层采用数组实现,默认长度10,1.5倍扩容。扩容需要数组的拷贝,删除需要移位,由于共享全局的数组,且其对数组的维护没有加锁,故非线程安全。存在fail-fast机制。
HashMap1.7采用数组+单向链表实现,初始容量16,负载因子0.75,允许存放key为空的数据(放在下标为0处),将key的hashcode值通过hash算法得出hash值,再将hash值与数组的长度按位与,得出下标,在下标处以头插法存放数据。由于没有对共享的数组加锁,非线程安全。2倍扩容,高并发下,由于头插法和非线程安全,可能导致链表死循环。HashTable是在HashMap的基础上,对其所有方法加sync保证线程安全,但HashTable不允许出现空值。
HashMap1.8采用数组+单向链表+红黑树实现。当单条链表的长度大于8且集合元素超过64,会将这条链表转为红黑树,提升查询效率(链表时间复杂度为O(n),红黑树时间复杂度为O(log(n))。链表的插入方式改为尾插法。
ConcurrentHashMap1.7底层采用16个segment对象,segment和hashTable类似,所以ConcurrentHashMap1.7保证线程安全的方式采用分段锁,从而提升效率。
ConcurrentHashMap1.8是在HashMap1.8基础上优化,对其所有非线程安全的操作加锁处理。比如数组初始化和扩容,采用CAS方式保证线程安全,对于单个下标下数据的存放,采用sync锁起来,粒度更细。且当某个线程访问ConcurrentHashMap时,如果发现正在扩容,不会阻塞这个线程,而是帮助ConcurrentHashMap完成扩容。
Java内存结构(快手/阿里/猎聘/好未来/京东/当当)
问:类加载过程。双亲委派如何打破。Java内存如何划分。常用的垃圾回收器和垃圾回收算法有哪些。
答:类加载器通过编译、链接(验证、准备、解析)、初始化的操作将class文件加载至Java内存中。
双亲委派是指Java查找类的方式自上而下(启动类加载器、扩展类加载器、应用类加载器、自定义类加载器)。类加载器中有个findClass方法,递归向上查找上级类加载器,我们只需要绕过这个方法即可。
Java内存机构分为:
线程独占:栈(由栈帧组成,每一个栈帧就是一个方法。栈帧由局部变量表、操作数栈、动态链接、返回地址组成)、程序计数器(记录当前线程运行的位置)和本地方法栈(被native修饰,与C通讯)。
线程共享:元空间(存放静态信息,类的字节码信息)、堆(创建的对象、字符串常量池)。
堆内存:以分代算法划分为新生代和老年代。新生代又分为eden区(刚创建的对象)、from和to区(复制算法,用于年龄累加)。
常用垃圾回收算法有:标记清除、标记整理、复制、GCROOT。
常用垃圾回收器有:CMS、G1、Servier New/Servier Old
参考:被解刨的JVM
Java内存模型和volatile(快手/阿里/滴滴/携程/猎聘/京东/当当)
问:介绍下Java内存模型。Volatile如何保证内存可见的。
答:Java内存模型分为主内存和工作内存(本地内存)。主内存存放全局共享变量数据,而工作内存存放这些共享变量的副本数据。每一个线程都会开辟自己的工作内存。而Volatile修饰的变量通过MESI缓存一致性协议去保证一致性,即当变量修改,CPU总线嗅探机制会捕获到它的变动,将其内容刷新至主内存,然后将其它工作内存的变量值置为无效,使得其它工作内存变量的值重新从主内存获取此变量值,保证一致。
mybatis组件(快手/阿里)
问:介绍下Mybatis的原理。
答:详情参考通俗易懂的Mybatis工作原理
spring组件(快手/字节/阿里/猎聘/好未来/京东/当当)
问:介绍下bean的生命周期。AOP如何实现的。如何解决循环依赖问题。声明式事务嵌套会发生什么问题。
答:spring容器启动时,会初始化spring的各种组件:beanFactory、后置处理器、event与listener的绑定、初始化单例对象等。初始化单例对象时,反射其无参构造创建对象,然后进行属性赋值,检查aware的依赖信息,执行后置处理器的前置操作,执行自定义init方法,执行后置处理器后置处理。
Spring通过三级缓存来解决单例的循环依赖,多例需要我们通过@primy或@qualify手动声明。
AOP也是后置处理器的一种特殊实现。在后置处理中判断当前类是否实现接口,进而判断使用JDK动态代理还是CGLIB动态代理,在对应的invoke中,通过责任链+递归的方式去依次执行切面的通知,最后执行目标方法。
事务是一种特殊的通知,通过手动try catch来提交或回滚事务。
当同类的事务嵌套时,this会使其失效。不同类事务嵌套,按照事务传播机制进行判断是否回滚。
springMVC组件(快手/阿里)
问:拦截器的原理。
答:springMVC的核心类DispatherServlet的doDispath方法中为springMVC的执行原理,通过模版方法获取所有的拦截器,循环调用执行其三个抽象方法的实现。
三、主流技术
以简历为例。
Redis(快手/字节/阿里/滴滴/boss直聘/携程/猎聘/好未来/京东/美团/当当)
问:redis的数据结构。淘汰策略。持久化机制。集群方式。分布式锁。分片原理。缓存击穿/穿透/雪崩的原因和解决方案。
Zookeeper(滴滴/京东/当当)
问:ZK节点的类型。分布式锁原理。集群原理。Base和CAP的理论。
答:节点类型分为有序临时、无序临时、有序持久和无序持久。支持权限认证,有着强大的事件通知功能。
分布式锁分为临时节点和有序临时节点。前者存在羊群效应,后者类似于Java的lock公平锁。
集群要保证过半机制,通过MVCC和myid来选取事务ID最大、性能最好的节点当选leader,其数据同步遵循最终一致性(BASE),保证CAP中的CP,通过过半机制保证脑裂现象。
消息队列(快手/阿里/滴滴/猎聘/好未来/京东/美团/当当)
问:MQ的工作方式有哪些。如何确保消息不丢失。如何解决重复消费。如何解决顺序消费。基于mq解决分布式事务。
答:工作方式有点对点、能者多劳(消费者手动ACK)、发布订阅fanout、路由direct、模糊路由topic。
消息不丢失:生产者投递消息成功后,采用confirm确保投递成功;消费者消费消息后,手动ACK保证消费成功;mq通过持久化机制将消息持久化至磁盘。
重复消费:记录消息ID(雪花算法生成),消费消息前主动查询比对。
顺序消费:多个消息通过key保证其落在同一台broker节点,然后使其被一个消费者消费。消费者在自己本地可采用内存队列的方式提升消费效率。
分布式事务:阿里的rocketMQ支持事务消息,通过两端提交协议去保证事务参与者的事务提交或回滚的一致性。
微服务(快手/字节/阿里/滴滴/携程/好未来/京东/美团)
问:什么是服务治理。服务注册和发现的过程。网关的工作原理。如何做到本地负载均衡。服务熔断器的原理。
答:服务治理就是管理服务之间调用混乱的现象。注册中心统一管理服务地址,其他服务启动时将自身的信息以服务名称的键值对投递注册至注册中心,若想要获取其它服务地址,也是通过其它服务名称去注册中心查找到集群地址,然后本地通过负载均衡算法进行轮询。
熔断器可以实现服务降级、熔断和线程隔离等功能。当此接口被熔断器保护时,只要请求时长超过预设时间,就会走指定的方法进行响应,实现降级处理。熔断是指如果请求此接口的线程数太多,则会走服务降级处理。我们也可以对热点接口实现线程池隔离的方式提升性能,不同的接口采用不同的线程池处理。
Netty(字节/阿里/boss直聘/携程/猎聘/好未来/京东/美团)
问:netty的作用。什么是NIO多路复用。为什么NIO在LINUX比WINDOWS性能要好。
答:netty本质上就是对NIO的多路复用做了一层包装。IO模型中分为BIO(获取不到就阻塞),NIO(获取不到不阻塞)和AIO(异步)。为了提升性能和降低CPU使用,采用一个线程统一管理这些socket的IO,Windows中才selector去不停的循环这些IO,但IO不一定每次循环都有数据,故会出现空轮询的情况;而linux采用epoll的事件驱动回调,当socket的IO有数据时主动通知机制去获取数据,故效率更高。
四、数据库
主要是mysql。
数据结构(快手/字节/阿里/滴滴/boss直聘/携程/猎聘/好未来/京东/美团)
问:MySQL的索引使用什么数据结构。为什么使用B+树。
答:mysql索引支持B树、B+树、Hash。默认采用B+树,hash查询快,但对范围查询不友好,B树的非叶子节点存放了索引值和数据(或数据地址),导致其单个节点存放的索引值变少,树的高度提升,增加IO次数。B+树非叶子节点只存放索引值,故而树的高度小,IO次数少,且叶子节点是一条有序的链表,对范围查询友好。
底层原理(滴滴/携程)
问:mysql的redolog、undolog和binlog分别有什么作用。
答:mysql默认采用innodb作为存储引擎,innodb维护了一个buffer pool的缓冲池来缓存数据。当更新数据时,会先将数据通过随机IO的方式从磁盘读取至缓冲池,在更新缓冲池数据前,会将缓冲池的数据写入至undolog中,用于事务回滚;更新完缓冲池数据后,将缓冲池的数据写入redolog中,用于灾备的恢复;然后将缓冲池的数据同步至物理磁盘,并记录在binlog日志,用于做主从复制或与mq同步。
索引(快手/字节/阿里/滴滴/猎聘/好未来/京东/美团/当当)
问:为什么索引可以提高查询效率。聚簇索引和非聚簇索引的区别。联合索引的使用方式。
答:数据是按照索引的方式有序存放,如同我们的新华字典存放汉字一样。聚餐索引即主键索引,数据和索引存放在一个文件中,而非聚簇索引的叶子节点存放的是主键的ID。联合索引遵循最左原则。
事务隔离级别(快手/阿里)
问:事务隔离级别有哪些。
答:读未提交(一个事务读到另一个事务未提交的数据,有脏读现象)、读已提交(一个事务读到另一个事务已经提交的数据,有不可重复度现象)、可重复读(事务之间通过MVCC隔离,但有幻读现象)、串行化(单线程)。
sql优化(快手/字节/阿里/滴滴/猎聘/好未来/京东/美团/当当)
问:项目中如何优化sql,以什么为标准,需要注意什么。
答:以阿里开发手册为准,通过explain去输出sql的执行计划,将其type优化为range级别以上(至少为range),同时避免filesort的内存排序情况。
欢迎大家和帝都的雁积极互动,头脑交流会比个人埋头苦学更有效!共勉!
公众号:帝都的雁
本文地址:https://blog.csdn.net/yxh13521338301/article/details/111991627