本人已参与[新人创作礼]活动,一起开启掘金创作之路。
分享一些来自大厂比较常见的面经~
主要来源:网易,阿里巴巴,百度,京东,字节跳动
Java的jvm模型(内存的划分和垃圾回收机制)
1. 程序计数器(线程私有); 记录当前线程所执行字节码行号的指示器,确保线程切换后(上下文切换)能恢复到正确的执行位置
2. 本地方法区; 执行native方法的区域
3. 方法区(线程共享); 存储了每个类的信息、静态变量、常量以及编译器编译后的代码等
4. 栈内存(线程私有); 存放一个个栈帧,每一个栈帧对应一个被调用的方法,栈帧中有局部变量表,操作数栈,引用,存储基本数据类型和引用数据类型的实例。
5. 堆内存(线程共享)。 存储对象本身和数组 Java堆分为新生代和老年代,新生代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。 新生代是minorGc,老年代是fullGc
Java线程池的四种创建方法和其中的参数
1.Executors.newFixedThreadPool(n); 2.Executors.newCachedThreadPool(); 3.Executors.newScheduledThreadPool(5); 4.Executors.newSingleThreadExecutor();
七大参数:核心线程数,最大线程数,多余线程生存时间,unit时间单元(生存时间的单位),任务队列,线程工厂,拒绝策略。
但阿里巴巴规范手册中指出,现实开发环境应避免Executors的方式创建线程池,容易在超出数值最大范围时发生OOM,应自定义线程池newThreadPoolExecutor使用。
补充:
四种拒绝策略: 1.AbortPolicy:流产策略,直接抛出异常。 2.CallerRunsPolicy:调用线程直接运行任务 3.DiscardPolicy:直接抛弃任务,不做任何处理 4.DiscardOldestPolicy:丢弃队列中最老的任务,添加新任务。
Java的lambda表达式和stream
在某处只需要一个能做一件事情的函数,连它叫什么名字都无关紧要。Lambda 表达式就可以用来做这件事。 在Java 8中,对于只有一个方法的接口可以把它视为一个函数。可使用Lambda替换。
stream 的方法里面大多都使用了 lambda 表达式,它是 Java 8 提供给我们的对于元素集合统一、快速、并行操作的一种方式,所有 Stream 的操作必须以 lambda 表达式为参数。
简述一下你对线程安全的理解
如果代码所在进程中有多个线程同时运行,其运行结果总和单线程的运行结果完全一致,且其他期望值和预期值也总能完全一致,就是线程安全的。
线程安全确保:锁机制(会展开问的这里不详细说)
callable 和 runnable的区别
1、最大的区别,runnable没有返回值,而实现callable接口的任务线程能返回执行结果(结合FutureTask) 2、callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是runnable接口实现类中run方法的异常必须在内部处理,不能抛出
聊聊你理解的死锁吧
死锁指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵直状态如果没有外力作用则它们都无法向前推进。
产生死锁的四个条件: 1.互斥:一段时间内某一资源仅由一个进程占有。 2.请求保持:当进程因请求资源而阻塞时,对已获得的资源保持不放。 3.不可剥夺:进程资源未使用完时只能自己释放,不可被剥夺。 4.循环等待:在发生死锁时,存在进程-资源的环形等待
预防死锁
- 资源一次性分配 ------破坏请求保持
- 可剥夺:当某进程新的资源没有满足时,释放已经占有的资源 ------ 破坏不可剥夺
- 资源有序分配:给每类资源编号,每个进程按编号递增的顺序请求资源,释放则相反顺序 ------破坏循环等待
避免死锁 死锁避免的策略中,允许进程动态请求资源,系统在资源分配之前会计算资源分配的安全性,若此次分配不会导致系统不安全,则讲资源分配给进程,否则进程等待。
死锁避免——银行家算法
建立资源分配表: MAX:最大可分配资源 Allocation:已分配的资源 Need:还需要的资源(MAX - Allocation) Available:当前可获得资源数(如果小于need则等待)
死锁检测
Jstack:java自带的堆栈跟踪工具,用于打印出给定的java 进程ID或core file或远程调用服务 的java 堆栈信息,该工具可以生成jvm当前时刻的线程快照,用户定位停顿原因。
JConsole:JDK自带的监控,提供一个强大的可视化界面
简述一下乐观锁和悲观锁
就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
讲解一下自旋锁和自适应自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。基于CAS实现。
JVM对于自旋周期的选择,jdk1.5这个限度是一定的写死的,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行当中,那么虚拟机认为这次也有可能再次成功,进而允许它自旋时间更久一点,比如100个循环。相反,如果对于某个锁,自旋很少成功过 ,那么会忽略自旋过程直接进入阻塞,以免造成cpu浪费。
介绍可重入锁及其底层原理
可重入指:某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
ReentrantLock 和 synchronized不一样,ReentrantLock需要手动释放锁,且手动释放和加锁次数必须一样,加减锁次数不一样会导致死锁。
底层:每一个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,当某一线程请求成功后,jvm会记录下锁的持有线程并将计数器置1,此时其他线程请求该锁需要等待,持有锁的线程再次请求他,计数器加一。
mysql innodb的四种隔离级别
1.读未提交:啥都避免不了 2.读已提交:避免脏读 3.可重复读:避免脏读,不可重复读 4.串行化:避免脏读,不可重复读,幻读
脏读,幻读,不可重复读的区别
脏读指读到其他事务未提交的修改后数据,结果事务回滚,使得读到了不存在的一个数据,这样的数据就是脏数据。
不可重复读指,某事务多次读同一数据,但另一事务在本事务多次读取过程中,对数据更新并提交,导致本事务多次读同一数据结果不一致。
幻读更侧重插入数据时的问题,他侧重的是某一次select操作得到的结果所表征的数据状态无法支撑后续的业务操作,在执行了一次select后,确认某数据不存在,准备插入数据,但执行insert后发现此纪录竟然又mmp存在了,插入报错,就像出现了幻觉,称为幻读。
hashmap的底层机制
在JDK7 : HashMap 是由 数组,链表 组成 采用头插法 按照原来的方式进行扩容后存储位置计算,扩容后插入,单独计算
在JDK8: HashMap 是由 数组,链表,红黑树 组成的 采用尾插法 按照扩容后的规律计算扩容后的存储位置,扩容前插入,转移数据时统一计算。
扩容规则:数组的大小可以在构造方法时设置,默认大小16,数组中每一个元素就是一个链表,负载因子的默认值是0.75,当添加的数组元素大于了数组长度 * 0.75,数组长度扩容为两倍。
在还没出现红黑树的条件下,添加元素使得数组中某链表长度超过了8,数组会扩容为两倍(32),假设此时链表排列不变,再添加一个元素,数组长度在扩容两倍(64),此时链表中存在是个元素(hashmap链表元素容纳的最大值)。
注意:!!!Attention Please!
此时数组长度64,链表长度达到了8,已经满足链表化树的条件,再添加元素,链表将转换成红黑树。
hashSet的底层是hashMap,在hashSet的add方法中 map.put(e,PRESENT),
private static final Object PRESENT = new Object();
说一下spring的ioc和aop
ioc是spring控制反转的容器,把我们创建对象的方式从new对象转为从容器中获取对象,交由spring管理bean的整个生命周期,把所有bean的依赖关系通过配置文件或注解关联起来,降低耦合度。
spring aop,aop指Aspect Oriented Programming,面向切面编程,我们的软件开发是一个纵深的过程,而切面指交叉业务逻辑,在某一个方法前后切开编织进去我想编织的公共逻辑,通常编织的业务是日志记录,性能统计,安全控制,事务处理,异常处理等等。
aop在java中主要由动态代理实现,借助动态代理对一个类或是一个方法进行增强,有两种实现方式:jdk动态代理和cglib动态代理。
对于jdk动态代理:如果目标类实现了接口,spring aop会选择jdk动态代理,其核心是invocationHandler和proxy,每个代理类的调用处理程序都必须实现InvocationHandler接口 ,jdk动态代理针对的对象一定是要有顶层实现接口的。(手写原生必须要有的:实现了invocationHandler接口的代理类,可在method.invoke()前后添加代码,主方法中把被代理类传进上述类的构造方法即可,再通过反射技术拿到loader和interfaces,最终调用核心方法: Subject subject = (Subject) Proxy.newProxyInstance(loader, interfaces, handler); )
对于cglib动态代理:如果目标类没有实现接口,spring aop会调用cglib动态代理,cglib可以在运行时动态生成类的字节码,动态创建目标类的子类对象,在子类对象中增强目标类。
bean的生命周期
1.实例化bean,当请求一个未被初始化的bean时,beanFactory调用createBean初始化,ApplicationContext在容器启动结束后,获取beanDefinition对象中的信息,实例化所有的bean。
2.依赖注入,实例化后的对象会被封装到beanWrapper中,Spring根据beanDefinition中的信息和beanWrapper中提供的设置属性的接口完成依赖注入。
3.Aware接口的处理:Spring检测对象是否实现了xxxAware接口
如果实现了beanNameAware接口,调用setBeanName方法,传递beanId 如果实现了beanFactoryAware接口,调用setBeanFactory,传递Spring 工厂自身 如果实现了ApplicationContextAware接口,调用setApplicationContext,传递Spring上下文
4.调用beanPostProcesser预初始化:调用postProcessBeforeInitialization让用户自定义处理bean
5.InitializingBean:如果Bean在Spring配置文件中配置了init-method属性,就会自动调用预先配置的初始化方法。至此,Bean完成了创建的所有步骤。
6.调用disposableBean的destroy()方法
手写个单例
饿汉式:构造器私有化,且直接static修饰,上升为类变量,在类初始化时已经实例化,有且只有一个,线程安全但消耗过多内存
public class Singleton1 {
private Singleton1() {}
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
return single;
}
}
懒汉式:构造器私有化,但考虑到多线程要使用双重检查锁,并使用volatile关键字保证对象实例化过程中的顺序性,防止jvm重排。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {
};
public static Singleton getInstance() {
if (instance == null) {
synchronized (instance) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
说一下你了解的spring自动装配以及其中@autowired和@resource的区别
@Resource和@Autowired都可以作为注入属性的修饰,在接口仅有单一实现类时,两个注解的修饰效果相同,可以互相替换,不影响使用。
@Autowired是 spring的注解,通过type进行注入,如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier
@Resource是java自己的注解,Spring将@Resource注解的name属性解析成bean的名字,type是bean的类型,所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
springMVC的工作流程
1.用户发送请求到前端控制器DispatcherServlet 2.DispatcherServlet收到请求后调用HandlerMapping处理器映射器请求获取Handle 3.处理器映射器根据请求url找到具体的处理器,生成处理器对象以及处理器拦截器一并返回给DispatcherServlet 4.DispatcherServlet调用HandlerAdapter适配器。 5.适配器去调用具体的Handler 6.Handler执行完成后返回ModelAndView 7.HandlerAdapter将handler的执行结果ModelAndView返回给DispatcherServlet 8.DispatcherServlet把ModelAndView传给ViewResolver视图解析器进行解析 9.解析后返回具体的view 10.DispatcherServlet对view进行渲染 11.DispatcherServlet响应用户
简述一下回表查询
在聚簇索引里,叶子节点直接存储行数据,因此只需要查询一遍,因此聚簇索引查询非常快,可以直接定位行记录。
在非聚簇索引里,叶子节点不存储行记录头指针,而是存储主键值,因此普通索引查询需要先定位到主键值(第一次查询),再通过聚簇索引定位到行记录,因此需要查两遍,所以叫回表查询。
索引是什么?
类似于书本的目录,是存储引擎用于提高数据库访问速度的一种数据结构,数据是存储在磁盘的,查询时如果没有索引则会加载所有的数据到内存,引起大量的磁盘读取。
讲一下最左前匹配原则,并聊聊你理解的覆盖索引
如果语句中用到了组合索引中最左边的索引,这条语句就可以利用它进行匹配,但遇到范围查询和索引计算等就会导致后面的字段都用不到索引,例如对(a,b,c)建立索引
ALTER TABLE mytable ADD INDEX name_city_age (a(10),b,c);
相当于建立了三组:a,ab,abc
如果我仅仅使用bc或者使用ac这种没有紧贴着左侧传递的索引就会导致后面的索引失效。
覆盖索引指如果select的数据仅仅通过索引就可以取得,不需要回表二次查询,即查询列要被所使用的索引覆盖
简述前缀索引
指对字符串的前几个字符建立索引,检索长度短,检索效率高。要求足够长的前缀以保证较高的索引选择性,高选择性意味着查找的时候就过滤掉更多的数据行。
索引的设计原则
选择区分度高的字段,大家常常吐槽的性别这种就是区分度很低的没有价值的索引,效果很差。设计和查询时要满足最左前匹配原则。且索引不是越多越好,索引是占着额外空间的,并且需要维护。
什么情况会导致索引失效
对于组合索引:不满足最左前匹配,后面的字段会失效。 %开头无法使用,判断索引列是否不等于某值时或是对索引列进行运算
mybatis和hibernate的区别
相同点:hibernate和mybatis都可以通过SessionFactoryBuilder生成SessionFactory,然后由SessionFactory生成session,最后由Session来开启执行事务和SQL语句。
不同点:hibernate全自动,mybatis半自动,hibernate拥有完整的映射模型自动生成sql,mybatis只有基本的字段映射,对象数据以及关系仍然需要手动写sql。
hibernate拥有完整的日志系统,mybatis有所欠缺。 mybatis比hibernate需要关心更多细节,但学习成本低 sql优化方面,mybatis更容易 hibernate缓存更好
TCP三次握手和四次挥手
三次握手: 1.客户端向服务端发送连接请求,SYN置1,seq = x,随后处于SYN - SENT状态。 2.服务端收到请求后,返回给客户端ack,且seq = y, ack = x + 1,随后处于SYN - RCVD。 3.客户端收到ack后,返回ack,seq = x + 1, ack = y + 1,随后连接完全建立,ESTABLISH
四次挥手 1.客户端向服务端发送关闭请求,FIN置1,随后处于FIN-WAIT-1状态。 2.服务端收到客户端请求,返回一个ack,随后处于COLSE-WAIT状态。 3.客户端收到服务端ack进入FIN-WAIT-2。
若服务端没有要向服务端发送的数据,通知客户端
4.服务端向客户端发送请求,进入LAST-ACK。 5.客户端收到请求后发送最后一个ack,进入TIME-WAIT,等待2msl,closed 6.服务端收到ack,closed
等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。
介绍你项目中常用的注解
@Controller:声明控制器。 @RestController:不走视图解析器,返回一个字符串。 @Resource,@Autowired @Service:在ServiceImpl层上注明 @Mapper:mapper层注解 @RequestBody主要用来接收前端传递给后端的json字符串中的数据;GET方式无请求体,所以使用@RequestBody接收数据时,前端不能使用GET方式提交数据,而是用POST方式进行提交。在后端的同一个接收方法里,@RequestBody与@RequestParam()可以同时使用,@RequestBody最多只能有一个,而@RequestParam()可以有多个。 @ResponseBody:作用于方法上,可以将整个返回结果以某种格式返回,如json或xml格式。
介绍你项目中常遇到的异常
最多的一个:空指针异常:java.lang.NullPointerException 运行时某一值为空,再用其调用函数出现问题。
java.lang.ClassNotFoundException:类名称或路径错误或资源无法导出
concurrentModifyException:并发不安全集合类
解决hash冲突的四种方案
1.开放定址法:遇到hash冲突,去寻找一个新的空闲位置 ① 线性探测:+1取mod..+1取mod....一直到找到新的 ② 平方探测:+1平方取mod一直找到新的 (堆积或聚集问题:本来人家俩hash不相同,却又再抢一个位置)
2.再hash:同时构造多个不同的hash函数,hash冲突了就用其他的。(容易增加计算时间)
3.链地址法:hash值相同的记录在同一链表中,连到相同值的后面。(不容易序列化)
4.建立公共溢出区:发生冲突的都放在溢出表
什么是虚拟内存
虚拟内存指的是让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存,虚拟内存使用了部分加载的技术,仅让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样使得内存看起来好像变大了,但这部分内存其实包含了磁盘或硬盘,因此叫虚拟内存
final,finally,finalize
final声明属性:属性不可变 final声明方法:方法不可覆盖 final声明类:类不可继承
finally:异常处理语句总是会执行的一部分
finalize:垃圾回收时会调用被回收对象的此方法,可以覆盖此方法,对象的该方法被调用即宣告其死亡。