大概在29日中午11点30左右,招银科技对我进行了电话面试,主要问了有关于Redis、Mybatis、Mysql、Spring、单例模式、集合等一系列问题。感觉答得不是很好,很多地方一深入就回答不上来,很多看过的内容细节都忘记了。
下面把一些印象比较深刻的问题总结一下。
一上来自我介绍之后就开始问我有关于Redis的内容,主要是因为我在简历上写了有关于Redis的缓存雪崩的事情,然后面试官就开始问我是怎么解决的,并发量有多少?过期时间是怎么设置的,以及数据库连接数量是如何考虑的。
缓存雪崩这里回答的不是很好,面试官问我使用了什么过期策略,是如何设置过期时间的?
事后回想一下这么回答可能会比较好一些:
一开始的时候我们服务器存储用户数据时通过控制台登录的时候把该控制台的用户数据读取到内存中,所以过期时间是按照控制台的登录时间增加一个常数设定的。
然后有一天假期,控制台长时间没有交班,并且那一天的上机人数非常多,导致热点用户数据超过了这个过期时间,最后导致所有的请求都需要去访问数据库。
这个时候其实还行,不过数据库访问比较缓慢,但是客户觉得太慢了就直接重启了服务器。这个时候由于重启了服务器,控制台需要重新登录所以需要查询数据,还原计费线程又需要查询数据库,最后导致整个web就查询不了数据库了。
服务器停了一会,等到数据库的请求不是那么多了才恢复正常。
后来我把过期时间修改为了用户上机的时间增加24小时+随机数,随机数取在0~1小时之间,并且修改了数据库的连接数量。
那么如何设计数据库的连接数量呢?
https://www.jianshu.com/p/f48fa9b458eb
这篇文章里就讲述了数据库连接数量的设置原则应该是(2*CPU核心数+有效磁盘数)。所以下次不要回答直接把数据库连接数量增加了10倍这种答案了。
问了Redis之后开始问我对java的集合是否有了解,然后我回答说对Arraylist、linkedList、hashMap、concurrentHashMap有一些了解。
然后就让我说一下对hashMap的了解,我讲了负载因子、扩容、哈希冲突时候的处理方式。
然后问我如果给我一个数据长度固定为16的数据,设计hashMap应该怎么设计。
最后问了我有关于concurrentHashMap的内容,我说这个需要分为1.7和1.8两个部分,然后让我说一下1.8是如何实现线程安全的。
说完了线程安全就顺便提到了CAS和synchronizded关键字。
关于hashMap和concurrentHashMap的源码我反正之后会整理一篇源码解读的,所以这边先按下不表吧。
介绍了synchronized关键字之后就顺势问我对锁的了解有多少,有几种锁的实现方式。
我说除了synchronized还有Lock,以及AQS里面的一些锁,但是我对这个不是很了解。然后说了一下对象头里的锁信息以及膨胀策略。
然后问我可重入锁了解吗?
最后问我公平锁的原理是什么样子的。
说来惭愧,前几天才看过多线程,果然看的不够细还是要多看源码。
ReentranLock是在JDK层面实现的,而synchronized是在JVM层面实现的。
synchronized通过对象头里的锁信息,根据线程并发情况逐渐膨胀由偏向锁到轻量级锁再到重量级锁。
ReentranLock通过lock()和unlock()方法配合try/finally语句块执行,ReentranLock相比较synchronized有以下高级功能:
lock.lockInterruptibly()
方法实现,可以放正在等待的线程选择放弃等待,改为处理其他事情。notify()、notifyAll()
方法,通过Condition可以实现选择性通知。所以ReentranLock之后还是看看源码吧。
ReentranLock和synchronized都是可重入锁,可重入锁的特点在于在进入锁之后可以重复获取对象,每次获取锁,计数器+1,释放锁计数器-1,等到锁的计数器下降为0的时候才能释放。
而不可重入锁如果尝试再次获取对象会导致死锁。
公平锁是指锁的获取顺序完全按照线程请求顺序,按照先进先出的原则。
在ReentranLock会有一个tryAcquire
方法来尝试获取锁,该方法中调用了名为hasQueuedPredecessors
的方法来判断加入了同步队列的当前节点是否有前驱节点,如果有的就必须等前驱节点释放锁才能执行,这就保证了先进先出原则。
非公平锁的nofairTryAcquire
则没有调用这个方法。
公平锁的好处在于不会导致线程饥饿,但是切换线程会非常频繁。而非公平锁能够节约切换线程的开销,这是因为如果一个线程刚释放锁又要获取锁的话,这个线程获取所锁的概率会非常大,但是这样容易导致其他线程饥饿。
除此之外,还有读写锁,刚才说的锁都是排他锁,JUC包中的ReentranReadWriteLock就是一个读写锁,一般情况下由于读的操作总是比写的操作多一些,所以读写锁的性能就相对排他锁有着更好的吞吐量和并发性。
ReentranReadWriteLock主要定义了两个方法读锁和写锁——readLock()
和writeLock()
。
读锁类似于数据库中的S锁,多个读锁不会相互阻塞,但是写锁会阻塞所有其他线程。
数据库先问我用的什么引擎?我回答说InnoDB,然后介绍了一下InnoDB索引结构,使用B+树实现的。
然后问我是否建立了索引,主键索引和普通索引的区别,最后问了一下联合索引的最左匹配原则。
这一块回答的感觉还行,所以暂时就不展开了,忘记了可以看我之前的博客。
数据库(MySQL篇)
说到数据库之后,询问了我在使用Mybatis写like语句的时候需要注意什么?
我不太明白想问什么,所以没有回答上来。
这里想问的可能是索引失效?
好吧,确实有一些细节需要注意,但是我没有回答上来。
参考文章:https://blog.csdn.net/zhenwei1994/article/details/81876278
like语句使用一般有以下方法
and MemId like "%"#{Id}"%"
。and MemId like CONCAT('%',#{Id},'%')
之后问了事务的内容,问我Spring中事务有几种传播行为。
这里我忘记了,所以就说了两种,继承事务和新开子事务。
说完事务传播的问题之后,面试官问我是否了解Spring是如何实现事务的,我回答说使用AOP织入,然后说明一下JDK代理和CGlib代理,以及事务传播失效的问题。
说完这个之后问我,子事务是如何知道自己应该使用哪种传播行为的?
我说我不太清楚,因为这一块没有看过源码,事后回想起来应该是想问我如何获取注解?
找了半天终于找到了,参考了这两篇文章:https://zhuanlan.zhihu.com/p/73096544
https://blog.csdn.net/andy_zhang2007/article/details/85846066
直接来看AnnotationTransactionAttributeSource
类,正是这个通过注释来获取事务的信息,我这边选取了部分方法,第一个方法用于获取类上的注释,第二个用于获取方法上的注释,调用的都是determineTransactionAttribute
方法,在这个方法中存获取注释解析器的实现类annotationParsers
,然后通过这个类的迭代器来逐个获取注释信息。
@Nullable
protected TransactionAttribute findTransactionAttribute(Class<?> clazz) {
return this.determineTransactionAttribute(clazz);
}
@Nullable
protected TransactionAttribute findTransactionAttribute(Method method) {
return this.determineTransactionAttribute(method);
}
@Nullable
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement element) {
Iterator var2 = this.annotationParsers.iterator();
TransactionAttribute attr;
do {
if (!var2.hasNext()) {
return null;
}
TransactionAnnotationParser parser = (TransactionAnnotationParser)var2.next();
attr = parser.parseTransactionAnnotation(element);
} while(attr == null);
return attr;
}
我们还是再来复习一下事务的传播行为。
package org.springframework.transaction.annotation;
public enum Propagation {
/** 需要事务,是个默认传播行为,如果当前存在事务则沿用当前事务,
* 否则新建一个事务运行子方法。
*/
REQUIRED(0),
/** 支持事务,如果当前存在事务则沿用当前事务,
* 否则无事务。
*/
SUPPORTS(1),
/** 必须事务,如果当前不存在事务则抛出异常,
* 如果存在当前事务就沿用当前事务。
*/
MANDATORY(2),
/** 无论当前事务是否存在都会创建新事务运行方法。
* 新事务有单独的锁、隔离级别等特性,与当前事务相互独立。
**/
REQUIRES_NEW(3),
/** 不支持事务,如果当前方法存在事务的时候则挂起。
*/
NOT_SUPPORTED(4),
/** 不支持事务,如果当前方法存在事务的时候则抛出异常,否则无事务执行。
*/
NEVER(5),
/** 当前事务中的子方法发生异常的时候只回滚子方法而非整个事务。**/
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
在事务之后,问了我是否了解单例模式,让我简单的说明一下实现方式。
然后我说明了最简单的饿汉式,以及三种常用的懒汉式(双重校验锁、静态内部类、枚举)。
说了双重校验锁的volatile关键字和synchronized关键字,问我是否了解volatile关键字,我说了有序性和可见性,当我说到有序性的时候问我指令重排序如果优化了顺序会发生什么情况?我说这一块我有些忘记了,所以跳过了这一块说明了一下可见性是如何实现的。
volatile关键字实现了有序性和可见性,可见性这里就不再赘述了,主要是每个线程拥有一块自己的缓存,每次修改现在缓存中修改,然后更新到主存上去。
有序性指的是禁止虚拟机重排序。
重排序在单线程的时候一般没有问题,比如如下代码,指令a=1
先执行还是b=2
先执行都不影响结果。
int a = 1;
int b = 2;
int c = a+b;
但是在多线程中,如果有如下代码,虚拟机会对第一个方法进行重排序,导致a还未初始为2,就执行了flag = true
。这就会导致线程2中的res = a+a
结果变成0。
public void write(){
a = 2;
flag = true;
}
public void add(){
if(flag){
int res = a+a;
}
}
经杰哥指出,面试想问的应该是有关于对象初始化时候的指令重排序。对象初始化的时候会经过3步:1.分配内存 2.初始化变量 3.将变量指针指向内存地址。但是由于指令重排序,3可能和2同时发生,就会导致其他线程获得了一个还没有完全初始化完成的对象。
最后问了我项目的参与情况,我回答说基本是我和客户进行沟通,确认需求之后制作出一个DOME,然后再和客户确认,没问题之后交于他们部署。
说完参与情况之后问我是否有开发文档,然后我说有给客户使用的API文档和使用说明,自己内部还有一个数据库文档和全API文档。
大致就是走流程说的话,我就问了一下关于新人入职之后的培训流程。
然后他回答说会有项目和框架的集体培训,然后有新人课题,具体他也不是很了解。
听到他这么回答,我回想起了我回答Redis时候支支吾吾的样子。
最后问了一下他们现在使用的技术,回答说主要是C++、C#、Java,最近几年都是在使用Java,然后C#之类的也逐渐不太用了。
这次面试的过程中JVM的相关知识暂时还没有问到,但是每个问的问题都感觉挺有深度的,如果没有看过源码的话感觉很容易就回答不上来。
话说回来事实证明集合、数据库索引、事务是必问问题,所以在之后的学习中还是对这几个方面多下功夫。
最后总结了一下发现很多问题都没有回答到点子上,那凉了呀。