JAVA技术面试关键点
2025-04-27 11:22:09 0 举报
AI智能生成
在准备Java技术面试时,重点关注以下核心内容: 1. Java基础:理解Java的基本概念,如数据类型、运算符、控制结构和数组。 2. 面向对象编程:掌握类与对象、封装、继承、多态和接口的使用和理解。 3. 异常处理:理解Java的异常处理机制,包括try、catch、finally、throw和throws关键字的使用。 4. 集合框架:熟悉List、Set、Map等接口和它们的实现类,以及如何在实际场景中选择和使用它们。 5. 多线程编程:掌握线程的创建和管理,以及Java并发API,包括synchronized关键字、Lock接口、线程池的使用。 6. 输入/输出(I/O):理解Java I/O模型和流的概念,以及如何操作文件和序列化对象。 7. Java内存模型:了解垃圾回收机制,以及如何通过调优减轻内存泄漏和提高性能。 8. 数据库连接:熟悉JDBC的使用以及SQL基础,能够进行数据库操作。 9. 设计模式:具备常用的GoF设计模式知识,并能够根据实际情况合理应用。 10. Java框架:了解Spring、Hibernate或MyBatis等框架的基本使用和原理。 11. 微服务架构:理解微服务相关概念如服务注册与发现、配置管理、负载均衡和API网关等。 12. 单元测试:掌握JUnit或其他测试框架进行单元测试的能力。 在描述这些内容时,可采用以下修饰语:“深刻理解”、“熟练掌握”、“能够运用”、“充分了解”。此外,文件类型指的是面试准备者可能创建的类型的文档,如简历、笔记或代码示例。 示例描述(使用限制字数格式): 在Java技术面试中,确保您“深刻理解”Java基础、“熟练掌握”面向对象设计原则、“能够运用”高级特性如Lambda表达式,并且准备好讨论您在“充分了解”集合框架和并发API方面的实际应用。同时,准备展示您在数据库I/O操作、单元测试以及“熟练掌握”Spring框架方面的知识。创建清晰“笔记”文档,以备不时之需。
作者其他创作
大纲/内容
计算机基础
数据库
隔离级别
读未提交
读已提交
可重复度
串行化
索引
索引失效原因
模型数空运最快
模:模糊查询的意思。like的模糊查询以%开头,索引失效
型:代表数据类型。类型错误,如字段类型为varchar,where条件用number,索引也会失效
数:是函数的意思。对索引的字段使用内部函数,索引也会失效。这种情况下应该建立基于函数的索引。
空:是Null的意思。索引不存储空值,如果不限制索引列是not null,数据库会认为索引列有可能存在空值,所以不会按照索引进行计算
运:是运算的意思。对索引列进行(+,-,*,/,!, !=, <>)等运算,会导致索引失效
最:是最左原则。在复合索引中索引列的顺序至关重要。如果不是按照索引的最左列开始查找,则无法使用索引
快:全表扫描更快的意思。如果数据库预计使用全表扫描要比使用索引快,则不使用索引
索引类型
常规索引(INDEX)
唯一索引(UNIQUE)
主键索引(PRIMAY KEY)
全文索引(FULLTEXT)
调优
explain
引擎
Innodb
nnodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统
MyIASM
不提供事务的支持,也不支持行级锁和外键
MEMORY
所有的数据都在内存中,数据的处理速度快,但是安全性不高
mybatis
#{}”与”${}区别
"#{}" 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,一个 #{ } 被解析为一个参数占位符;而${}仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换
#{}" 解析之后会将String类型的数据自动加上引号,其他数据类型不会;而${} 解析之后是什么就是什么,他不会当做字符串处理。
"#{}" 很大程度上可以防止SQL注入(SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作);而${} 主要用于SQL拼接的时候,有很大的SQL注入隐患。
在某些特殊场合下只能用${},不能用#{}。例如:在使用排序时ORDER BY ${id},如果使用#{id},则会被解析成ORDER BY “id”,这显然是一种错误的写法
分库分表
分表方式
水平分表
按时间分表,区间范围进行分表
hash分表
通过一个原始目标的ID或者名称通过一定的hash算法计算出数据库存储表的表名,然后访问相应的表
垂直分表
把不常用的大字段分离出去,通过外键关联
MySQL InnoDB锁原理
锁住了索引项,如果没有索引则会锁整表
1、为什么锁住唯一索引后,主键索引也会被锁住?
这个是由InnoDB索引的存储和检索方式决定的。辅助索引中存储的是二级索引和主键的ID,所以锁住辅助索引后,会根据主键ID找到对应的主键索引,也锁定之。
而通过主键索引检索数据加锁,则只会锁住主键索引。
而通过主键索引检索数据加锁,则只会锁住主键索引。
2、为什么表没有索引,表里所有的记录都会被锁住?
当表上没有创建索引的时候,InnoDB会为每一行创建一个隐藏的主键作为聚集索引。这个隐藏的主键是一个6个字节的列,该列的值会随着数据的插入自增。
当不通过索引检索数据的时候,MySQL会使用全表扫描,此时所有行的索引都会被锁定,行锁升级为表锁。
当不通过索引检索数据的时候,MySQL会使用全表扫描,此时所有行的索引都会被锁定,行锁升级为表锁。
Mysql锁
按粒度划分
行锁
表锁
页锁
按使用方式划分
共享锁
排他锁
思想上
乐观锁
悲观锁
死锁
MyISAM
MyISAM中是不会产生死锁的,因为MyISAM总是一次性获得所需的全部锁,要么全部满足,要么全部等待。而在InnoDB中,锁是逐步获得的,就造成了死锁的可能。(不过现在一般都是InnoDB引擎,关于MyISAM不做考虑)
InnoDB
在InnoDB中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql语句操作了主键索引,MySQL就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。
MVCC
是什么?
多版本并发控制
主要是为Repeatable-Read事务隔离级别做的
实现方式
每一行记录都有隐藏列:如 DATA_TRX_ID、DATA_ROLL_PTR
DATA_TRX_ID
标记了最新更新这条行记录的transaction id,每处理一个事务,其值自动+1
DATA_ROLL_PTR
指向当前记录项的rollback segment的undo log记录,找之前版本的数据就是通过这个指针
DB_ROW_ID
6字节的DB_ROW_ID,当由innodb自动产生聚集索引时,聚集索引包括这个DB_ROW_ID的值,否则聚集索引中不包括这个值.,这个用于索引当中
DELETE BIT
DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在commit的时候
特点
1.每行数据都存在一个版本,每次数据更新时都更新该版本
2. 修改时Copy出当前版本随意修改,各个事务之间无干扰
3. 保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
mybatis缓存
一级缓存
SqlSession级别
如果用同一个sql执行两次相同的sql,第一次会执行查询打印sql,第二次则是直接从缓存中去获取,不会打印sql,从日志可以看出来只打印了一次sql,
默认开启
二级缓存
是Mapper级别的缓存
内部维护着一个hashMap
三大范式
第一范式
数据表中每个字段必须是不可拆分的最小单元(确保每一列的原子性)
userInfo:山东省烟台市 131777368781 userAds:山东0省烟台市 userTel:131777368781
第二范式
一张表中包含了所种不同的实体属性,那么要必须分成多张表
要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系
例如:订单表只描述订单相关的信息,所以所有字段都必须与订单id相关 产品表只描述产品相关的信息,所以所有字段都必须与产品id相 关;因此不能在一张表中同时出现订单信息与产品信息
第三范式
表中的每一列只与主键直接相关而不是间接相关
已经分成了多张表,那么一张表中只能有另一张表中的id(主键),而不能有其他的任何信息(其他的信息一律用主键在另一表查询)
jvm调优
java内存模型
堆
新生代
1/3堆空间
伊甸区/s0/s1
老年代
2/3堆空间
新生代进行多次Minor GC仍然存活的对象会移动到老年区,若老年区也满了,会产生FullGC,进行老年区的内存清理。若老年区执行了Full GC之后依然无法进行对象保存,就会产生内存溢出异常
新生代中的eden->from->to 每熬过一次Minor GC,年龄会加1,当它的年龄增加到一定程度(默认为15岁),就会晋升为老年代
新生代中的eden->from->to 每熬过一次Minor GC,年龄会加1,当它的年龄增加到一定程度(默认为15岁),就会晋升为老年代
永久代/元空间
jdk1.8以前的永久代,是方法区的实现,之前放在java虚拟机中jdk1.8以后用的是本地内存
程序计数器
方法区
本地方法栈
虚拟机栈
类加载机制
全盘委托机制
当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入
双亲委派机制
说明
指先委托父类加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类
优势
避免类的重复加载,确保一个类的全局唯一性
保护程序安全,防止核心API被随意篡改
自定义类加载器
可以继承 java.lang.ClassLoader 类,实现自己的类加载器。如果想保持双亲委托模型,就应该重写findClass(name) 方法;如果想破坏双亲委托模型,可以重写 loadClass(name) 方法
垃圾回收算法
引用计数法
给对象中添加一个引用计数器,没当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就认为可以被回收。
无法解决对象之间循环引用问题
可达性分析算法
以“GC Roots”的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象时不可能用的
finalize()方法最终判定对象是否存活
即使在可达性分析算法中不可达的对象,需要经历再次标记过程才真正宣告一个对象死亡
垃圾收集算法
标记清除算法
场景
适用于老年代
缺点
空间问题(标记清除后会产生大量不连续的碎片)
效率问题(会遍历内存)
会扫描两次
标记存活对象
清除没有标记的对象
标记复制算法
场景
适用于年轻代
存活对象少比较高效
扫描了整个空间标记存活对象,并复制
缺点
需要空闲空间
需要复制移动对象
标记整理算法
场景
适用于老年代
优点
在清除死亡对象后会整理内存,需要进行内存移动,但是内存空间是连续的,在分配内存时比较简单
消除了内存碎片
缺点
性能较低 ; 执行该垃圾回收算法时 , 需要 对内存进行重排 , 此时不能随意变动内存的数据结构 , 因此 执行该 标记-整理算法 时 , 整个线程需要整体停下来 , 但这样大大影响程序的执行效率
分代收集算法
新生代
复制算法
不存在内存碎片,但占内存
老年代
标记清除
耗时,有内存碎片,不占内存
标记压缩
不占内存,耗时,移动对象的成本
调优工具
jProfile
JAVA GUI监控工具
MAT
分析dump文件
jVisualvm
整合了jstack、jmap、jinfo
支持远程监控(需要监控的主机要添加启动参数)
-Dcom.sun.management.jmxremote.port=1099
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
可以生成dump文件,一般不会直接下载到本地,因为dump文件比较大
jConsole
jvisualvm是升级版
arthas
jstack
可以定位到线程堆栈
1.查看java进程
2.通过进程ID查询对CPU消耗最大的线程
ps -Lfp pid
top -Hp pid
ps -mp PID -o THREAD,tid,time
-m 显示所有的线程
-p 指定进程id
-o 该参数后是用户自定义格式
-p 指定进程id
-o 该参数后是用户自定义格式
3.将线程ID转成16进制用于查询
printf "%x\n" pid
4.打印线程的堆栈信息
jstack pid |grep tid -A 30
-A 显示多少行
tid 线程号
tid 线程号
jmap
用来查看堆内存使用状况
查看进程堆内存使用情况
jmap -heap pid
使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图,如果带上live则只统计活对象并且会强制执行一次GC
jmap -histo pid
jmap -histo:live pid
jmap -histo:live pid
用jmap把进程内存使用情况dump到文件中
jmap -dump:format=b,file=dumpFileName pid (文件的后缀建议采用".hprof")
查看堆外内存使用状况
jmap -J-XX:NativeMemoryTracking=summary <pid>
查看目标进程的堆内存占用前100的对象
jmap -histo 1 | head -n 100
该命令将生成一个包含Java堆中所有对象的直方图,显示每个类的实例数量和占用的内存大小。可以根据类的实例数量和占用的内存大小来估计每个对象的平均大小。
jmap -histo:live 1 | head -n 100
jstat
性能分析
Heap size和垃圾回收状况的监控
jstat -gc pid
可以显示gc的信息,查看gc的次数,及时间。
jhat
dump出来的文件可以用MAT、VisualVM等工具查看,这里用jhat查看,
jhat -port xxxx dumpFileName
jhat内置 web服务器,它会支持你通过浏览器以图形化的方式分析堆快照
默认端口 7000
jps
列出java所有进程
jps -l
输出传递给JVM的参数
jps -v
调优
OOM
首先配置JVM启动参数,让JVM在遇到OutOfMemoryError时自动生成Dump文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path
jmap -dump:format=b,file=/path/heap.bin 进程ID
如果只dump heap中的存活对象,则加上选项-live
如果只dump heap中的存活对象,则加上选项-live
然后使用MAT分析工具,如jhat命令,eclipse的mat插件
内存泄漏
help dump
生产机 dump
mat
-help dump
线程死锁
jstack
jstack 用于生成 Java 虚拟机当前时刻的线程快照,“-l”表示长列表(long),打印关于锁的附加信息
jconsole
jvisualvm
java进程消耗CPU过高
1.top 命令查看 cpu过高的进程
2.ps -ef | grep java 或jps命令 找出java进程
3.根据进程号查找线程TID
ps -mp PID -o THREAD,tid,time
查找出具体占用cpu利用率最厉害的线程号
top -Hp pid (shift+ p排序)
ps -mp PID -o THREAD,tid,time
查找出具体占用cpu利用率最厉害的线程号
top -Hp pid (shift+ p排序)
4.将需要的线程ID转换为16进制格式
printf "%x\n" tid
printf "%x\n" tid
5.打印线程的堆栈信息 jstack pid |grep tid -A 30
spring
依赖注入和控制反转
依赖注入
组件之间的依赖关系由容器在运行时决定,动态的向某个对象提供它所需要的其他对象
控制反转
由spring来负责对象的生命周期和对象间的关系(相当于通过中介买房子,所有的房子都登记在中介处,而你问Spring这个中介你需要哪个房子,在你需要买房的时候,可以直接找中介,中介会给你推荐房子,而不是由自己直接去找房)
三级缓存
单例池
用于存放 已经属性赋值、完成初始化的 单列BEAN
早期暴露的bean对象
用于存在已经实例化,还未做代理属性赋值操作的 单例BEAN
早期暴露的bean工厂
存储创建单例BEAN的工厂
循环依赖问题
怎么破解
属性注入可以破解(set方法)
构造器注入无法破解
描述
A依赖B,B依赖A
1、A创建过程中需要B,于是先将A放到三级缓存,去实例化B。
2、B实例化的过程中发现需要A,于是B先查一级缓存寻找A,如果没有,再查二级缓存,如果还没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A。
3、B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中的状态)。然后回来接着创建A,此时B已经创建结束,可以直接从一级缓存里面拿到B,去完成A的创建,并将A放到一级缓存。
bean生命周期
1.实例化
简单理解就是new了一个对象
AbstractAutowireCapableBeanFactory中的createBeanInstance方法
2.属性注入
为实例化中new出来的对象填充属性
AbstractAutowireCapableBeanFactory的populateBean方法
3.初始化方法
执行aware接口中的方法,初始化方法,完成AOP代理
AbstractAutowireCapableBeanFactory的initializeBean
如果实现了 Aware 接口,会通过其接口获取容器资源
如果实现了 BeanPostProcessor 接口,则会回调该接口的前置和后置处理增强
如果配置了 init-method 方法,]会执行该方法
4.销毁
如果实现了 DisposableBean 接口,则会回调该接口的 destroy 方法
如果配置了 destroy-method 方法,则会执行 destroy-method 配置的方法
springmvc流程
1. 用户通过浏览器发起 HttpRequest 请求到前端控制器 (DispatcherServlet)。
2. DispatcherServlet 将用户请求发送给处理器映射器 (HandlerMapping)。
3. 处理器映射器 (HandlerMapping)会根据请求,找到负责处理该请求的处理器,并将其封装为处理器执行链 返回 (HandlerExecutionChain) 给 DispatcherServlet
4. DispatcherServlet 会根据 处理器执行链 中的处理器,找到能够执行该处理器的处理器适配器(HandlerAdaptor) --注,处理器适配器有多个
5. 处理器适配器 (HandlerAdaptoer) 会调用对应的具体的 Controller
6. Controller 将处理结果及要跳转的视图封装到一个对象 ModelAndView 中并将其返回给处理器适配器 (HandlerAdaptor)
7. HandlerAdaptor 直接将 ModelAndView 交给 DispatcherServlet ,至此,业务处理完毕
8. 业务处理完毕后,我们需要将处理结果展示给用户。于是DisptcherServlet 调用 ViewResolver,将 ModelAndView 中的视图名称封装为视图对象
9. ViewResolver 将封装好的视图 (View) 对象返回给 DIspatcherServlet
10. DispatcherServlet 调用视图对象,让其自己 (View) 进行渲染(将模型数据填充至视图中),形成响应对象 (HttpResponse)
11. 前端控制器 (DispatcherServlet) 响应 (HttpResponse) 给浏览器,展示在页面上。
锁
synchronized
jvm层面的锁
可以修饰静态方法,实例方法,代码块
锁膨胀
无锁
存储内容
hash码,分代年龄
偏向锁
执行流程
1.对象头 Mark word 存储锁偏向的线程id
2.有线程进入后,检测 Mark Word 里是否存储着指向当前线程的偏向锁
3.判断 Mark Word 中的线程 ID 和访问的线程 ID 是否一致
yes
4.1 直接进入同步块进行代码执行
no
4.2 使用 CAS 尝试获取锁,如果获取成功则进入同步块执行代码,否则会将锁的状态升级为轻量级锁。
适用于只有一个线程访问同步块场景。
引入原因
在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。
存储内容
线程id,时间戳,分代年龄
CAS轻量级锁
存储内容
指向栈中锁记录的指针ptr
Mark Word复制到锁记录,CAS更新指针及标志位00
自旋方式竞争,竞争的线程不会阻塞,提高了程序的响应速度
自旋方式竞争,竞争的线程不会阻塞,提高了程序的响应速度
描述
当关闭偏向锁或者多个线程竞争偏向锁时就会导致偏向锁升级为轻量级锁,轻量级锁的获取和释放都通过 CAS 完成的,其中锁获取可能会通过一定次数的自旋来完成
自旋一定次数
死循环
重量级锁
存储内容
指向monitor的指针ptr
锁消除
在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的
锁粗化
将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
自适应自旋锁
线程自旋的次数不再是固定的值,而是一个动态改变的值,这个值会根据前一次自旋获取锁的状态来决定此次自旋的次数
特性保证
有序性
可见性
内存强制刷新
原子性
单一线程持有
可重入性
计数器
对象在内存中存储的布局
对象头(Header)
Mark Word(标记字段):我们的偏向锁信息就是存储在此区域的
Class 对象指针
实例数据(Instance Data)
对象实例数据
对齐填充(Padding)
对象数据
用于补齐对象内存长度的。
果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小
lock
java类
lock和sync的区别
synchrozied会自动释放锁,而Lock必须手动释放锁。
synchrozied是不可中断的,Lock可以中断也可以不中断。
通过Lock可以知道线程有没有拿到锁,而synchrozied不能。
synchrozied能锁住方法和代码块,而Lock只能锁住代码块。
Lock可以使用读锁提高多线程读效率。
synchrozied是非公平锁,ReentrantLock可以控制是否是公平锁。
CAS
描述
CAS全称Compare and Swap,比较和交换的意思,乐观锁就是CAS实现的(内部使用的是CAS无锁算法)java并发包中许多Atomic的类的底层原理都是CAS
缺点
1.CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力
2.不能保证代码块的原子性
CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronize了
3.ABA问题
如果内存地址V初次读取的值是A,在CAS等待期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。
解决方案
使用带版本号的原子引用AtomicStampedRefence<V>,或者叫时间戳的原子引用,类似于乐观锁。
公平锁/非公平锁
公平锁
hasQueuedPredecessors方法
查询是否有任何线程在等待获取锁的时间比当前线程长
方法会出现误判,所以本质也是不公平的
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁
非公平锁
直接尝试获取锁
消息队列
rocketMq
消息丢失问题怎么解决
生产者网络抖动,通讯异常:rockemq自带事务机制,使用事务机制传输消息
rocket mq消息持久化到磁盘失败,或者磁盘损害:接收到 发送者的消息,采用同步落盘
消费者宕机:mq在消费者中注册了一个监听器,消费成功会返回一个CONSUME_SUCCESS,生产者就会知道消息已经消费完了,如果没成功会重发,重试次数是16次,如果超过次数会进入死信,3天后会自动删除
RocketMQ支持异步实时刷盘,同步刷盘,同步Replication,异步Replication
长轮询,消息投递延时通常在几毫秒
RocketMQ消费失败支持定时重试,每次重试间隔时间顺延
RocketMQ消息顺序,broker宕机,不会乱序
延时消息功能
有18个延时级别,最大支持2小时
Kafka
Kafka只支持异步刷盘,异步Replication
Kafka的TPS高,不稳定
KafkaTPS高因为producer将多个消息合并,批量发向broker。这里会有一些问题,缓存过多消息,gc是个问题;producer发送消息到broker,向业务返回成功,但是producer宕机,导致消息丢失;producer分布式系统,且每台机器是多线程发送,单个producer产生的数据量有限;发送消息单一功能,缓存可由上层业务完成。
短轮询,实时性取决轮询间隔时间
Kafka消费失败不支持重试
Kafka消息顺序,broker宕机,产生消息乱序
java基础
面向对象特性
继承
封装
多态
抽象
拆装箱
装箱
把基本数据类型转换为对应包装类型,在编译时调用Integer.valueOf() 方法装箱
拆箱
把包装类型转换为基本数据类型,在编译时调用intValue() 方法拆箱
String,StringBuffer,StringBuilder
String
是不可变的字符串,因为底层使用了不可变的字符数组(用final修饰)
StringBuffer
是线程安全的,但是效率较低
StringBuilder
是线程不安全的,但是效率高
集合
list
ArrayList
数组
查询快,修改慢
初始容量10,扩容:0.5*容量+1
LinkedList
链表
插入删除效率高
Vector
数组
线程安全
Vector所有方法都是同步,有性能损失。
Vector初始length是10 超过length时 以100%比率增长,相比于ArrayList更多消耗内存
set
HashSet
底层数据结构是哈希表
TreeSet
底层数据结构是红黑树(是一个自平衡的二叉树)
LinkedHashSet
底层数据结构由哈希表和链表组成。
map
LinkedHashMap
hashmap的子类,具有hashmap的所有特性,遍历的是内部维护的双向链表
增加了before和after指针可以保持顺序
加了双向链表头结点header 和 标志位accessOrder两个属性用于保证迭代顺序
重写hashmap迭代器
TreeMap
有序的
HashMap
扰动函数
增加随机性,让数据元素更加均衡的散 列,减少碰撞
hashCode的二进制码右移16位
jdk7 四次>>>运算,jdk8一次>>>
负载因子
默认值0.75,阀值容量占了 3/4 时赶紧扩容
初始容量
16
线程不安全
jdk1.7中多线程环境下,使用Hashmap进行put操作会引起死循环(环形链表列表),jdk1.8解决了这个问题,但是仍有数据覆盖的问题
扩容
扩容为两倍,如初始长度16,扩容后是32
jdk7和8区别
jdk7
数组+链表
头插法
扩容时,存在环形链表死循环问题(扩容时重新定位每个桶的下标,并采用头插法将元素迁移到新数组中。头插法会将链表的顺序翻转,)
hash值计算
h^ =(h>>>20)^(h>>>12) return h ^(h>>>7) ^(h>>>4)
jdk8
数组链表+红黑树
当链表的个数大于8,hash 容量大于64时 转为红黑树
尾插法
解决jdk7中头插法导致的环状链表问题
hash值计算
(key==null)?0:(h=key.hashCode())^(h>>>16)
一维数组存放散列地址,以便更快速的遍历;用链表存放地址值,以便更快的插入和删除
HashKey算法
使用取模的方式(key%table) size 算出散列值,散列键表示存储或者查找项中的位置
ConcurrentHashMap
jdk1.7
分段锁 Segment
大的hashtable 集合分成n个小hashtable集合,默认是16个,计算两次index值,第一次计算key在哪个hashtable 集合中,第二次计算具体位置
根据key的hash值的高n位就可以确定元素到底在哪一个Segment中
Segment是ReentrantLock的子类,因此Segment本身就是一种可重入的Lock,使用lock上锁解锁
默认并发级别为16,支持16个线程执行并发写操作
get方法
1.为输入的Key做Hash运算,得到hash值。
2.通过hash值,定位到对应的Segment对象
3.再次通过hash值,定位到Segment当中数组的具体位置。
put方法
1.为输入的Key做Hash运算,得到hash值2.通过hash值,定位到对应的Segment对象
2.获取可重入锁
3.再次通过hash值,定位到Segment当中数组的具体位置。
4.插入或覆盖HashEntry对象。
5.释放锁。
jdk1.8
CAS+Synchronized
value 使用volatile 保证可见性
数组+链表+红黑树(和hashMap一样)
避免计算两次index值,所以效率高
锁住对应的链表位置
hashtable
synchronized
锁住整个hashtable数组,效率低,该类目前已废弃,用的较少
事务特性
原子性
事务不可分割,要么都成功要么都失败
持久性
修改数据后,在数据库中生效是永久的
一致性
要么都成功,要么都失败,后面失败了,需要对前面的操作进行回滚
隔离性
一个事务开始后,不能被其他事务干扰,举例:对于A对B进行转账,A没把这个交易完成的时候,B是不知道A要给他转钱。
多线程
实现方法
继承Thread类,重写run方法
实现runable接口,重写run方法
线程池
作用
限定线程个数,不会导致由于线程过多导致系统运行缓慢或崩溃
线程池不需要每次都去创建或销毁,节约了资源
线程池不需要每次都去创建,响应时间更快
threadlocal
内存泄漏问题
手动remove、get、set
volatile
可以保证可见性和可见性,无法保证原子性
线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
顺序一致性:禁止指令重排序
为什么++执行结果不对
1.读取值
2.做+1,-1操作
3.赋值
当一个线程在进行第一步操作之后,
另外一个线程对变量进行了++或者--操作,
这时候就会出现原子操作了
另外一个线程对变量进行了++或者--操作,
这时候就会出现原子操作了
优雅的关闭线程池
在 shutdown hook 的回调方法中,我们通常需要处理一些 close 任务,比如 necos 注销等。另外,如果有一些业务代码使用了线程池,那么线程池的关闭也需要注意优雅,即保证线程执行安全
start和run的区别
start
同步方法
start方法在执行过程中会产生一个新线程
run
非同步方法
run方法在执行过程中不会产生新的线程,run()方法将作为当前调用线程本身的常规方法调用执行,并且不会发生多线程
线程生命周期
1.New 新建状态
创建线程对象后,该线程处于新建状态,此时它不能运行,和其他Java对象一样,仅仅有Java虚拟机为其分配了内存,没有表现出任何线程的动态特征
2. Runnable 就绪状态
线程对象调用了start()方法后,该线程就进入了就绪状态(也称可运行状态)。处于就绪状态的线程位于可运行池中,此时它只是具备了运行的条件,能否获得CPU的使用权开始运行,还需要等待系统的调度;
3. Runing 运行状态
处于就绪状态的线程获得了CPU使用权,开始执行run()方法中的线程执行体,则线程处于运行状态。当一个线程启动后,它不能一直处于运行状态(除非它的线程执行体足够短,瞬间结束),当使用完系统分配的时间后,系统就会剥脱该线程占用的CPU资源,让其他线程获得执行的机会。只有处于就绪状态的线程才可能转换到运行状态
4.Blocked 阻塞状态
一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引用阻塞的原因,被消除后,线程才可以进入就绪状态
5.Terminated 中止状态
线程的run()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能转换为其他状态。
分布式
布隆过滤器
bitmap 元素只存储0和1 占用空间小
标记为1的位置表示存在,标记为0的位置标示不存在
标记为1的位置表示存在,标记为0的位置标示不存在
布隆过滤器是可以以较低的空间占用来判断元素是否存在进而用于去重
分布式锁
基于数据库 (唯一索引)
如可以把方法名作为唯一约束
基于zooKeeper
基于redis
set nex
需要设置过期时间
看门狗线程每隔10s检查客户端是否持有锁,如果是,就延长锁的过期时间
使用lua脚本保证原子性操作
redission
tryLock
会不断尝试去获取锁
加锁成功后会启动看门狗线程默认续期时间30s,每隔10s轮训一次
内部使用lua脚本
加锁/释放锁采用lua脚本完成
redissionLock类
redlock
需要五个完全独立的redis
TTL
Time To Live;只 redis key 的过期时间或有效生存时间
clock drift
指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值;如果电脑距离过远会造成时钟漂移值 过大
加锁过程
获取当前时间戳
只要n/2+1个节点加锁成功就认为获取了锁,加锁成功
三个节点加锁成功并且花费时间小于锁的有效期
根据一定的超时时间来推断是不是跳过该节点
顺序向五个节点请求加锁
分布式事务
强一致性事务
2PC:两阶段提交(XA)
准备阶段
提交阶段
利用数据库的事务
3PC
准备阶段
预提交阶段
提交阶段
有数据不一致,阻塞等风险,而且只能用在数据库层面。
依赖数据库的事务
补偿性事务
TCC (Try - Confirm - Cancel)
Try 指的是预留,即资源的预留和锁定,注意是预留。
Confirm 指的是确认操作,这一步其实就是真正的执行了。
Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
业务层面的分布式事务
TCC可以跨数据库、跨不同的业务系统来实现事务。
描述
TCC 是一种补偿性事务思想
对业务的侵入性较大,每一个操作都需要实现对应的三个方法
业务层面需要实现
最终一致性事务
消息事务
执行流程
1.给broker发送事务消息(半消息)
这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务
2.执行本地事务
3.根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
本地事务执行成功
commit
订阅方收到这条消息
执行失败
RollBack
丢弃消息
4.订阅方接收到消息
5.订阅方执行本地事务
6.订阅方消费消息
补偿机制
需要提前定义好反查事务接口
broke反查事务状态
如果一段时间内半消息没有收到任何操作请求,RocketMQ 的发送方会提供一个反查事务状态接口,Broker 会通过反查接口得知发送方事务是否执行成功
成功
Commit
订阅方就能收到这条消息
失败
RollBack
丢弃消息
本地消息表
将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态
seata
AT模式
对代码无侵入,只通过注解方式就能分析出数据库做了什么
添加@GlobalTransactional注解
原理
一阶段提交数据
做了aop切面拦截
对数据库操作记录在undolog回滚里(seata自己解析的数据库日志)
二阶段
如果成功则删除回滚日志,释放全局锁
有分支执行失败,则执行undolog回滚日志
牺牲了一定的并发能力
子主题
中间件
redis
集群模式
哨兵模式
描述
和主从模式不一样的是,哨兵模式中增加了独立进程(即哨兵)来监控集群中的一举一动。客户端在连接集群时,首先连接哨兵,通过哨兵查询主节点的地址,然后再去连接主节点进行数据交互。 如下图所示,如果master异常,则会进行master-slave切换,将最优的一个slave切换为主节点,同时,哨兵持续监控挂掉的主节点,待其恢复后,作为新的从节点加入集群中
Sentinel 的节点数量要满足 2n + 1
工作方式
1) 每个哨兵每秒向集群中的master、slave以及其他哨兵发送一个PING命令;
2) 如果某个实例距离最后一次有效回复ping命令的时间超过一定值,则会被标记为主观下线;
3) 如果master被标记为主观下线,那么其他正在监视master的哨兵以每秒的频率确认其确实进入主观下线状态,且数量达到一定值时,master会被标记为下线,然后通知其他的从服务器,修改配置文件,让它们切换主机;
4) 客户端在master节点发生故障时会重向哨兵要地址,此时会获得最新的master节点地址。
扩容问题
水平扩容
通过增加节点来进行扩容,即在当前基础上再增加一个master节点。
垂直扩容
通过增加master内存来增加容量
主从模式
工作方式
全量同步
1.当从节点启动时,会向主节点发送SYNC命令
2.主节点接收到SYNC命令后,开始在后台执行保存快照的命令生成RDB文件,并使用缓冲区记录此后执行的所有写命令;
3.主节点快照完成后,将快照文件和所有缓存命令发送给集群内的从节点,并在发送期间继续记录被执行的写命令;
4. 主节点快照发送完毕后开始向从节点发送缓冲区中的写命令;
5. 从节点载入快照文件后,开始接收命令请求,执行接收到的主节点缓冲区的写命令。
故常处理方式
主从模式中,每个客户端连接redis实例时都指定了ip和端口号。如果所连接的redis实例因为故障下线了,则无法通知客户端连接其他客户端地址,因此只能进行手动操作。
缺陷
主从模式很好地解决了数据备份的问题,但是主节点因为故障下线后,需要手动更改客户端配置重新连接,这种模式并不能保证服务的高可用
Cluster模式
无中心节点:3 主 3 从
主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用
虚拟槽分区,所有的键根据哈希函数映射到 0~16383 个整数槽内,每个节点负责维护一部分槽以及槽所映射的键值数据。
数据分片
数据分片
Master1负责0~5460号哈希槽
Master2负责5461~10922号哈希槽
Master3负责10922~16383号哈希槽
Master2负责5461~10922号哈希槽
Master3负责10922~16383号哈希槽
客户端连接集群时,直接与redis集群的每个主节点连接,根据hash算法取模将key存储在不同的哈希槽上;
主节点故障处理方式
redis cluster中主节点故障处理方式与哨兵模式较为相像, 当约定时间内某节点无法与集群中的另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态,同时将这个信息向整个集群广播。 如果一个节点收到某个节点失联的数量达到了集群的大多数时,那么将该节点标记为客观下线状态,并向集群广播下线节点的fail消息。然后立即对该故障节点进行主从切换。等到原来的主节点恢复后,会自动成为新主节点的从节点。 如果主节点没有从节点,那么当它发生故障时,集群就将处于不可用状态。
网络
分层
物理层
数据链路层
网络层
传输层(tcp-udp)
会话层
表示层
应用层(http)
http
明文数据传输
效率相对https低
https
为啥安全
结合ssl证书 ,加密传输
tcp
三次握手
四次握手
udp
视频
直播
语音
IO模型
BIO
阻塞式
同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。
NIO
同步非阻塞
同步非阻塞的I/O模型
AIO
异步非阻塞
注册中心
Nacos
Nacos 并不是通过推的方式将服务端最新的配置信息发送给客户端的,而是客户端维护了一个长轮询的任务,定时去拉取发生变更的配置信息,然后将最新的数据推送给 Listener 的持有者
拉的优势
如果用推的方式,服务端需要维持与客户端的长连接,这样的话需要耗费大量的资源,并且还需要考虑连接的有效性,例如需要通过心跳来维持两者之间的连接。而用拉的方式,客户端只需要通过一个无状态的 http 请求即可获取到服务端的数据。
如果检查数据变化
比对listener 所持有的 md5 值已经和 CacheData 的 md5 值
客户端是通过一个定时任务来检查自己监听的配置项的数据的,一旦服务端的数据发生变化时,客户端将会获取到最新的数据,并将最新的数据保存在一个 CacheData 对象中,然后会重新计算 CacheData 的 md5 属性的值,此时就会对该 CacheData 所绑定的 Listener 触发receiveConfigInfo 回调
Eureka
缓存
缓存穿透
描述
出现Redis中不存在的缓存数据。例如:访问id=-1的数据。可能出现绕过redis依然频繁访问数据库,称为缓存穿透,多出现在查询为null的情况不被缓存时。
解决方案
布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
描述
由于设置缓存时,key都采用了相同expire,导致缓存在某刻同时失效,请求全部直到DB,DB瞬时负载过重而雪崩。
解决方案
在原有失效时间基础上增加一个随机值,比如1~5分钟的随机,这样每个缓存的过期时间重复率就会降低,集体失效概率也会大大降低。
首先环境保证redis高可用,主从+哨兵,redis cluster,避免全盘崩溃。
接着用户并发请求后,服务会先通过hystrix限流&降级,再查本地Ehcache缓存,若没有再查redis,都没有,就查MySQL。
最后,将MySQL中的结果写入ehcache和redis、即使 redis挂了,通过redis持久化,也能快速恢复缓存数据
首先环境保证redis高可用,主从+哨兵,redis cluster,避免全盘崩溃。
接着用户并发请求后,服务会先通过hystrix限流&降级,再查本地Ehcache缓存,若没有再查redis,都没有,就查MySQL。
最后,将MySQL中的结果写入ehcache和redis、即使 redis挂了,通过redis持久化,也能快速恢复缓存数据
缓存预热
存预热就是系统上线后,先将相关的数据构建到缓存中,这样就可以避免用户请求的时候直接查库。
缓存击穿
这个key是一个热点key,访问量非常大,缓存的构建是需要一定时间的。(可能是一个复杂计算,例如复杂的sql、多次IO、多个依赖(各种接口)等等)
解决方案
互斥锁
其他
spi
描述
SPI 是服务方不再提供具体的方法,而是提供对象的接口,客户方需要实现接口,然后服务方调用客户方的接口实现类,再来实现客户的某种功能。
Java 提供的一套用来被第三方实现或者扩展的接口,JDK内置的一种服务提供发现机制
提供具体方法
例子
springboot
2.7以下/META-INF/spring.factors加载默认配置
2.7以上/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
jdbc
Java 中定义了 java.sql.Driver 接口,并没有具体的实现,具体实现由各厂商来提供,通过厂商提供的驱动包来连接自己的数据库。
描述
api
API 可以理解为服务方暴露自己的服务供客户调用,JDK 就作为服务方,提供了一系列的 api 可以直接使用的方法供客户方开发人员直接进行使用,从而达到某种功能的实现
只提供实现功能的规范(接口)
0 条评论
下一页