文章目录
简历注意事项
简历的结构
基本信息\教育背景\求职意向\工作经历*职业技能 **项目经历*\个人优势荣誉
职业技能
- 放到简历的黄金位置
- 职业技能 = 必要技术+第三方技术
- 针对性准备,引导面试官针对性提问
- 写在简历上的必须能聊,不然就别写
项目描述
- 项目体现业务深度或技术深度
- 主导设计过XXX模块(0-1,研发或1-2,改进),不要说太大,逮着模块说
- 尽可能展示指标数据
如何找到适合的练手项目
gitee或github搜索开源项目,b站黑马项目课程
深入学习项目
- 技术选型,通用模块,可嵌套到大部分项目中
- 学习方式,多方位深入挖掘业务和技术
- 功能实现->常见问题->系统设计
笔试面试可能就是发过来一个牛客网的链接
不仅要投校招,也要投社招
游戏策划
还要重点学的东西,MySQL,JUC(多线程),JVM,jdbc,rabbitMQ,kafka,直接刷题也行
数据经常问,hashmap,数组什么的
黑马的java虚拟机,多线程,mysql,redis,八股文,javaguide
redis黑马的课程只看实战篇就可以了
csdn上两个最火的java八股文要看
简历二月开始就要投了,找实习
https://heuqqdmbyk.feishu.cn/wiki/RymLwLLWfieibHkjf17cKhY4nlf(问答文稿地址)
Redis篇
缓存穿透
方案一,缓存空数据
方案二,布隆过滤器
检索一个元素是否在一个集合中
一个元素对应多个hash也是为了减小hash数组的大小
bitmap,只存0或1
SQL篇
如何定位慢查询
- 开源工具,调试工具Arthas,运维工具,prometheus\Skywalking
- mysql的配置文件中配置慢查询日志
语句执行很慢,如何分析
框架篇
spring中的单例bean是线程安全的吗
不是线程安全的
spring中的bean默认都是单例的
因为在spring中的bean中注入的一般都是无状态(不能被修改)的对象,没有线程安全问题
如果在bean中定义了可修改的成员变量,要考虑线程安全问题,可以使用多例或者加锁来解决
AOP相关
什么是AOP
面向切面编程,用于将哪些与业务无关,但对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合
你的项目有没有用到AOP(哪里可以用到AOP)
- 记录操作日志
- 缓存
- spring事务
spring中的事务如何实现
通过AOP功能,对方法前后进行拦截,在执行方法签开启事务,在执行方法后根据执行情况提交或回滚事务
spring事务失效的场景
- 异常捕获处理,自己处理了异常,没有抛出->手动抛出
- 抛出检查异常(编译时异常),配置rollbackFor属性为Exception
- 非public方法导致的事务失效,改为public
spring的bean的生命周期
构造函数->依赖注入->Aware接口->BeanPostProcessor#before->初始化方法->BeanPostProcessor#after->销毁bean
bean的循环依赖
什么是循环依赖
循环依赖,两个bean互相引用,形成闭环
如何解决
用三级缓存解决问题
- 一级缓存,单例池
- 二级缓存,缓存早期的bean对象(没有走完生命周期的bean对象)
- 三级缓存,缓存的是ObjectFactory,表示对象工厂,用来创建对象
构造方法出现循环依赖怎么办
用@Lazy进行懒加载,什么时候需要对象再进行bean对象的创建
SpringMVC的执行流程
SpringMVC中重要的组件
- DispatcherServlet(前端控制器)
- HandlerMapping(处理器映射器)
- HandleAdaptor(处理器适配器)
- ViewResolver(视图解析器)
SpringMVC的执行流程知道吗
视图版本(JSP)的前后端不分离的流程比较复杂
这里介绍前后端开发,接口开发版本
- 用户发送出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
- DispatcherServlet调用HandleAdapter(处理器适配器)
- HandleAdaptor经过适配调用具体的处理器(Handler/Controller)
- 方法上添加了@ResponseBody (一般包含在@restController中)
- 通过HttpMessageConverter来返回结果转换为JSON并响应
Springboot自动配置原理
SpringBoot项目中的引导类上有一个注解@SpringBootApplication,此注解对三个注解进行了封装:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
@EnableAutoConfiguration是实现自动化配置的核心注解,该注解通过@Import注解导入对应的配置选择器
内部是读取了该项目和该项目引用的Jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名
在这些配置类中所定义Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中
条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用
Spring常见的注解
spring的常见注解有哪些
用于依赖注入,bean放入IOC容器中,IOC,DI
- @Component@Controller@Service@Respository,实例化bean
- @Autowired
- @Qualifier,结合@Autowired用于根据名称进行依赖注入
- @Scope,标注bean的作用范围
- @Configuration,指定当前类是一个spring配置类,当创建容器时会从该类上加载注解
- @ComponentScan,用于指定Spring在初始化容器时要扫描的包
- @Bean,用在方法上,将返回值放入容器
- @import,用于将其他配置类或组件类导入到当前的 Spring 配置类 中,通常是手动引入 Bean 的一种方式。
- @Aspect@Before@After@Around@Pointcut,用于AOP切面编程
springMVC常见的注解有哪些
用于请求响应 - @RequestMapping,映射请求路径
- @RequestParam,指定请求参数的名称
- @PathVariable,从请求路径中获取参数
- @RequestBody,将请求json转为Java对象
- @ResponesBody,将Controller返回值转化为json对象响应
- @RestController,@Controller+@ResponesBody
- RequestHeader,获取指定的请求头数据
Springboot常见注解有哪些 - @SpringBootConfiguration,组合了@Configuration注解,实现配置文件的功能
- @EnableAutoConfiguration,自动配置
- @ComponentScan,spring组件扫描,扫描那些带@Component及延伸注解的类
mybatis的执行流程
配置文件->构建会话工厂->创建会话->Executor执行器->MappedStatement对象->数据库
输入参数->MappedStatement对象->输出结果
mybatis的延迟加载
嵌套查询的时候可能用到,但一般不会用嵌套查询
mybatis是否支持延迟加载
mapper映射文件中启用延迟加载lazyloadingEnabled=true|false
延迟加载的底层原理知道吗
用CGLIB创建目标对象的代理对象,用反射invoke方法,检查目标方法是null值,执行sql查询
一级\二级缓存
mybatis的一级二级缓存用过吗
mybatis的二级缓存什么时候会清理缓存中的数据
集合篇
面试中常被问集合,ArrayList,LinkedList,HashMap,ConcurrentHashMap
ArrayList
介绍一下什么是数组array
用连续的内存空间存储相同数据类型 数据的线性数据结构
数组如何获取其他元素的地址值
寻址公式:baseAddress+idataTypeSize
为什么数组索引从0开始:,假如从1开始不行吗
从0开始:baseAddress+i dataTypeSize
从1开始:baseAddress+(i-1)*dataTypeSize
从1开始多了一次减法指令运算,从0开始计算效率较高
查找的时间复杂度
通过下标,o(1)
未知下标,o(n)
已排序,二分查找,o(logn)
插入和删除时间复杂度
插入和删除,为了保证数组的内存连续性,连续挪动,时间复杂度都是o(n)
源码分析
源码基于JDK8
底层是依据数组实现的
这里建议去看C++的理论分析,源码分析还是不好理解,主要内容就是初始构造,添加和扩容的关系
空集合没有容量
容量和集合长度(大小)不是一个概念
ArraysList底层的实现原理是什么
- ArrayList底层是用动态的数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- 在进行扩容时是原来容量的1.5倍,每次扩容都需要拷贝数组
- 在添加数据时
ArrayList的size加1后如果大于当前的数组长度(length),则调用grow方法扩容(1.5倍)
确保新增的数据有地方存储后,将新元素添加到位于size的位置上
ArrayList的size加1后如果小于等于当前的数组长度(length),则不需要扩容
这里容量就是数组长度(length),ArrayList的size是位于数组的size索引位置
ArrayList list=new ArrayList(10)中的list扩容几次
只是声明和实例了一个ArrayList,指定了数组的长度为10,集合的容量和10,未扩容
如何实现数组和list之间的转换
数组转list,使用JDK中java.util.Arrays工具类的asList方法
List转数组,使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
用Arrays.asList转List后,如果修改了数组内容,list受影响吗
list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,只是引用了原数组,指向的都是同一个内存地址
list用toArray转数组后,如果修改了List内容,数组受影响吗
数组不会受影响,调用了toArray以后,在底层进行了数组的拷贝,跟原来的元素没关系了,
LinkedList
单向链表和双向链表的区别是什么
单向链表只有一个方向,节点只有一个后继指针next
双向链表支持两个方向,每个节点不止有一个后继指针next,还有前驱指针prev指向前面
双向链表和单向链表都有frist和last分别指向头和尾
链表操作数据的时间复杂度是多少
无论是单向链表还是双向链表,增删的平均时间复杂度都是O(n),主要是要有查询的过程
ArrayList和LinkedList的区别是什么
四个层面,结构,时间效率,空间效率,安全
- 底层数据结构
ArrayList是动态数组的数据结构实现
LinkedList是双向链表的数据结构实现 - 操作数据效率(增删改查)
ArrayList按照下标查询的时间复杂度O(1),linkedList不支持下标查询
查找:ArrayList需要遍历,链表也需要链表时间复杂度都是O(n)
新增和删除,ArrayList的平均时间复杂度是O(n)(准确来说查要O(n),删增也要O(n)),尾部是O(1)
LinkedList平均是O(n),头尾增删为O(1) - 内存空间占用
ArrayList底层是数组,内存连续,节省内存
LinkedList是双向链表需要存储数据和两个指针,更占内存 - 线程安全
ArrayList和LinkedList都不是线程安全的(比如,两个线程同时 add() 时,一个线程的修改可能覆盖另一个线程的修改)
如果要保证线程安全,两种方案:
在方法内使用,局部变量则是线程安全的
使用线程安全的ArrayList和LinkedList
List objects = Collections.synchronizedList(new ArrayList<>());
List objects1 = Collections.synchronizedList(new LinkedList<>());
HashMap
数据结构,数组+链表+红黑树,阿伟前面讲的不是这样的结构呀???
红黑树
红黑树的复杂度
二叉树->二叉搜索树(相比二叉树就是插入的时候排好了,找的时候方便)->红黑树(自平衡的二叉搜索树(BST))->B+树
遍历都可以使用前\中\后序遍历和层次遍历,但只有中序遍历是按排序遍历的
红黑树的规则都是为了让二叉树保证平衡,不形成极端的二叉树
查找,O(logn)
添加和删除,都要先查找(O(logn)),再旋转(O(1)),所以复杂度就是O(logn)
散列表HashTable
散列表又名哈希表
怎么解决哈希冲突
拉链法,数组的每个下标位置称为桶(bucket)或槽(slot),每个槽对应一条链表
hash冲突后的元素放到相同槽对应的链表或红黑树中(更好用的是红黑树,一是降低时间复杂度,二是防止恶意的Dos攻击,大量的访问攻击,将链表的时间复杂度O(n)降为O(logn))
HashMap的实现原理
HashMap的实现原理是什么
底层使用hash表数据结构,即数组+链表或红黑树
添加数据时,计算key的值确定元素在数组中的下标
- key相同则替换
- 不同则存入链表或红黑树中
获取数据通过key的hash计算数组下标获取元素
HashMap的jdk1.7和jdk1.8有什么区别
JDK1.8之前采用的拉链法,数组+链表
JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转化为红黑树
HashMap的put方法的具体流程
HashMap的put方法的具体流程
- 判断键值对数组table是否为空或null,是的话,执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i]==null,条件成立,直接新建节点添加
- 如果table[i]==null,不成立
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i]是否为treeNode(红黑树),即table[i]是否是红黑树,则遍历,有相同key覆盖,无则插入
- 如果是链表,则遍历,有相同key覆盖,无则插入,判断链表长度是否大于8,大于8把链表转换为红黑树,在红黑树中执行插入操作
- 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容
我这里有个疑问hashcode是如何计算的,如果扩容之后,hashcode的计算方法是否会变,如果变了,之前插入的数据按现在的hashcode算法是否还是定位到那个数组索引
HashMap的扩容机制
讲一讲HashMap的扩容机制
注意在旧数组中在一个桶中的节点,在扩容后可能分配到不同桶
在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到扩容阈值(数组长度*0.75)
每次扩容的时候,都是扩容之前容量的2倍(数组的长度必须也肯定是2的n次幂)
扩容之后,会创建一个数组,需要把老数组中的数据挪到新的数组中
没有hash冲突的桶的节点,直接使用e.hash&(newCap-1)计算新数组的索引位置
如果是链表,遍历链表,判断每个节点(e.hash&oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+旧数组大小的位置
如果是红黑树,遍历红黑树,和链表节点的放置类似,但是红黑树的节点在新位置的添加和旧位置删除很复杂,如果插入的桶里面是红黑树,在新的位置还要考虑左旋右旋那些东西
为什么不管是这个桶是一个节点还是链表还是红黑树不都用e.hash&(newCap-1)计算节点的索引位置??
我不知道,但应该是甲酸效率更高
hashMap的寻址算法
为什么计算索引位置不取模而是按位与
采用e.hash&(newCap-1)而不是(newCap-1)%e.hash是因为e.hash&(newCap-1)性能更好,计算资源消耗小
为什么可以用(e.hash&oldCap)是否为0这么判断
因为老数组和新数组的长度的二进制数差异就在oldCap二进制数为1的那一位
e.hash&(Cap-1)计算数组的索引位置
比如,长度为16时,Cap是00001111
32时,Cap是00011111
oldCap是00010000,位与判断第4位是不是1就可以知道放在新位置还是不动
hashMap的寻址算法
计算对象的hashCode()
再进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更为均匀(扰动算法,hash^(hash>>16))
最后(capacity-1)&hash得到索引
为何HashMap的数组长度一定是2的次幂
计算索引时效率更高,如果是2的n次幂可以使用位与运算代替取模
扩容时重新计算搜因效率更高,hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
hashMap在1.7情况下的多线程死循环问题
hashMap在1.7情况下的多线程死循环问题
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据前移的过程中,有可能导致死循环
所以JDK8中采用尾插法
具体过程很绕,这里就不赘述了
并发编程篇
线程的基础知识
线程和进程的区别
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行和并发
并行和并发的区别
在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流在不同时间片使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,比如4核CPU同时执行4个线程
创建线程的方式
创建线程的方式有哪些
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程(项目中使用方式)
runnable和callable有什么区别 - Runnable接口run方法没有返回值
- Callable接口call方法有返回值,需要FutureTask获取结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
run()和start()有什么区别
start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码,start方法只能被调用一次
run():封装了要被线程执行的代码,可以被调用多次
线程包含的状态,状态之间的变化
线程包含了哪些状态
新建\可运行\阻塞\等待\时间等待\终止
线程之间是如何变化的
创建线程对象是新建状态
调用start()方法转变为可运行状态
线程获取到CPU的执行权,执行结束是终止状态
在可运行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获取锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep()方法,进入计时等待状态,到时间后可切换为可执行状态
线程按顺序执行
新建T1\T2\T3三个线程,如何保证它们按顺序执行
使用线程中的join方法解决
比如,在T2中调用T1.join(),阻塞调用此方法的线程进入timed_waiting直到线程T1执行完成后,此线程再继续执行
notify()和notifyAll()的区别
notify()和notifyAll()有什么区别
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个wait线程
java中wait和sleep方法的不同
java中wait和sleep方法有什么不同
共同点
wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点
方法归属不同
- sleep(long)是Thread的静态方法
- 而wait(),wait(long)都是Object的成员方法,每个对象都有
醒来时机不同 - 执行sleep(long)和wait(long)的线程都会在等待响应毫秒后醒来
- wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
- 它们都可以被打断唤醒(参考下面一个面试题)
锁特性不同(重点) - wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
- wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃CPU,但你们可以用)
- 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃CPU,你们也用不了)
停止一个正在运行的线程
如何停止一个正在运行的线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
-
- 打断阻塞的线程(sleep\wait\join),线程会抛出interruptedException异常
-
- 打断正常的线程,可以根据打断状态来标志是否退出线程
线程的并发安全
synchronized的底层原理
synchronized关键字的底层原理
synchronized底层是Monitor
- synchronized对象锁采用互斥的方式让同一时刻至多只有一个线程能持有对象锁
- 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner\entrylist\waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程
synchronized的底层原理进阶
轻量级锁\重量级锁
轻量级锁在线程间没有竞争时使用,都没竞争为什么要上锁 ,上锁是为了防止竞争,但是实际可能没竞争,这种情况用重量级锁性能低,一但竞争就会升级为重量级锁
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
- 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
- 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
一旦锁发生了竞争,都会升级为重量级锁