跟Java面试官对线的一天!唬住就要50K,唬不住就要5K
Posted 鱼小洲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跟Java面试官对线的一天!唬住就要50K,唬不住就要5K相关的知识,希望对你有一定的参考价值。
前言
不积跬步无以至千里,不积小流无以成江海
终于呀,怀着期待的心走进了公司的大门,迎面而来的就是一个小姐姐。
hr:您好,请问你是今天过来面试的吗?
我:哇,这里的小姐姐都这么漂亮吗。嗯,你好,我是今天来面试的。
hr:嗯,那你先简单的做个自我介绍好吧。
我:(以下自我介绍是自己的)
嗯,好的。面试官你好,我叫彭于晏,毕业于XX大学。今天来面试贵公司的Java开发。我从事这个行业已经两年多了,先后做过XX项目。最近做的一个项目是在上家公司做的一个XXXX平台,我主要是负责里面的行为模块,以及XX功能。在处理并发这块我也有一定的经验,之前的项目QPS大概是在100W左右。对于技术栈这块,我比较熟练的是Spring Boot以及Spring Cloud。数据库方面能熟练的使用mysql和MongoDB以及Redis,并且搭建过公司的服务集群,对集群化配置这方面也比较熟悉。除此之外,我还喜欢平时看看书,看看妹子之类的。以上就是我的自我介绍。
hr:嗯,还不错。那你这边的情况我待会儿会给我们的技术经理 ,然后我们的技术经理会对你进行一个简单的面试。
我:嗯,谢谢了哦。
我:(这儿的hr妹子都这么漂亮吗,得加油了呀,为了终身幸福)
终于,面试官顶着一个地中海坐到了我的对面。
面试官:你好,那我们就开始面试可以吗?
我:好的。
JVM篇
面试官:嗯,看你的简历上写了你对JVM比较熟悉,那我就先问几个关于JVM的可以吧。
我:嗯,可以,没问题。
面试官:你简单的说一下JVM的运行时数据区。
我:我对JVM运行时数据区是这么去理解的。一共有两个部分,一部分是线程私有,另一部分是线程共有的区域。我们先说说线程私有的区域。在这其中一共又分为三部分:分别是程序计数器,本地方法栈和Java虚拟机栈。线程公有的部分其实在版本迭代的过程中,有了一些变化。在JDK1.6的时候,这部分是由堆和方法区组成的。但是在1.8 的时候,取消了方法区,变成了元空间,元空间使用直接内存。
面试官:嗯,你理解的还不错,那你讲一下程序计数器主要是干嘛的。
我:程序计数器在JVM中,是一块比较小的空间,听名字主要就是计数,但是我们一般习惯性的将他理解成当前线程所执行的字节码的行号指示器。之所以这块部分是线程私有的呢,是因为每个线程都拥有属于自己的程序计数器,最大的作用就是可以找到上一次线程执行的位置,从而继续执行我们的代码。各个线程之间的计数器是互不影响的。
面试官:嗯,回答的不错,那你知道在JVM中唯一一块不会出现OOM的区域是哪块区域吗?
我:嗯,就是我刚才说的程序计数器,因为这块是跟着线程的创建而创建,线程执行完了,也就销毁了。
面试官:那你再简单的讲一下Java的虚拟机栈这块区域吧
我:好的。这块区域和我们的程序计数器是一样的。也是线程私有的部分,而且他的生命周期和线程相同,描述的是方法执行的内存模型。 在Java内存中,我们大概就粗分为堆内存和栈内存,其中的栈就是我们说的这个虚拟机栈,实际上,虚拟机栈是由一个个的栈帧组成,而每个栈帧中都有局部变量表,操作数栈,动态链接和方法出口等信息。我们的局部变量表主要存放了编译期间的各种数据类型。比如你写个int i=0;那么他就会放在我们的局部变量表。
面试官:嗯,那我们的方法是怎么被调用的呢?
我:其实方法调用比较简单,主要就是将我们的方法压入栈,然后对应的调用就是一次方法被弹出栈的过程。比如说你调用了return或者说thr一下,都会被弹出栈。
面试官:那你再讲一下动态链接是什么?
我:动态链接最大的作用就是我们的多态类型了。我们举个例子,在栈中有个Map,但是在堆区有个HashMap,这俩货的类型肯定是子类和父类。所以动态链接最主要的就是将我们的类型自动指向子类去调用。
面试官:可以嘛小伙子,看不出来知识量储备还不错。那你接着说一下JVM中的垃圾回收。
我:首先,我们在讲这块的时候得去想,垃圾回收是在哪儿回收的。在我们new的所有对象,其实都是放在堆区中进行存储的。那么当这个new对象没有引用了咋办?那自然就会触发JVM的垃圾回收机制。为了方便垃圾回收,将堆区又细分成了年轻代和老年代这两个部分,他们的比例分别是占1/3和2/3。其中,在年轻代中,又细分成了伊甸区(Eden)和幸存区,在幸存区中我们又习惯性的说成是From区和To区。这三部分的比例是8:1:1。当我们的对象在Eden区放不下的时候,这时候就会触发一次GC,将我们的对象放进From区。然后当From区也满了之后,就会将部分未清理掉的对象放进去To区。当To区也满了之后,就会将对象放进From区,这样来回循环15次之后,对象如果还不死,就将这个对象放进去老年代。当我们的老年代也满了之后,就会完成一次Full GC(整堆收集),会造成STW(Stop The Word)的情况。所以我们JVM调优,其实最主要的就是尽量避免Full GC的次数。如果说我们创建的对象很大,那么这个对象就会直接进入老年代。一般来说,如果你想调整From和To区的次数,可以通过-XX:MaxTenuringThreshold来调节。
面试官:嗯,你讲的很不错,不过你刚才说的时候忽略了一个地方,那就是堆区容易产生OOM异常。当你的垃圾回收用了太多的时间,就会出现这个问题。
我:嘿嘿,尴尬尴尬,没想起来这块。
面试官:那你接着说一说垃圾收集器吧。
我:在说这块之前我们先说一个问题,那就是垃圾收集算法;我们知道,常见的垃圾收集算法大致分为以下两种:
引用计数法和可达性分析算法。
引用计数法主要实现的原理就是,给定一个标志位,当一个对象被引用的时候,就给这个标志位加上1,当没有对象去引用的时候,就去减1,这样当这个标志位变成0的时候就代表这个对象没有引用,可以收集。但是,这种算法有个弊端,那就是当有一对对象互相引用的时候,这时就没办法使用。所以又衍生出来另一种算法:可达性分析算法。
可达性分析算法最主要的原理是有一个GC Roots作为根节点。向下搜索,如果这个根节点搜索的过程中,没有对象去引用这个根节点,那么就说明这个对象是一块垃圾,需要去清理掉他。就我们常见的GC Roots,在Java中,有如下几种:static属性变量,JNI变量,字符串常量池的引用,被同步锁持有的对象。
垃圾收集算法一共大致分为以下几种:
分代收集,标记-清除,标记-整理,标记-复制。就拿jdk1.8来说,是混合使用的垃圾收集机制,在年轻代使用的标记复制,而在老年代使用的是标记整理。
我们说完了垃圾收集算法,再来聊一聊垃圾收集器。
垃圾收集器正是基于这些垃圾收集算法而实现的一些具体的收集器。现在的垃圾收集器共有以下几种:
Serial,ParNew,Parallel Scavenge,CMS,Garbage First(又称G1)。
这其中的一些细节问题,如果有兄弟需要的,请在文章底部留言,我会单独出一篇文章来详细解释。
面试官:那你知道Java中的四种引用类型吗?
我:在Java中,一共有四种引用类型,分别是强、软、弱、虚。
在强引用中,被引用的类型不管怎么样都不会被垃圾收集器回收。
在软引用中,通过可以理解为可有可无的对象,如果说在Java中,内存足够就把你留着,内存不够了,就把你扔了。你可以这样去理解。你去打工,人家店里面并不是很想要你,因为人已经招满了。所以就告诉你,如果哪天我们实在是人太多了,你就自己主动拜拜。
弱引用指的是只要垃圾收集器一运转,那么弱引用的对象就会被自动回收。
虚引用主要的作用是用来跟踪对象被回收的状态,收到一个回收的通知。
面试官:你再说说Java中的类加载机制吧。
我:(心里mmp,这么多了,还要问)嗯,好的。Java中的类加载是一种懒加载的形式。只有当这个类需要被使用的时候才会去加载,这样做的好处就是避免一次性加载全部,从而占用很大的内存。
在类加载的过程中,大致会经历以下7个阶段:加载、验证、准备、解析、初始化、使用、卸载。
在加载过程中,会将我们的.java文件转成class文件,然后将这个class文件转成二进制流。
在验证阶段会去验证当前的二进制流会不会威胁到虚拟机的安全,并且验证是不是当前虚拟机所需要的东西。
在准备阶段,为类变量进行赋值,并分配内存。但是应该注意一点,如果是实例变量,那只是做了一个初始化,而不是去赋值。比如说public static int value = 123; // 这儿只是给了一个初始化值为0,而不是123 public static final int value = 123; // 这儿作为一个类常量,就是直接赋值为123
解析阶段,将常量池的符号替换成直接引用。
初始化阶段开始真正执行类中定义的Java程序代码。在准备阶段,类变量已经赋值过一次系统要求的初始化,在初始化阶段,根据程序员通过制定的主观计划去初始化类变量和其他资源。我们说完了上面的加载生命周期,再说类的双亲委派和全盘委托。
以前使用的类,在加载阶段中,将这个类关联的所有类加载器全都加载出来。但是这样做有一个问题,那就是如果找到的类和我们使用的类的加载级别不是同一个。这样就会出现大问题。举个例子:我们知道在java.lang包下有个String类,那么如果我们也自定义一个这样的包和类,在使用的时候去调用这个类的main方法(原本自带的不存在main方法),这个时候就会出现一个异常(名字我忘了)。所以为了解决这样的问题,Java虚拟机采用了双亲委派机制来处理类加载。通常上面的问题也被说成是为了解决沙箱安全机制。
所谓的双亲委派机制大致就是这样的流程:在jvm中,按照等级一共分为三种类加载器,分别是系统类加载器,拓展类加载器和引导类加载器。在类加载的过程中,首先会去使用系统类加载器,但是系统类加载器会将这个类交给拓展类加载器,拓展类加载器又会将这个类交给引导类加载器去完成,简单的一句话就是,自己不想做的事情交给爹去做。然后在引导类加载器中发现没有,就又会往下一级一级的返回去加载,如果找不到,就报错。
这样做的好处是什么呢?就是可以让Java类随着它的类加载器一起具有一种带有优先级层次关系,从而使基础类得到统一。举个例子:Object类放在rt.jar中,如果我们自己去写一个一样的包结构和类名,那么由于双亲委派的存在,会从引导类加载器自上而下的去加载,由于rt.jar的优先级比ClassPath等级高,所以在整个过程中使用的都是rt.jar中的Object类。
面试官笑着说:小伙子可以嘛,JVM这一块你还是准备的不错,那我们接着问下一个部分呗。
我:嘿嘿,谢谢,那我们继续(继续幻想着hr小姐姐和那个前台小姐姐,顺便贴一张前台小姐姐的图)。
计网篇
面试官:那我简单的问问计网的知识。
我:好的,没问题。
面试官:OSI与TCP/IP各层的功能结构你简单介绍一下
我:在学习计算机网络的时候,我们一般采用折中的方法。将OSI的七层协议抽成五层。将最上层的应用层和表示层以及会话层抽成一个应用层。
所以自上而下一般都是:应用层、运输层、网络层、数据链路层和物理层。我简单的介绍一下这些层的作用。
应用层:主要任务是通过应用进程间的交互来完成特定的网络应用。应用层谢以定义的是应用进程间的通信和交互的规则,对于不同的网络应用需要不同的应用层协议。比如域名系统DNS,HTTP协议,电子邮件的SMTP协议。
运输层:主要作用是负责向两台主机进程之间的通信提供通用的数据传输服务,应用进程利用该服务传送应用层报文。
网络层:在计算机网络中进行通信的两个计算机之间可能会经过很多个数据链路,也可能还要经过很多通信子网。网络层的任务就是选择合适的网间路由和交换节点,确保数据及时传送。
数据链路层:通常简称为链路层。两台主机之间数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议。在节点之间传输数据时,链路层将网络层交下来的IP数据报组成帧,然后在节点上进行传输。
物理层:物理层的作用是实现相邻计算机节点之间比特流的透明传送,尽可能屏蔽掉具体传输介质和物理设备的差异。
面试官:那你能讲一下三次握手和四次挥手吗?
我:在网络传输的过程中,为了保证数据传输无误,通常采用三次握手的策略。
客户端发送带有SYN标志的数据包 代表第一次握手
服务端发送带有SYN/ACK标志的数据包 代表第二次握手
客户端发送带有AC标志的数据包 代表第三次握手
面试官:那为什么断开连接又需要四次挥手呢?
我:任何一方都可以在数据传送结束后发出连接释放的通知,等待对象确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放的通知,对方确认后就完全关闭了TCP连接。举个例子:你和张三打电话,然后你没啥说的了,你跟张三说我们挂了吧,然后张三说好,但是张三还没有说完,于是又说了一会儿,然后张三说,好了没啥说的了,挂了吧,然后你回张三,好的。
面试官:嗯…回答的比较简单,这个你回去了之后可以再看看这方面的书。我们问下一个问题,一个URL输入之后发生了什么?
我:通常来说,一个URL输入之后,会先给DNS解析,然后找到这个域名,然后去浏览器缓存或者DNS缓存或者路由器缓存去查找。这里使用的协议是DNS,找到了之后向web服务器发送一个HTTP请求(比如我们的Tomcat或者Netty),携带我们的cookie,服务器处理这个请求,生成一个html响应,然后将这个响应发送给浏览器,浏览器显示这个HTML。这里使用到的协议有TCP协议,用来与服务器建立连接。IP协议,发送数据。OPSF协议,路由选择器使用。ARP协议,将IP地址转为MAC地址。HTTP协议,访问网页。
面试官:TCP如何保证传输可靠性。
我:首先将应用数据包分割成TCP适合发送的数据块,然后对这些数据块进行编号,排序,再把有序数据传送给应用层。然后开始校验和,主要目的是检测数据在传输过程中的任何变化,如果有差错,将丢弃掉这个报文段。然后TCP的接收端会丢弃重复的数据。完事儿之后做一次流量控制,保证只能接收缓冲区能接受的数据,如果来不及处理,则提示对方降低发送的速率,防止丢失。
随后面试官问了我一个问题,你觉得我们公司的氛围咋样。我想也没想,公司的前台妹子长得不错。面试官听完笑了笑,其实我们公司的UI部门妹子也很不错,你要不要看看,说完就掏出手机找到了UI妹子朋友圈,翻了几张生活照给我看。
我顿时就有精神了,说,来吧,面试官我们继续。
面试官:嗯,可以啊,那我再问几个基础题。
Java基础篇
面试官:既然有了字节流,为啥还要有个字符流呢?
我:在Java中处理数据最终都是转换成字符流来处理。因为我们的数据每次都需要通过字节去转换字符,而且这转换的过程中还很耗时,如果我们不知大编码的话转换还很麻烦,所以Java为了避免这种频繁的转换,就直接有一个字符流来供我们操作。
面试官:浅拷贝和深拷贝有什么区别?
我:浅拷贝在基本数据类型中只是拷贝了一份数据值,在引用数据类型中是拷贝了一份内存地址(你可以理解为Linux中的软链接)
深拷贝:对基本数据类型进行值传递,对引用数据类型,新建一块内存区,将原地址拷贝一份放在这个内存区中(你可以理解为文件复制)
面试官:讲一下接口和抽象类的区别
我:在Java层面,接口是使用interface修饰,抽象类是使用abstract class修饰。在设计层面,接口更多是作为一种规范,而抽象类更多的是作为一种模板。在接口中除了static和final变量不能有其他变量。在抽象类中则没有这个限制。一个类可以实现多个接口,但是只能继承一个抽象类。
面试官:==和equals()区别
我:这个得分两种情况。 == 如果比较的是基本数据类型,那么就是比较两者的值是否相同。如果比较的是引用数据类型,那么就会去比较两者所在的内存地址是否相同。比如new String(“a”)和new String(“a”)是不一样的。
equals比较如果比较的是基本数据类型,那和 == 一样,如果比较的是引用数据类型,则需要看这个类是否重写了equals()方法。如果重写了,那么就比较的是两者的数据值,如果没有重写,那么比较的是两者的内存地址。
面试官:为什么重写了equals()还必须重写hashcode()
我:这是因为两者如果hashCode一样,但是equals()并不一定一样。在了解这块的时候先去了解一下hashCode是如何被计算出来的。hashCode其实就类似于一个int值,采用的杂凑算法实现。越捞的杂凑算法越容易计算出相同的hashCode值,所以如果不重写hashCode()方法的话,两者比较的值可能就会出现明明不同,但是计算出来的hash值却是相同的。(仔细想想hashSet是不是这个道理,建议看看源码)。
面试官:你知道Java创建对象有哪些方式吗?
我:一共可以通过四种方式创建。分别是 new一个对象 、 通过clazz.getInstance() 、通过constructor.getInstance(),和克隆(其实这儿我不确定序列化会不会创建对象,所以就暂定为四种)
面试官:知道jdk8中的stream流吗?怎么创建这个stream流
我:假如我们有一个数组,那么可以通过调用Arrays.stream(arrs)来创建。除此之外。如果我们有一个list,那么可以通过list.stream()来创建。得到的对象是一个Stream对象。用来对我们的数组进行操作。
面试官:刚才听你说自我介绍的时候,对多线程这块还可以,那我们换个多线程的话题接着聊。
我:…(嗯)
多线程篇
面试官:你知道哪几种创建线程的方式
我:可以通过继承Thread类,可以实现Rannable接口,还可以实现Callable接口,可以通过线程池来创建。
面试官:Callable接口和Rannable接口,这两个有啥区别?
我:Callable接口可以返回值,通过FutureTask来接收返回值,Runnable接口没有返回值,只能执行线程。
面试官:说一说线程的生命周期和状态。
我:在线程创建后处于 (new)创建 状态,调用start()方法开始执行,这个时候线程处于 (ready)可运行 状态。可运行的线程获得了CPU时间片后处于 (running)运行 状态。当线程执行wait()方法后,线程进入 (wait)等待 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 (TIME_WAITING)超时等待 状态相当于在等待状态的基础上增加了超时限制,比如通过sleep()方法或者wait()方法可以将线程置于timed waiting状态。当超时时间到达之后Java线程将会返回到runnable状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 (block)阻塞 状态。线程在执行Runnable的run()方法之后将会进入到 (terminated)终止 状态。
面试官:为什么我们启动线程时调用start()方法而不是run()方法?
我:因为调用start()方法才是让线程进入可运行状态,调用run()方法只是会当成一个main()下面执行的一个普通方法。
面试官:你对synchronized关键字怎么理解的。
我:synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被他修饰的方法或者代码块在任意时刻只能有一个线程执行。
在早期的Java版本中,synchronized属于重量级锁,效率比较低。但是在Java6以后对synchronized做了优化,所以能看到现在很多的框架源码底层都是用的synchronized。
(ps:锁升级的过程参考《Java并发编程的艺术》,这里就不多赘述了)
面试官:你平时用的哪种锁用的多,说说业务场景。
我:我个人比较喜欢使用Lock类中的lock()锁。首先,这个锁自由控制度比较高,而且能知道是否获取了这个锁。而且Lock锁可以自由设置公平与非公平锁。业务场景是使用了BlockingQueue来实现一个数据同步和监视的功能(具体不做赘述)
面试官:JMM了解吗?说一下
我:在jdk1.2之前,Java的内存模型总是从主内存中读取变量。而在当前的内存模型中,中间加了一个高速缓存,线程可以把变量保存在本地内存,这就会导致从主内存中读取到的值和从本地内存中读取到的值不一样,所以为了解决这个问题,我们就需要使用volatile来解决。volatile的主要作用就是保证变量的可见性和防止JVM的指令重排。
面试官:synchronized和volatile的区别是什么?
我:首先,两者是互补,而不是对立。volatile关键字是线程同步的轻量级实现,所以性能肯定是比synchronized好的。但是volatile只能用于变量,而synchronized可以修饰方法块和代码块。
volatile能保证数据的可见性,但是不能保证数据的原子性。synchronized两者都能保证。
volatile主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
面试官:ThreadLocal用过吗?讲一下底层原理实现。
我:我们平时使用ThreadLocal最主要的还是为了解决一个变量在多个操作中不同步的问题。假如说,我用了一个jdbc连接,但是在使用的时候发现这个连接没有了。这就有可能是其他线程在使用的时候使用完毕,就已经关掉了。所以用ThreadLocal就可以将这个连接保存一份副本,然后存储在自己的私有区域。这样,每个线程拿到的连接就都是自己的那一份,而不会互相打扰。他的底层原理还是使用的一个叫做ThreadLocalMap,我们可以理解为一个线程安全的concurrentHashMap,这个map中保存的就是我们每次存放进去的私有的副本。如果查看源码可以知道,其实我们的值并不是放在ThreadLocal中,而是放在ThreadLocalMap中。其中的有个方法执行的就是map.put(this,value);
面试官:线程池用过吗?讲一下他的构造实现
我:所有线程池的父接口都是Executor,但是根据阿里巴巴的开发规范,并不推荐我们使用这个去创建线程池,容易导致OOM异常,更加希望我们自己去设置参数。如果你自己去观察之后会发现,默认提供的四个线程池对象都是调用了ThreadPoolExecutor。其中共分为七个参数:分别是核心线程数,最大线程数,队列容量,存活时间,时间单位以及拒绝策略。(详细的我就不讲了,有需要的小伙伴可以去看一下构造方法)
面试官:你在自己的业务中怎么去使用线程池的?
我:在Spring boot中有自带的线程池叫做ThreadPoolTaskExecutor,它内部是维护了一个ThreadPoolExecutor,我们平时使用的时候就去重写这个类,然后设置好相应的参数进行使用。
面试官:JUC下的四种原子类你知道吗?简单的介绍一种
我:分别是基本类型、数组类型、引用类型和对象的属性修改类型。就简单的介绍其中用的比较多的AtomicInteger。我们知道i++并不是一个原子类的操作。正是由于主内存和工作内存的切换,导致变量i的值在多线程的操作下可能会出现数据紊乱的情况。我们除了使用上面说的那种volatile解决外,还可以使用AtomicInteger来保证i的原子性。
面试官:AQS你用过吗?你的业务场景是什么样的。
我:AQS全称叫做抽象队列同步器(AbstractQueuedSynchronizer)。他的内部设计主要是运用了模板设计模式,使用者去继承这个类,然后实现里面的模板方法。其中Java自带的核心的三个组件就是CountDownLatch、CyclicBarrier以及Semaphore。我在处理业务的时候遇到一个需求就是将一个很大的excel文件导入数据库中。最开始考虑采用MyBatis的forEach进行批量插入,但是最后执行的效果并不理想,最后使用了CountDownLatch,将数据进行切片,然后采用多线程+异步的方式进行处理。
面试官:嗯,你写个CountDownLatch的代码,说完就喊了一声,小馨,拿只笔和一张纸过来。
说完,门打开,进来了一个身穿汉服的小姐姐,长得是真漂亮,我都怀疑自己进了一个不正规的场所。要不是面试官是地中海我都准备办会员卡了。
我:public class CountDownLatchDemo { public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(6); ExecutorService threadPool = Executors.newFixedThreadPool(10); for (int i = 0; i < 6; i++) { threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+"Go out"); latch.countDown(); }); } try { latch.await();// 等待计数器归零,再向下执行 } catch (InterruptedException e) { e.printStackTrace(); } threadPool.shutdown(); System.out.println("执行完毕,关门"); } }
面试官:嗯,这部分的知识我们暂时就面到这里。我们来说一下Spring框架,你对Spring框架掌握的怎么样?
我:嗯,出了手写框架不太可能之外,其他的都没问题。
面试官:好,那我们继续。
(ps:顺便贴一下小馨的图)
Spring框架篇
面试官:看你做过的项目中大多都是Spring做的,那你说说Spring中的几个重要的模块
我:在Spring中,一共有以下几个模块:core、Aspect、aop、jdbc、jms、orm、web、test。其中,我们最常用的就是core中的ioc功能,aop实现切面,jms实现消息服务,web实现网络请求,test实现测试功能
面试官:刚才你提到了ioc,那你能说说ioc是什么吗?(谈谈对ioc的理解)
我:ioc(Inverse of Contol)名叫控制反转,是一种设计思想,就是原本将在程序中手动创建对象的控制权交给Spring框架来管理。ioc并不是spring中特有的。ioc容器是spring用来实现ioc的载体,他的本质实际上就是一个ConcurrentHashMap,里面存放的key就是bean的名字,value就是一个个对象。
将对象之间的相互依赖关系交给ioc容器来管理,并由ioc容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。ioc容器就像一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件即可,完全不需要考虑对象是怎么被创建出来的。
面试官:你是怎么去理解aop的
我:aop,俗称面向切面编程,能够将哪些与业务无关,但是又能为业务模块共同调用的逻辑或责任(事务、权限、日志)封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于拓展性和可维护性。
Spring AOP本质是基于动态代理,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy去创建对象,而对于没有实现接口的对象,就无法使用JDK Proxy去创建,这个时候就可以使用Cglib来生成一个被代理对象的子类来作为代理。
面试官:Spring中使用了哪些设计模式?
我:工厂设计模式、代理、单例模式、包装器、观察者模式、适配器模式。
面试官:Spring的事务管理方式有几种?
我:编程式事务和申明式事务。不过一般来说不推荐使用编程式事务,那样对代码的拓展性不强。所以一般都是使用申明式事务,而申明式事务又分两种,分别是基于XML配置和基于注解。
面试官:Spring中的事务隔离级别有哪些你知道吗?
我:一共有五种隔离级别。default、uncommitted、committed、repeated_read、serialzable。其中default主要使用的就是数据源默认的隔离级别,uncommitted允许读取尚未提交的数据变更,可能导致脏读、幻读和不可重复读。committed允许读取并发事务已经提交的数据,但是可能会有幻读或者不可重复读的发生。repeated_read可能会导致幻读。serialzable这个隔离级别可以阻止以上的全部问题,但是会严重影响程序性能,一般来说也不会用这个。像MySQL默认的就是repeatable。
ps:其实小伙伴关注的比较多的spring循环依赖在我认知中很少遇到问这个的。如果有需要,后期我会补上。
面试官:嗯,聊了这么久了,我们换个话题聊一聊。
我:哦,愿闻其详。
面试官:你平时喜欢钓鱼吗?
我:喜欢啊,就XX那个湖,我每次有时间就往那儿去。
面试官:哦,你也去那里吗?那下次我们有机会一起,加个微信
我:好啊,没问题(内心独白:嗯,感觉这次稳了)
面试官:嗯,那我们继续换个问题问问
我:没问题,随便问。
MyBatis框架篇
面试官:你知道
${}
和#{}
在sql语句中使用的区别是什么吗?
我:首先,#{}
类似于jdbc中的占位符,这个主要的作用就是会将我们的string类型的数据自动在两边拼接上一个引号,但是如果是${}
的话,则是直接拼接字符串。我们为啥用#{}
用的多,因为${}
会造成sql注入的问题。打个比方,如果我们执行一条语句是select * from t_user where name=xxx
,那如果我们使用的是${}
,在传入参数的手传个zhangsan;delete from t_user
,那么这条语句最后就会变成select * from t_user where name=xxx;delete from t_user
,我相信,你的技术经理看到你这么写,你当天下午就要被扫地出门。但是这个也并不是没有使用场景。当我们使用order by条件进行排序的时候可以使用这个,因为order by是一定在where之后生效,所以就算注入一条非法语句也不会产生影响,会被异常捕获。
面试官:通常在xml文件中,都会写一个Dao层接口与之对应,请问这个Dao接口的工作原理是什么?如果方法参数不同,能重载吗?
我:一般来说,接口的权全限定名就是namespace的值,接口的方法名就是MappedStatement的id值。Mapper接口是没有实现类的,当调用接口方法时,接口全限定名+方法名拼接字符串作为key,可以定位唯一一个MappedStatement。Dao接口的工作原理就是JDK动态代理,MyBatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象会拦截接口方法,转而执行MappedStatement所代表的sql,然后返回结果。方法是不能重载的,因为是按照全限定名+方法名去执行。
面试官:MyBatis的分页插件你用过吧,他的基本原理是什么?
我:MyBatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页,可以在sql内直接书写带有物理分页的参数来完成分页功能,也可以使用分页插件来完成屋里分页。
分页插件的基本原理是使用MyBatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,添加分页参数。
面试官:MyBatis中的插件接口有哪些?你怎么去自己编写一个插件?
我:MyBatis中提供了四种接口插件,分别是ParameterHandler,ResultSetHandler,StatementHandler,Executor。MyBatis使用JDK动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是invocationHandler中的invoke()方法。假如我们需要自己写个插件,那么只需要去实现MyBatis的Interceptor接口并复写intercept()方法,然后给插件编写注解,指定要拦截哪些方法就行。
面试官:MyBatis中将sql执行结果进行映射成对象的方式你是怎么做的?
我:一般就两种形式,一种是使用sql的别名,另一种是使用resultMap
面试官:MyBatis中有哪些Executor执行器?他们的作用是什么?
我:SimpleExecutor,ResuseExecutor,BatchExecutor。SimpleExecutor每执行一次update或者select,就开启一个Statement对象,用完就立刻关闭。ResuseExecutor:可以重复使用Statement。BatchExecutor:执行update(jdbc批处理不支持select),将所有sql都添加到批处理等待统一执行。
面试官:怎么去开启mybatis的懒加载
我:通过设置配置文件中的lazyLoadingEnabled=true|false
面试官:聊了这么久了,喝杯水吧。小馨,麻烦倒杯水。
稚子:她上厕所去了,我给你们倒吧。
我:嗯?又是一个妹子。
说完,门开,走进来一个上身胸口一个可爱萌的短袖,下半身一个牛仔短裤,长发飘飘的仙女就走了进来,端着两杯水。当然,处于礼貌的我还是站起身来帮她接了下来。嗯,妹子的身高刚好到我肩膀。
面试官:嗯,我们接着聊聊MySQL如何?
我:嗯,当然可以。
面试官:今天也聊了这么多了,MySQL我就简单的问问。
我:(信你个鬼)嗯,好的。
MySQL篇
面试官:讲一下MySQL中的索引吧。
我:MySQL中使用的数据结构来说主要有BTree索引和哈希索引。对于哈希索引来说,底层的数据机构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以考虑采用哈希索引,性能最快。其余场景,还是建议选择BTree索引。
MySQL的BTree索引使用的是B树中的B+Tree,但是对于主要的两种存储引擎的实现方式还是不用的。
MyISAM:b+Tree叶子节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree的搜索算法搜索索引,如果指定的key存在,则取出其data域中的值,然后以data域的值为地址读取相应的数据记录。这也被称为ie“非聚簇索引”
INNODB:其数据文件本身就是索引文件。相比MyISAM来说,索引文件和数据文件是分离的。树的叶子节点保存了完成的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被成为“聚簇索引”。而其余的索引都作为辅助索引,辅助索引的data存储相应记录主键的值,而不是地址。这也是和MyISAM不同的地方。在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,在走一遍主索引。所以一般在设计表的时候,不建议使用很长的字段去作为主键。
面试官:什么是事务?
我:事务是相对逻辑上的操作,简而言之就是要么都执行,要么就都不执行。举个例子:你和张三转账,你给他转100,那么你的账户相应的要减少100块,他的账户要多100。不能出现说你的少了,但是他的没有增加。
面试官:事务的四个特性说一下。
我:ACID。分别是原子性,持久性,一致性和隔离性。
面试官:怎么看sql的执行有没有走索引?
我:通过explain去查看语句。比如:explain select * from t_user
面试官:索引失效的十个原则你知道哪些?
我:
1、不要使用select *,这样会导致全表扫描
2、如果索引了多列,比如(id,name,age),那么在查询时,必须根据最左原则。中间的索引列不能断开,也就是必须根据表的从左到右的顺序
3、不能在索引列上做任何操作,这样会导致索引失效
4、存储引擎不能使用索引中范围条件右边的列,比如有个索引是x,y,那么只使用where y=xxx索引不会生效
5、尽量匹配精确的字段值,不能全扫描
6、尽量不要使用!=或者<>
7、尽量不要使用is null,is not null
8、like查询,%开头会失效,%结尾不会
9、避免隐式转换,字符串要加单引号
10、少用or查询
面试官:嗯,数据库知识储备还不错。我们接着问问Redis
我:(mmp,这么多了还问)嗯,可以啊。
Redis篇
面试官:你们为什么要使用Redis来做缓存,有没有考虑过其他的缓存。
我:除了redis,还有一种缓存叫做Memcached,不过对于我自己的技术栈来说,我个人更偏向于使用redis。在系统没有引入redis之前,系统的延迟很高,并且MySQL的压力很大。之后引入了Redis之后,能很好的降低系统数据库的压力。我们将大部分不易变动的数据全部放在了Redis,这样,由于Redis的查询高效性,一方面能提高用户的体验度,另一方面也能降低数据库的压力。
面试官:你知道Redis的单线程模型吗?能简单的说说吗?
我:Redis中使用了Reactor模式来开发了一套Redis 基于 Reactor 模式来设计开发了⾃⼰的⼀套⾼效的事件处理模型 (Netty 的线程模型也基于 Reactor 模式,Reactor 模式不愧是⾼性能 IO 的基⽯),这套事件处理模型对应的是 Redis中的⽂件事件处理器(file event handler)。由于⽂件事件处理器(file event handler)是单线程⽅式运⾏的,所以我们⼀般都说 Redis 是单线程模型。
(ps:具体的情况,如果有兴趣的可以看看我这篇文章:https://blog.csdn.net/weixin_43581288/article/details/118939539)
面试官:Redis使用的是单线程还是多线程?
我:在Redis6.0之前一直都是使用的单线程,但是在6.0以后引入了多线程。
面试官:那为什么平时说的时候还是习惯说Redis是单线程。
我:那是因为Redis引入的多线程并不是用来处理事件IO,而是为了提高网络IO的读写性能从而引入的多线程。它的文件事件处理器其实还是个单线程的。
面试官:你知道怎么开启Redis的多线程吗?
我:在redis.conf文件中找到 io-threads-do-reads yes # 原本是no,需要改成yes。然后在设置它的线程数 io-threads 4 #官⽹建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程
面试官:如果我现在MySQL中有200w数据,需要在Redis中存20w数据,怎么保证这20w数据都是热点数据?
我:这个得去考虑Redis中得淘汰策略;Redis中一共有6中数据淘汰策略,分别是:
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使⽤的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移除最近最少使⽤的 key(这个是最常⽤的)
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁⽌驱逐数据,也就是说当内存不⾜以容纳新写⼊数据时,新写⼊操作会报错。这个应该没⼈使⽤吧!
在4.0版本后增加以下两种:- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使⽤的数据淘汰
- allkeys-lfu(least frequently used):当内存不⾜以容纳新写⼊数据时,在键空间中,移除最不经常使⽤的 key
面试官:你知道Redis中的持久化机制吗?
我:在redis中有两种持久化机制。分别是rdb和aof机制。redis默认是开启的rdb机制。如果要开启aof机制,则需要去改动redis.conf文件中的appendonly yes
面试官:怎么开启Redis的事务?
我:使用multi命令开启一组事务,使用multi之后,书写的命令并不会马上执行,而是将这一组命令放在队列中,然后使用exec命令去执行 如果不想执行这些命令 则可以使用discard命令取消这一组事务 我们还可以使用watch去监视key的变化,这种场景经常用来实现分布式事务控制。
面试官:你刚才提到了分布式事务,你平时处理分布式事务怎么做的?
我:我个人比较喜欢使用Redisson这个框架。在之前的时候我习惯是用一个key来当作标志位,然后使用watch命令去监视这个key是否有发生变化,如果是这个key发生了变化,那么事务则取消。
面试官:你知道缓存穿透、击穿以及雪崩吗?
我:缓存穿透是指大量不存在的key请求redis,然后直接打到了数据库。缓存击穿是指某一个key在过期的时候,同时有这个key的大量请求。然后这个时候又会去设置数据到缓存中,但是大量的并发还是可能会把db击垮。缓存雪崩是指大量的key在同一个时间段大面积过期,造成大量数据直接打到数据库,造成雪崩。
面试官:那你能说说具体的解决方式吗?
我:缓存穿透我们可以考虑采用布隆过滤器去解决。缓存击穿我们可以考虑采用Redis得setnx去设置一个互斥锁,当操作成功后返回的时候在进行load db并重设缓存。或者我们可以考虑采用逻辑过期的策略。缓存雪崩我们可以将这些key的过期时间设置一个随机值,不让他们在同一个时间段过期。
面试官:嗯,今早上我们暂时先聊到这里,下午我们继续?
我:啊,下午我们聊什么?
面试官:嗯,今早上问你的这些都是比较基础的问题,下午我们聊一聊你项目中的分布式和微服务怎么样?
我:ok,没问题。那我们先去吃个饭?
面试官:走吧,一起,楼下有家餐馆还不错。(喊了喊小馨)
小馨:来了。
我:(os:这个妹子越看越好看)嗯,这位是叫小馨吧。
小馨:嗯,你好,我叫小馨。
我:你好,我叫彭于晏,那我们就一起吃个饭?
面试官、小馨:走吧。
(ps:给大家看看小馨吃饭的图片)
分布式、微服务篇
面试官:谈谈你对分布式的理解
我:所谓的分布式就是将不同的业务分散到不同的地方。在我的理解中,第一种分布式就是将不同的服务,比如MySQL服务和web服务部署到不同的机器上供用户访问。第二种分布式就像我们现在的微服务架构,一个大型的项目拆分成为各个小服务,对于这个单个服务,可以交给不同的技术团队去完成,最后完成部署上线,这也是分布式的一种。
面试官:我看你最近的项目用了SpringCloud做微服务,那你讲讲他的几个组件。
我:用通俗的话讲,既然是微服务,那么肯定是众多的服务了。这些服务得有一个统一的管理中心去做为管理,就比如说你作为一个公民,你的身份证肯定是要受统一的管理。这个管理呢就叫做注册中心。一般就是使用eureka或者nacos,consul我们很少用。
当我们项目业务中有一个服务压力过于庞大,那么这个时候我们会考虑采用集群化部署,但是集群化部署也有一个问题,那就是我们的A服务怎么去调用这个B服务的集群。所以这里就会涉及到另一个组件,那就是feign。feign组件的最大作用就是做服务的调用。但是光服务的调用肯定是不行,我们的服务集群肯定是得考虑一种负载均衡的方式去让其他服务调用。所以这个组件就叫做ribbon。在feign的依赖中默认是集成了ribbon组件。
当我们的单个服务接受到庞大的用户量之后,难免在服务之间调用的时候会引发服务雪崩的现象,从而给用户造成不良好的体验。所以这个时候我们会考虑引入一个叫做服务的熔断,限流和降低的组件,这个组件叫做Hystrix或者sentinel。
在众多的服务中,可能有一些配置是分散在各个地方,以后我要修改,比如说我要在这个服务中加一个数据源配置,那么这样对于我们来说我很不友好的,所以这个时候我们会去使用一个全局配置中心,比如说config,或者nacos。
在我们的这些微服务中,最终是要交给前端进行调用的,如果我们每一个服务就有一个地址,那这样对于前端来说肯定是不友好的,所以我们引入了另一个组件叫做网关,也叫gateway,这个网关最大的作用就是可以提供一个路由地址,根据这些路由地址来匹配对应的服务,然后供前端进行调用。网关的另一个作用就是可以对前端访问的用户做统一的鉴权操作。就好比小区门卫,你要进小区你肯定得先经过门卫,然后做身份信息认证。
目前的spring cloud技术选型:注册中心:nacos;远程调用:openfeign;负载均衡:ribbon;服务熔断:sentinel;配置中心:nacos;网关:gateway
面试官:你刚才说到了微服务的鉴权操作,你一般会怎么去做这个鉴权的设计。
我:权限的设计,现在用的比较多的就是叫rabc,就是基于角色的权限设计。就是我们会先去考虑一个用户有哪些角色,然后根据这个角色去分配对应的权限。这样来说,我们并不会一开始给用户就赋予权限,而是先给用户角色,然后再根据角色去设计用户的权限。所以我在设计的时候一般就会去做三个实体,分别是用户,角色和权限。这三者之间的关系呢,用户和角色是一个多对多的关系,角色和权限的关系也是多对多的关系。
面试官:那你们现在用的比较多的权限框架是什么?
我:之前做的一个项目是前后端分离的单体架构,用的是shiro。微服务项目中用的比较多的是Spring Security。
面试官:那假如说你用了这个权限框架,提供给用户的url,是任何人知道这个url的用户都可以访问吗?
我:既然我们已经做了一个权限的设计,那我们肯定得先去判断当前访问这个url的用户应该有哪些权限,如果说权限不够的话,我们是不允许访问的。
面试官:那我看你项目中使用的是jwt做的这个资源控制,你能具体说一下吗?
我:jwt的话,在我们以前做的时候,都是用户登录成功之后,就返回一个消息,然后服务端保存好用户的session,然后下次访问资源的时候直接看这个session中是否有用户,用户是否有对应的权限。但是session存在一个问题,那就是在分布式中,session他只是保存在一个机器中,在另一个机器中就会出现ses以上是关于跟Java面试官对线的一天!唬住就要50K,唬不住就要5K的主要内容,如果未能解决你的问题,请参考以下文章
阿里6面,成功唬住面试官拿了26K,突然感觉软件测试面试貌似不太难...
蚂蚁金服6面,成功唬住面试官拿了36K,突然感觉 “ 测试 ” 面试貌似也不太难...
阿里6面,成功唬住面试官拿了27K,软件测试面试也没有传说中那么难吧....