面试纪要一
1. 主线程开辟子线程后,如何等待子线程执行完再执行呢?
-
sleep
用sleep方法,让主线程睡眠一段时间,当然这个睡眠时间是主观的时间,是我们自己定的,这个方法不推荐,但是在这里还是写一下,毕竟是解决方法
使用Thread的join()等待所有的子线程执行完毕,主线程在执行,thread.join()把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
下面结合这个问题我介绍一些并发包里非常有用的并发工具类
在这里说明一点,countDownLatch不可能重新初始化或者修改CountDownLatch对象内部计数器的值,一个线程调用countdown方法happen-before另外一个线程调用await方法
- 使用join()方法,子线程调用join()方法,当子线程执行完后才会继续往后执行
- 通过isAlive()方法判断,线程是否存活,保证子线程执行完再执行主线程
- 通过while(Thread.activeCount()>1)
- 使用同步类CountDownLatch
- 使用同步屏障CyclicBarrier
- 用sleep方法,让主线程睡眠一段时间,当然这个睡眠时间是主观的时间,是我们自己定的,这个方法不推荐,但是在这里还是写一下,毕竟是解决方法
2. 一个List集合包含一个对象,如何根据对象的名字去重呢
下文中四种方法对List中的String类型以集合元素对象为单位整体去重。如果你的List放入的是Object对象,需要你去实现对象的equals和hashCode方法,去重的代码实现方法和List<String>
去重是一样的。
为什么要重写equals和hashCode方法呢,如果比较的是String、Integer类型则不需要重写,因为它们已经重写了equals和hashCode方法,比较的是内容是否相等。如果集合中放的是Object对象,由于未重写equals方法比较的是对象的引用地址是否相同,如果要根据对象的某些字段去重就无法实现,所以需要重写equals和hashCode方法。
下面是重写equals和hashCode方法例子:
package MaEquals.RewriteEquals;
import java.util.Objects;
public class Animals {
private String name;
private String skill;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSkill() {
return skill;
}
public void setSkill(String skill) {
this.skill = skill;
}
/**
* 重写对象的equals方法,判等
*
* @param objk
* @return
*/
@Override
public boolean equals(Object objk) {
// 判断引用地址是否相等
if (this == objk) {
return true;
}
// 判断类型是否相同
if (objk.getClass() != Animals.class) {
return false;
}
// 判断引用不同的情况,属性是否相同
if (objk != null) {
// 类型相同看属性是否相同
Animals animal = (Animals) objk;
if (Objects.equals(animal.getName(), this.name) && Objects.equals(animal.getSkill(), this.skill)) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(name, skill);
}
}
第一种方法
是大家最容易想到的,先把List数据放入Set,因为Set数据结构本身具有去重的功能,所以再将SET转为List之后就是去重之后的结果。这种方法在去重之后会改变原有的List元素顺序,因为HashSet本身是无序的,而TreeSet排序也不是List种元素的原有顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
第二种方法
使用就比较简单,先用stream方法将集合转换成流,然后distinct去重,最后在将Stream流collect收集为List。
1 2 3 4 5 6 |
|
第三种方法
这种方法利用了set.add(T)
,如果T元素已经存在集合中,就返回false。利用这个方法进行是否重复的数据判断,如果不重复就放入一个新的newList中,这个newList就是最终的去重结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
控制台打印结果和第二种方法一致。
第四种方法
这种方法已经脱离了使用Set集合进行去重的思维,而是使用newList.contains(T)
方法,在向新的List添加数据的时候判断这个数据是否已经存在,如果存在就不添加,从而达到去重的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
3. SpringBoot中 @value 注解如何实现的
4. mybatis中# 和 $ 符号的区别
一个 #{ } 被解析为一个参数占位符 ? ,而${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换。
一般如果可以使用#就不要使用$符号。
1)使用#入参,myBatis会生成PrepareStatement并且可以安全地设置参数(=?)的值。因为sql语句已经预编译好了,传入参数的时候,不会重新生产sql语句。安全性高。
2)用$可以会有sql注入的问题:
例如,select * from emp where ename = '用户名',如果使用$入参,用户名被传入例如‘smith or 1 = 1’,那无论ename是否匹配都能查到结果。
3)在特定场景下,例如如果在使用诸如order by '{param}',这时候就可以使用$.
5. 缓存击穿等三种常见情况的解决方案
key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。解决方案:
- 使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
- "永远不过期":
这里的“永远不过期”包含两层意思:
(1) 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
6. 对于redis 单线程模型的理解
首先redis是单线程的,以下是对redis单线程模型的理解:
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
消息处理流程
-
文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
-
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都推到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字:当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
IO多路复用:I/O是指网络I/O,多路指多个TCP连接(即socket或者channel),复用指复用一个或几个线程。意思说一个或一组线程处理多个TCP连接。最大优势是减少系统开销小,不必创建过多的进程/线程,也不必维护这些进程/线程。
I/O 多路复用程序的实现
Redis的I/O多路复用程序的所有功能是通过包装select、epoll、evport和kqueue这些I/O多路复用函数库来实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.c等。
因为Redis为每个I/O多路复用函数库都实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的,如下图所示。
7. mysql 500万的单表,如何优化
8. (mysql)主键索引和普通索引的区别
1). 什么是最左前缀原则?
以下回答全部是基于MySQL的InnoDB引擎
例如对于下面这一张表
如果我们按照 name 字段来建立索引的话,采用B+树的结构,大概的索引结构如下
如果我们要进行模糊查找,查找name 以“张"开头的所有人的ID,即 sql 语句为
1select ID from table where name like '张%'
由于在B+树结构的索引中,索引项是按照索引定义里面出现的字段顺序排序的,索引在查找的时候,可以快速定位到 ID 为 100的张一,然后直接向右遍历所有张开头的人,直到条件不满足为止。
也就是说,我们找到第一个满足条件的人之后,直接向右遍历就可以了,由于索引是有序的,所有满足条件的人都会聚集在一起。
而这种定位到最左边,然后向右遍历寻找,就是我们所说的最左前缀原则。
2). 为什么用 B+ 树做索引而不用哈希表做索引?
1、哈希表是把索引字段映射成对应的哈希码然后再存放在对应的位置,这样的话,如果我们要进行模糊查找的话,显然哈希表这种结构是不支持的,只能遍历这个表。而B+树则可以通过最左前缀原则快速找到对应的数据。
2、如果我们要进行范围查找,例如查找ID为100 ~ 400的人,哈希表同样不支持,只能遍历全表。
3、索引字段通过哈希映射成哈希码,如果很多字段都刚好映射到相同值的哈希码的话,那么形成的索引结构将会是一条很长的链表,这样的话,查找的时间就会大大增加。
3). 主键索引和非主键索引有什么区别?
例如对于下面这个表(其实就是上面的表中增加了一个k字段),且ID是主键。
主键索引和非主键索引的示意图如下:
其中R代表一整行的值。
从图中不难看出,主键索引和非主键索引的区别是:非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行数据,其中非主键索引也被称为二级索引,而主键索引也被称为聚簇索引。
根据这两种结构我们来进行下查询,看看他们在查询上有什么区别。
1、如果查询语句是 select * from table where ID = 100,即主键查询的方式,则只需要搜索 ID 这棵 B+树。
2、如果查询语句是 select * from table where k = 1,即非主键的查询方式,则先搜索k索引树,得到ID=100,再到ID索引树搜索一次,这个过程也被称为回表。
现在,知道他们的区别了吧?
4). 为什么建议使用主键自增的索引?
对于这颗主键索引的树
如果我们插入 ID = 650 的一行数据,那么直接在最右边插入就可以了
但是如果插入的是 ID = 350 的一行数据,由于 B+ 树是有序的,那么需要将下面的叶子节点进行移动,腾出位置来插入 ID = 350 的数据,这样就会比较消耗时间,如果刚好 R4 所在的数据页已经满了,需要进行页分裂操作,这样会更加糟糕。
但是,如果我们的主键是自增的,每次插入的 ID 都会比前面的大,那么我们每次只需要在后面插入就行, 不需要移动位置、分裂等操作,这样可以提高性能。也就是为什么建议使用主键自增的索引。
10. 公司微服务的架构体系
11. 项目中Controller层如果不处理异常,有几种方法统一处理异常
(1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver;
(2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器;
(3)使用@ControllerAdvice、@ExceptionHandler注解实现异常处理;
(4)使用AOP,切面编程
@ControllerAdvice
public class ExceptionConfigController {
@ExceptionHandler
public ModelAndView exceptionHandler(Exception e){
ModelAndView mv = new ModelAndView(new MappingJackson2JsonView());
mv.addObject("success",false);
mv.addObject("mesg","请求发生了异常,请稍后再试");
return mv;
}
}
我们在如上的代码中,类上加了@ControllerAdvice
注解,表示它是一个增强版的controller,然后在里面创建了一个返回ModelAndView对象的exceptionHandler方法,其上加上@ExceptionHandler
注解,表示这是一个异常处理方法,然后在方法里面写上具体的异常处理及返回参数逻辑即可,如此就能捕捉所有controller的异常
12. AOP的切点,切面概念等
1. 通知: 就是我们编写的希望Aop时执行的那个方法。我们通过Aop希望我们编写的方法在目标方法执行前执行,或者执行后执行。
2. 切点:切点就是我们配置的满足我们条件的目标方法。比如我们规定:名字前面是select开头的才执行我们自定义的通知方法。那么这些select开头的方法就是切点。
3. 连接点:连接点可以说是切点的全集。切点是连接点的子集。也可以理解为,连接点是我们没有定义那个select开头规则时,满足条件的全部的方法。
4. 切面:切面是切点和通知的组合称谓,就是变相给组合起了个名字。
13. Spring 体系中拦截器,过滤器的区别,是否能够实现上述统一异常处理
①拦截器是基于Java的反射机制的,而过滤器是基于函数回调。
②拦截器不依赖与servlet容器,依赖于web框架,在SpringMVC中就是依赖于SpringMVC框架。过滤器依赖与servlet容器。
③拦截器只能对action(也就是controller)请求起作用,而过滤器则可以对几乎所有的请求起作用,并且可以对请求的资源进行起作用,但是缺点是一个过滤器实例只能在容器初始化时调用一次。
④拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
⑤在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
⑥拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑
使用过滤器处理异常,实现IExceptionfilter接口,重写OnExcption,然后记录异常
4. ES的用途,ES为什么搜索那么快,处理时和MySQL的区别?
ElasticSearch是一个分布式,高性能、高可用、可伸缩的搜索和分析系统
数据库的索引是B+tree结构
主键是聚合索引 其他索引是非聚合索引,先从非聚合索引找,见下图
elasticsearch倒排索引原理
两者对比
对于倒排索引,要分两种情况:
1、基于分词后的全文检索
这种情况是es的强项,而对于mysql关系型数据库而言完全是灾难
因为es分词后,每个字都可以利用FST高速找到倒排索引的位置,并迅速获取文档id列表
但是对于mysql检索中间的词只能全表扫(如果不是搜头几个字符)
2、精确检索
这种情况我想两种相差不大,有些情况下mysql的可能会更快些
如果mysql的非聚合索引用上了覆盖索引,无需回表,则速度可能更快
es还是通过FST找到倒排索引的位置并获取文档id列表,再根据文档id获取文档并根据相关度算分进行排序,但es还有个杀手锏,即天然的分布式使得在大数据量面前可以通过分片降低每个分片的检索规模,并且可以并行检索提升效率
用filter时更是可以直接跳过检索直接走缓存
5. 项目中Spring 事务处理机制
Spring 的声明式事务管理是建立在 Spring AOP 机制之上的,其本质是对目标方法前后进行拦截,并在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中作相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。总的来说,声明式事务得益于 Spring IoC容器 和 Spring AOP 机制的支持:IoC容器为声明式事务管理提供了基础设施,使得 Bean 对于 Spring 框架而言是可管理的;而由于事务管理本身就是一个典型的横切逻辑(正是 AOP 的用武之地),因此 Spring AOP 机制是声明式事务管理的直接实现者。
显然,声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要在XML文件中配置或者添加注解就可以获得完全的事务支持。因此,通常情况下,笔者强烈建议在开发中使用声明式事务,不仅因为其简单,更主要是因为这样使得纯业务代码不被污染,极大方便后期的代码维护。
基于 @Transactional 的声明式事务管理
除了基于命名空间的事务配置方式,Spring 还引入了基于 Annotation 的方式,具体主要涉及@Transactional 标注。@Transactional 可以作用于接口、接口方法、类以及类方法上:当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性
6. 两个List里取交集,A集合包含7个相同对象,B集合包含6个相同对象,取数量少的
import static java.util.stream.Collectors.toList;
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list1 = new ArrayList();
list1.add("1111");
list1.add("2222");
list1.add("3333");
List<String> list2 = new ArrayList();
list2.add("3333");
list2.add("4444");
list2.add("5555");
// 交集
List<String> intersection = list1.stream().filter(item -> list2.contains(item)).collect(toList());
System.out.println("---得到交集 intersection---");
intersection.parallelStream().forEach(System.out :: println);
// 差集 (list1 - list2)
List<String> reduce1 = list1.stream().filter(item -> !list2.contains(item)).collect(toList());
System.out.println("---得到差集 reduce1 (list1 - list2)---");
reduce1.parallelStream().forEach(System.out :: println);
// 差集 (list2 - list1)
List<String> reduce2 = list2.stream().filter(item -> !list1.contains(item)).collect(toList());
System.out.println("---得到差集 reduce2 (list2 - list1)---");
reduce2.parallelStream().forEach(System.out :: println);
// 并集
List<String> listAll = list1.parallelStream().collect(toList());
List<String> listAll2 = list2.parallelStream().collect(toList());
listAll.addAll(listAll2);
System.out.println("---得到并集 listAll---");
listAll.parallelStream().forEach(System.out :: println);
// 去重并集
List<String> listAllDistinct = listAll.stream().distinct().collect(toList());
System.out.println("---得到去重并集 listAllDistinct---");
listAllDistinct.parallelStream().forEach(System.out :: println);
System.out.println("---原来的List1---");
list1.parallelStream().forEach(System.out :: println);
System.out.println("---原来的List2---");
list2.parallelStream().forEach(System.out :: println);
// 一般有filter 操作时,不用并行流parallelStream ,如果用的话可能会导致线程安全问题
}
}
7. equals 和 hashcode关联
-
hashcode相等,equals不一定相等
-
equals相等,hashcode一定相等
-
当对象用到如HashSet、TreeSet等中时需要重写equals 和 hashcode方法,首先判断hashcode是否相等,如果相等再用equals进行判断
8. controller 上传文件
springboot可以直接使用MultipartFile上传,它有自己的Multipart解析器
使用spring则需要配置Multipart解析器
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- one of the properties available; the maximum file size in bytes -->
<property name="maxUploadSize" value="100000"/>
</bean>
按照Multipart规定的格式将文件名和文件内容在请求中发送到服务器中,服务器从请求流中得到文件名和文件内容放入到Multipart定义的类中
9. SQL的一些简单场景题,如ABC关联表,如何查出A中对应的B的C表数据,如何查出A中对应的B的C表最新的数据
10. 起一个线程,如何拿到其执行结果
使用Callable和Future接口创建线程
具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。使用Future的get()方法获得返回值,注意其是阻塞函数。
11. 为什么用到了MySQL,MongoDB,ES这么多库
12. 分布式锁的使用
分布式锁
分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。 举个不太恰当的例子:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。然后许多人要去看书,可以,排队,第一个人拿着钥匙把门打开进屋看书并且把门锁上,然后第二个人没有钥匙,那就等着,等第一个出来,然后你在拿着钥匙进去,然后就是以此类推
实现原理
-
互斥性
-
保证同一时间只有一个客户端可以拿到锁,也就是可以对共享资源进行操作
-
-
安全性
-
只有加锁的服务才能有解锁权限,也就是不能让a加的锁,bcd都可以解锁,如果都能解锁那分布式锁就没啥意义了
-
可能出现的情况就是a去查询发现持有锁,就在准备解锁,这时候忽然a持有的锁过期了,然后b去获得锁,因为a锁过期,b拿到锁,这时候a继续执行第二步进行解锁如果不加校验,就将b持有的锁就给删除了
-
-
避免死锁
-
出现死锁就会导致后续的任何服务都拿不到锁,不能再对共享资源进行任何操作了
-
-
保证加锁与解锁操作是原子性操作
- 这个其实属于是实现分布式锁的问题,假设a用redis实现分布式锁
-
假设加锁操作,操作步骤分为两步:
-
1,设置key set(key,value)2,给key设置过期时间
-
假设现在a刚实现set后,程序崩了就导致了没给key设置过期时间就导致key一直存在就发生了死锁
如何实现分布式锁
实现分布式锁的方式有很多,只要满足上述条件的都可以实现分布式锁,比如数据库,redis,zookeeper,在这里就先讲一下如何使用redis实现分布式锁
使用redis实现分布式锁
-
使用redis命令 set key value NX EX max-lock-time 实现加锁
-
使用redis命令 EVAL 实现解锁
15. 怎么用Redis实现异步队列
redis队列模式(两种命令方式,均满足 先进先出 的队列模式)
①、lpush rpop/lpop:非阻塞式
②、lpush brpop/blpop :阻塞式
答:一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有
消息的时候,要适当 sleep 一会再重试。
如果对方追问可不可以不用 sleep 呢?
list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。如果对
方追问能不能生产一次消费多次呢?使用 pub/sub 主题订阅者模式,可以实现
1:N 的消息队列。
如果对方追问 pub/sub 有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ
等。
如果对方追问 redis 如何实现延时队列?
使用 sortedset,总体的思路很简单,就是每一个value
的score
保存的是时间,也就是说,在添加一个元素zadd时他的score
是当前时间+延时的时间。轮循获取数据时,查找小于或等于当前时间的数据项,就是具体的延时消息。