Java面试题小小汇总
文章目录
- Java面试题小小汇总
- 一、Java常见面试题
- 二、Spring
- 三. 消息队列的选择
- 四、 Elasticsearch
- 五、 MySQL
- 1. 四大特性
- 2. MySQL事务隔离级别
- 3. 脏读, 幻读, 可重复度, 不可重读度
- 4. 什么时候选择MyISAM, 和innodb区别
- 5. 为什么选择B+Tree做索引数据结构
- 6. MySQL索引四种类型
- 7. 建索引注意事项
- 8. B+Tree的叶子能存哪些数据
- 9. order by原理
- 10. 建表注意事项
- 11. SQL优化
- 12. MySQL索引失效
- 13 select * 和select全字段, select count(*) 和select count(1), select count(列)
- 14. MySQL 5 和MySQL 8区别
- 15. MVCC
- 16. MySQL集群
- 17. Explain关注字段
- 六、Redis
- 七、JUC
- 八、Tomcat, Jetty, Undertow
- 九、JVM
- 十、分布式事务
- 十一、Dubbo
一、Java常见面试题
1. 封装
- 比如说定义一个实体类, 属性都是私有的, 对外提供公开的构造方法, 公开的getset方法, 外部调用无需知道内部如何实现
2. 继承
- 子类公用父类属性, 对父类做扩展或者改变
3.多肽
- (必要条件就是继承或者实现接口, 方法重写 , 父类引用指向子类对象)比如两个类继承一个父类, 实现同一个方法, 方法名相同, 实现内容不同, 即基于对象所有类不同, 外部调用同一个方法, 执行逻辑不同
4. final
- 修饰类: 不可被继承
- 修饰方法: 不可被重写
- 修饰变量: 表示一旦赋值就不可改变他的值
- 修饰类变量: 只能在静态初始化中指定初始值或者声明该类变量时指定初始值
- 修饰成员变量: 可以在非静态初始化块声明该变量或者构造器中执行初始值
5. 重载和重写的区别
- 重载: 在同一个类中, 方法名名相同, 参数类型, 个数, 或者顺序不同, 返回值或者修饰符不同
- 重写: 在父子类中, 子类重写父类非私有方法 , 返回值返回小于等于父类, 抛出异常范围小于等于父类
6. 接口和抽象类区别
- 抽象类可以有普通带方法体的方法, 接口只能有抽象方法
- 抽象类中的成员变量可以任意声明, 接口中的变量只能是public static final类型
- 抽象类只能继承一个, 而接口可以实现多个
- 接口可以对实现他的类做行为有无的约束, 不限制实现方式
- 抽象类主要做代码复用, 很多子类有相同特性
7. List和Set区别
-
List可以用迭代器,for循环遍历, set只能迭代器遍历
-
List有序可重复, Set无序不可重复
-
HashCode和equals
- HashSet解决重复, 先判断hashCode是否相同, hashCode相同再用equals比较, hashCode就是对象在堆上索引
- hashCode相同对象不一定相同, 两个对象equals相等, 那么hashCode一定相等
- 两个对象相等那么equals也一定相等
8. ArrayList和LinkedList
- ArrayList: 底层数据结构是初始长度为10的动态数据, 扩容机制是原数组长度的1.5倍, 数组在内存中是一串连续的内存, 适合下标访问, 时间复杂度是O(1), 指定初始容量, 插入数据用尾插法, 可以大大提升性能, 甚至比LinkedList还要好, LinkedList要创建大量node
- LinkedList: 数据结构是双向链表, 插入删除效率较高, 查询相比较慢, 指针移动, 时间复杂度O(n)
- ArrayList读的效率比LinkedList高, 连续内存, 时间复杂度O(1), 写入效率没有LinkedList高, 会存在数据移动, 数组扩容复制操作
9. Map
基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键
初始容量16, 加载因子0.75
查询的时间复杂度
- 数组: O(1)
- 链表: O(n)
- 红黑树: O(logn)
1. 数据结构
- JDK 1.7 数据结构: 数组 + 链表 hash冲突后, 就都会形成链表, 是不想出现的结果, 数组就会扩容, 尽量避免hash冲突
- JDK 1.8 数据结构: 数组 + 链表 + 红黑树 JDK 1.7基础上, 链表长度过长, 会导致查询效率常数阶O(1)变成线程阶O(n) , 为了提高查询效率 , 引入红黑树O(logn)
2. HashMap存值过程
- 根据key计算一个hash值
- 在put的时候判断数组是否存在, 如果不存在创建一个默认长度为16的数组
- 根据key的hash值与数组中的最大索引值进行与(&)运算得到索引位置 , 确定node在数组中的位置
- 判断该位置是否有元素, 如果没有元素直接放进去, 如果有元素, 判断key是否相同, 如果相同 , 把原来的node赋值给一个变量
- 再去判断该位置是红黑树还是链表
- 如果是红黑树, 以红黑树的形式将node放在树上, 如果是链表放在链表的最后位置
- 放完之后判断链表长度 , 如果链表长度大于8则要判断是否变成红黑树
- 返回被覆盖的值
- 判断整个数组是否需要扩容
3. HashMap和HashTable、ConcurrentHashMap的区别
- HashMap线程不安全,
- HashTable, ConcurrentHashMap线程安全
- HashTable方法都是用synchronized修饰的同步代码块 , 并发情况下效率过低
- ConcurrentHashMap解决HashTable效率低问题
- 在JDK1.7的时候使用的分段锁
- 1.8使用CAS解决线程安全问题和高并发响应速度
4. 为什么要在put的时候创建数组
- 数组在内存中占用的一段连续的内存空间, 占用内存较多, 所以创建的map不使用也创建数组的话, 就会浪费内存.
5. HashMap什么时候会变成红黑树 什么时候由红黑树变成链表
- 在数组长度到达64 , 并且链表长度到达8个时候由链表变成红黑树 , 当元素数量小于等于6的时候由红黑树转成链表.
扩展: 解决hash冲突的常用方法:
- 开防寻址法
- 如果出现了散列冲突,就重新探测一个空闲位置
- 当往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止
- 链表法
- 就是hashMap解决hash冲突的方法
10. 为什么要设计迭代器
- 迭代器适用性更强, 可以在不了解集合内部数据结构的情况下直接遍历
- for循环需要知道遍历的数据类型
11.有哪些元注解
- @Target 描述注解的使用范围(类, 接口, 枚举, 方法, 参数, 构造器)
- @Inherited 允许继承父类注解
- @Retention 描述注解保留时间范围(保留到class文件中, 运行时保留 , 源文件保留)
- @Document 描述在写使用javadoc工具的时候是否要保留的注释信息
12. 构造器是否可以被重写?
- 不能, 构造器不能被继承, 所以不能被重写
- 构造器是构造对象唯一标识的方法
13. Object中的方法
- native方法
- getClass()
- hashCode()
- clone()
- notify()
- notifyAll()
- wait()
- equals()
- toString()
- finalize()
14. 加密
-
AES, DES, 3DES 对称加密
-
MD5 非对称加密 - 哈希加密 - 不可解密
-
BASE64
-
SHA - 哈密加密 - 不可解密
-
HMAC 不可解密
-
RSA, DSA - 数字签名
15. CAP理论
- Consistency : 一致性是指写操作后的读操作读取到最新的数据 , 当数据分布在多节点上的时候 , 从任意节点读取都是最新的数据(主从) 要么返回正确数据 , 要么报错.
- Availability : 可用性是指事务任何操作都可以得到响应结果 , 且不会出现响应超时或者响应错误 . 要么返回正确数据 , 要么返回旧数据.
- Patition tolerance 分区容错性: 通常分布式系统的各个节点都部署在不同的子网 , 这就是网络分区 , 不可避免的可能会出现网络问题而导致节点之间通信失败 , 此时仍可对外提供服务
二、Spring
1. IOC
1.1 控制反转
- 将创建管理对象的工作交给容器来做。在容器初始化(或在某个时间节点)通过反射机制创建好对象,在使用时直接从容器中获取。
1.2 依赖注入(DI)
- IOC来管理对象关系,在创建有依赖关系的对象时,由IOC容器来注入依赖的兑现。构造器注入、setter方法注入。
2. AOP面相切面编程
2.1 AOP作用
-
将一些系统性相关(日志、事务、安全)的编程工作,独立提取出来,独立实现,作为一个公共切面切入到相关的业务逻辑中。
-
将业务逻辑代码与公共功能代码分离开,使开发人员能更专注地编写业务逻辑代码。
-
在不改变业务逻辑代码的前提下,在业务逻辑之前、之后、或者周围添加横切关注点(切面),对目标功能进行扩展。
2.2 AOP术语解读
- 通知:定义AOP切面执行的工作,以及切面执行的时间。(Before、After、After-returning、After-throwing、Around)
- 切点:定义切面执行的地点,满足配置条件的目标方法(在什么地方执行切面)。
- 连接点:切点的全集。一个程序可能会有多个切面,不同切面映射需要不同切点。
- 切面:切点+通知。在什么时候、什么地点、执行什么工作。
2.3 AOP原理: 动态代理
实现动态代理的两种常用的方式:
- JDK动态代理 - 基于接口 - 被代理的类 至少实现一个接口
- jdk 官方的Proxy类
- 要求:被代理的类至少实现一个接口
- 实现:基于接口的动态代理
- CGlib动态代理 - 基于子类 - 被代理的类, 不能是final修饰
- 第三方的CGlib (如果想要使用的话 需要导入依赖 asm.jar)
- 要求:被代理类不能用 final 修饰的类(最终类)
3.Spring注入bean的三种方式
- 构造方法注入
- setter方法注册
- 注解注入(属性注入, @Resource, @Autowired, @Inject)
3. Spring Bean生命周期
- 容器先实例化一个bean
- 再根据配置设置属性
- 如果Bean实现了BeanNameAware则调用Bean的setBeanName()方法传递Bean的ID
- 如果Bean实现了BeanFactoryAware则调用Bean的setBeanFactory()方法传入自身工厂
- 如果Bean实现了BeanPostProcessor 则会调用postProcessBeforeInitialization()返回一个Bean
- 如果Bean实现了InitializingBean, 则调用afterPropertiesSet()
- 执行initBean()方法 ( Bean定义的文件中使用"init-method"属性设定方法名称)
- 如果Bean实现了BanPostProcessor则会调用postProcessAfterInitialization()
- 如果Bean实现了DisposableBean接口, 则会调用该Bean的destroy()方法, 否则调用创建Bean是定义的destroy-method方法
4. 动态代理实现方式
- JDK动态代理是利用反射机制生成一个实现代理接口的匿名类 , 调用具体方法前调用invokeHandle来处理
- cglib是利用字节码技术, 执行方法时生成一个实现接口的.class文件
5. JDK动态代理和cglib动态代理区别
- JDK动态代理只能代理实现了接口的类 , 实现方式是实现InvocationHandler
- cglib动态代理是针对类实现代理 , 对指定类生成一个子类 , 覆盖其中的方法, 因为是继承, 所以被代理类属性不要用final修饰, 实现方式是实现MethodInterceptor
6. Spring如何解决循环依赖的
- Spring利用三级缓存来解决循环依赖
- 比如是A-B-A循环依赖问题, 首先调用A的实例化, 会在缓存中去找A对象, 发现缓存中没有,此时还没有设置属性, 就会调用实例化流程, 会将未实现完全的对象A存入三级缓存, 设置属性时会触发B的实例化, 也是先在缓存中获取B对象, 缓存中没有就开始实例化B对象, 因为B对象依赖A, 此时会触发获取A对象(是第二次获取A对象了), 这时缓存中就能获取到A对象了, 那么B对象就能顺理成章实例化出来, 那么A也能正常实例化出来了
7. Spring的@Transactional如何实现
- 通过动态代理来实现的
- 有事务调用没有事务的, 可以保证事务正常, 因为代理类有事务, 并且数据事务具有原子性, 调用代理前的方法,所以事务会生效
- 没有事务调用有事务的, 事务会失效, 因为生成的代理类没有开启事务, 再去调用原有方法, 也是没有事务的
8. Spring事务的传播属性
所谓Spring事务的传播属性, 就是定义在存在多个事务同事存在的时候, Spring应该如何处理这些行为
- Propagation.REQUIRED: 支持当前事务, 如果当前有事务, 加入当前事务, 如果当前没有事务, 则新建一个事务(默认)
- Propagation.NOT_SUPPORTED: 不支持事务, 如果当前有事务, 就挂起当前事务, 执行完之后再恢复事务
- Propagation.SUPPORTS: 如果当前有事务则加入, 没有就不用事务
- Propagation.MANDATORY: 支持当前事务, 当前没有事务就抛出异常
- PROPAGATION_NEVER: 非事务的方式执行, 如果当前有事务, 则抛出异常
- Propagation.REQUIRES_NEW: 支持当前事务, 如果当前有事务, 则挂起当前事务, 然后创建一个新事务, 如果当前没有事务, 则创建一个新事务
- Propagation.NESTED: 嵌套事务, 如果当前存在事务, 则嵌套在当前事务中, 如果当前没有事务, 则新建一个事务自己执行, 当内部事务回滚时不会影响外部事务提交, 外部事务回滚时会把内部事务也回滚
三. 消息队列的选择
1. RabbitMQ:
支持AMQP协议
优点:
-
轻量级, 部署容易
-
灵活自定义路由规则
-
跨平台, 多种语言
缺点:
- 出现消息堆积, 性能大大降低
- 相比RocketMQ, Kafka性能低, 官方测试是每秒处理几万到几十万的消息
2. RocketMQ
优点
-
提供了多种场景功能,比如:有序、事务、流计算等等
-
性能比RabbitMQ要好,每秒处理几十万的消息量
缺点
- 没有多租户
- 不支持多语言
3. Kafka
优点
-
默认是异步发送和接收消息的,所以性能很强,比RocketMQ和RabbitMQ都要好,如果是异步并且开启了消息压缩,那Kafka可以达到每秒两千万的消息处理量
-
性能佳,可伸缩,支持消息持久化、分区、副本和容错
缺点
- 扩容成本高:集群中新增的broker只会处理新topic,如果要分担老topic-partition的压力,需要手动迁移partition, 难度较大
- partition过多会使得性能显著下降:ZK压力大,broker上partition过多让磁盘顺序写几乎退化成随机写。
四、 Elasticsearch
是一种存储和管理基于文档和半结构化的数据库, 也可以说是搜索引擎, 提供实时搜索, 分析结构化和非结构化的数据
1. 倒排索引
- 就是“以内容的关键词”建立索引
- 计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
2. 概念
- 索引就是MySQL的数据库
- 字段表示列
- 文档就是一行数据
3. 用途
- 存储少修改, 不修改的数据
- 存日志
- 存流水信息
五、 MySQL
1. 四大特性
- 原子性:事务是最小的执行单位, 执行要么都成功, 要么都失败
- 持久性:事务对数据库的操作是不可逆的, 对数据库的修改会持久化到磁盘
- 隔离性:事务并发执行的时候, 事务与事务之间互不干扰
- 一致性:数据库在事务操作前和操作后状态是一致的, 从一个一致性到另一个一致性
- 比如转账, A有100, B有0元, A向B转了100, 事务提交之后, 他们的钱加起来还是100
2. MySQL事务隔离级别
MySQL默认的隔离级别是可重复度, ORACLE默认是读已提交
- 读未提交: 最低的隔离级别, 事务能读到其他事务未提交的数据, 不能避免脏读, 幻读, 不可重复读
- 读已提交: 允许读取其他事务提交的数据, 可避免脏读, 不可重读读, 避免不了幻读
- 可重复度: 同一条sql的查询语句查询出的结果一致, 可避免脏读, 不可重读读, 避免不了幻读
- 串行化: 最高的隔离级别, 所有事务串行执行, 完全杜绝事务之间的影响
3. 脏读, 幻读, 可重复度, 不可重读度
-
脏读: 一个事务读到另一个事务未提交的数据
-
幻读: 一个事务两次读取的结果不一致, 第二次读的过程中另一个事务添加了数据
-
不可重复度: 一个事务两次读取的结果不一致, 第二次读的过程中另一个事务修改了数据
-
可重复度: 同一条sql的查询语句查询出的结果一致, 可避免脏读, 不可重读读, 避免不了幻读
4. 什么时候选择MyISAM, 和innodb区别
-
大量select和insert操作是选择MyISAM
-
数据量大, 没有事务操作的时候选择MyISAM
-
大量update和insert时选择InnoDB
区别
- MyISAM不支持事务, InnoDB支持事务
- MyISAM是表级锁, InnoDB行锁
- MyISAM不支持外键, InnoDB支持外键
- MyISAM支持全文索引, InnoDB不支持
- MyISAM表的查询,更新,插入效率比InnoDB高
5. 为什么选择B+Tree做索引数据结构
- BTREE: 有序数组 + 平衡多叉树
- B+TREE: 有序数组 + 链表 + 平衡多叉树
- BTREE数据结构是平衡二叉树, 数据一直增多, 树的深度一直增加, 影响查询效率
- B+TREE的数据全部存放在叶子节点中,非叶子节点用来做索引, 叶子节点中有一个指针指向下一个叶子节点, 提高了区间访问的性能
- B+TREE是一个扁平的数据结构, 避免了很高的深度
- 数据库引用B+TREE是因为BTREE提高了磁盘I/O性能, 但没有解决元素遍历效率低下的问题
6. MySQL索引四种类型
- B+TREE, HASH, FULLTEXT(全文索引)和R-TREE
7. 建索引注意事项
- 越小的数据类型在磁盘, 内存,cpu缓存中需要更少的空间, 处理起来更快
- 整形比起字符处理开销更小
- 排序字段加上索引
8. B+Tree的叶子能存哪些数据
- 真实数据库数据
- 主键索引指针
9. order by原理
- 数据过滤完之后, 把符合条件的数据放到sort buffer中进行排序, 当sort buffer中空间不足的时候会用临时文件排序
- 为了尽量避免临时文件排序, MySQL会避免非必要的字段放到sort buffer中, 等排序完之后, 再将这个字段取出来
10. 建表注意事项
- 建表时候尽量不要让字段默认值为null, 因为复合索引只有一个字段为null, 那么这一列索引是无效的
- 设置好字段长度, 以免浪费空间, 并且后期做索引的话, 字段长度越短, 处理越快
- 合理添加冗余字段
- 主键建议int类型, 分布式用雪花算法
- 对于字段太多的表, 考虑拆表, (比如用户有很多附加属性, 后期附加属性, 可以拆表)
- 对于表中经常不被用的字段或者存储数据比较多的字段, 考虑拆表
11. SQL优化
-
尽量不要使用select *,只需查询所需字段, 增加了不必要的消耗, 可能会导致不走覆盖索引
-
尽量不要使用!=, <>, is null, or, not in
-
适当的调大一些sort_buffer_size做排序时候用的
-
模糊查询用右模糊
-
避免在where条件中做函数运算
-
建合适的索引
-
in包含的值不应过多
-
尽量用union代替union all
union all前提是两个结果集没有重复数据, 因为不去重
- union
- 对两个结果集进行并集操作
- 不包括重复行,相当于distinct
- 同时进行默认规则的排序;
- union all
- 对两个结果集进行并集操作
- 包括重复行, 即所有的结果全部显示
- 不排序
- union
-
查询数据过多, 用程序来分段查询, 避免扫描行书过多
-
避免使用隐式转换, 查询int类型字段, 条件中用引号括起来
-
联合所以遵循最左匹配原则
-
使用join代替子查询
12. MySQL索引失效
- 索引列存null值
- 查询条件使用is null
- 前模糊, 或者全模糊
- or条件不是索引列
- 联合索引没遵循最左匹配原则
13 select * 和select全字段, select count(*) 和select count(1), select count(列)
-
select * 可能会导致用不了覆盖索引
- 什么是覆盖索引: 表中有5个字段(1,2,3,4,5), 1,3,5联合索引, 查询结果就这三个字段, 即覆盖索引
-
同样的查询条件, 没有索引的表select * 和select全字段效率基本相等, 有索引读到索引数据后还是再读取其他数据
-
count(*) 和count(1)会统计为null的列, count(列)不会
14. MySQL 5 和MySQL 8区别
-
性能: MySQL 8相对于MySQL 5.7有更高的性能和更好的资源管理能力。
-
安全性: MySQL 8对安全性的增强比MySQL 5.7更好,包括更强大的密码验证和更好的访问控制。
-
新特性: MySQL 8带来了许多新特性和功能,多了很多编码格式的数据, 包括JSON数据类型、多语言支持等,
-
数据字典: MySQL 8使用数据字典方式存储数据,而MVSQL5.7使用的是表格方式,这是一个重要的区别
-
兼容性: MySQL 8相对于MySQL 5.7有更好的兼容性,可以兼容更多的操作系统和平台。
15. MVCC
一多版本种并发控制方法
为什么需要MVCC
-
数据库原生的锁
最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。 -
读写锁的出现
读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够 -
mvcc概念出现(mysql快照读)
读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务只看到自己特定版本的数据
16. MySQL集群
一般采用双主双从单写模式
17. Explain关注字段
- type: 依次排列 , 最优到最差 , system > const > eq_ref > ref > range > index > ALL
- 最少达到range
- index: 全表索引扫描
- all: 全表数据扫描
- key 用到的索引
- rows : 该列是MySQL估计要读取并检测的行数 , 注意这个不是结果集里的行数
- Extra列 这一列是额外信息
- Using index : 使用覆盖索引
- Using where : 使用where语句来处理结果 , 查询的列未被索引覆盖
- Using index condition : 查询的列 不完全被索引覆盖 , where条件是一个前导列的范围
- Using temporary : mysql需要创建一张临时表来处理查询 , 出现这种情况一般要进行优化 , 首先要考虑进行索引优化
- Using filesort : 将用外部排序而不是索引排序 , 数据较小时从内存中排序 , 否则需要在磁盘完成排序 , 这种情况一般也要考虑建索引来优化
- impossible where 说明数据库这个查询是不可能有结果输出的, 会走全表扫描
六、Redis
1. key淘汰策略,过期策略
- 淘汰策略:
- 默认淘汰策略: 当redis内存满之后, 新进来的新增请求直接抛异常.
- 设置了超时时间的key:
- 淘汰最近最少使用的
- 所有key中随机淘汰
- 淘汰将要失效的
- 未设置过期时间的
- 淘汰最近最少使用的
- 所有key中随机淘汰
2. 过期策略
- 定时删除: 设置了过期时间的key到了超时时间就删除
- 优点是节省了内存空间
- 缺点是同一时间大量key到期, 会占用很多的cpu资源去删除到期的key
- 惰性删除: 到了过期的时间不删除, 下次请求时再删除
- 优点: 节省了cpu资源占用
- 缺点: 大量不使用key存在内存会导致内存泄露
- 定期删除: 制定删除时间和删除频率, 减少内存占用和同一时间cpu占用
- 内存占用友好度不如定时删除, cpu占用友好度不如惰性删除
3. 持久化机制
- RDB: 到了规定的时候后, Redis会拉起一个新线程将内存中的数据存一个二进制快照
- 优点:
- RDB文件小, 非常适合做备份
- Redis加载RDB速度要比AOF文件速度快得多, RDB是备份时的内存数据, AOF是存储的是操作指令
- 缺点:
- RDB不能做到完全的实时化, 在备份的时候如果Redis宕机, 则会导致丢失开始备份时候的数据, 不适用于实时性要求较高的场景.
- 优点:
- AOF: 存储Redis的新增操作命令写的AOF可读日志文件
- 优点:
- AOF机制是追加日志文件, 对服务器性能占用较低, 比RDB快
- 缺点:
- AOF生成的日志文件太大, 需要一直重写瘦身(重写: 读取Redis中的全量数据写到AOF中, 重写这期间的数据会写到原有的AOF文件中, 当重写结束后, 期间加操作也写入新的AOF文件中, 再将新AOF文件重命名, 替换原有AOF文件)
- 比RDB文件大得多, 重启Redis恢复数据的时候也比RDB慢得多
- 优点:
4. 如何解决缓存击穿, 缓存雪崩, 缓存穿透
- 缓存穿透: 一直请求缓存中没有的数据
- 解决方法: 将请求的key的值设置为null, 设置个超时时间, 不要太长, 布隆过滤器
- 缓存击穿: 缓存中没有数据, 数据库中有, 并发大的情况下, 所有请求全部打到数据库, 会给数据库造成很大的压力
- 解决方法: 缓存预热 , 热点数据用不过期 , 加互斥锁, 只让一个用户请求数据库, 其他请求查缓存
- 缓存雪崩: 同一时间大批量key过期, 大量请求进来打到数据库, 数据库压力巨大, 还可能会导致宕机, 缓存击穿是同一条数据, 雪崩是不同数据同一时间过期
- 解决方法: 缓存设置随机的过期时间, 热点数据用不过期, (如果数据库是分布式部署, 将热点数据均匀分布)
5. Redis分布式锁逻辑未执行完锁到期怎么办
- Redission开启一个锁的同时后台开启一个看门狗锁, 每隔10s判断一次, 如果最后一次检查业务逻辑还未结束, 就给原来的锁续期
七、JUC
1. 进程和线程,并发和并行
- 进程:进程是计算机程序在一个数据集上的一次运行过程,也是资源分配和调度的基本单位
- 线程:线城是进程中的一个执行单元。一个进程中至少有一个线程
- 并行:指的是两个或多个事件在同一时刻发生,
- 并发:指的是两个或多个事件在同一时间间隔内发生
2.死锁以及产生死锁的四个条件
- 死锁是指多个进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
- 产生死锁的必要条件:1. 互斥条件 2. 请求与保持条件 3. 不剥夺条件 4. 循环等待条件
3. Java中线程的状态
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
- 阻塞(BLOCKED):表示线程阻塞于锁。
- 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
- 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕
4. sleep() 和wait(),sleep()和yield()有什么区别
- sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- sleep() 不释放锁;wait() 释放锁。
- sleep() 方法执行完成后,线程会自动苏醒;wait() 方法被调用后,线程不会自动苏醒,与notify()搭配使用。
- 首先yield () 也是 Thread线程类的静态方法,也是不释放锁。
- 线程执行 sleep()方法后转入阻塞(blocked)状态;而执行 yield()方法后转入就绪(ready)状态,因此接下来的获得执行权的线程可能是当前线程,也可能是其他线程。
- sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
5. notify()和notityAll()有什么区别
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
- notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
6. CAS, synchronized
6.1 CAS
- CAS,compare and swap的缩写,中文翻译成比较并交换
- CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作
6.2 CAS的缺点以及怎样解决ABA问题
- 缺点
1) 如果CAS失败就会一直进行尝试,即一直在自旋,导致CPU开销。
2) 只能一个共享变量的原子操作
3) 会产生ABA问题。 - ABA问题
如果一个线程在初次读取时的值为A,并且在准备赋值的时候检查该值仍然是A,但是可能在这两次操作之间,有另外一个线程现将变量的值改成了B,然后又将该值改回为A,那么CAS会误认为该变量没有变化过。 - 解决:添加版本号来解决
6.3 为什么使用CAS代替synchronized
- synchronized加锁,是一种悲观锁, 同一时间段只允许一个线程访问,能够保证一致性但是并发性下降。而是用CAS算法使用do-while不断判断而没有加锁(实际是一个自旋锁),保证一致性和并发性。
6.4 java多线程锁
- 公平锁:多个线程按照申请锁的顺序来获取锁,先来后到在等待队列中FIFO
- 非公平锁:多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的比先申请的有先获取,在高并发的情况下,有可能造成优先级翻转或饥饿现象
- 乐观锁:每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。基本通过CAS实现
- 悲观锁:每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁
- 可重入锁(递归锁):同一线程在外层函数获取锁以后,进入内层函数时自动获取锁.其作用是避免死锁。
- 自旋锁:获取不到锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,好处是减少上下文切换的消耗,坏处是消耗CPU
- 独占锁:每次只允许一个线程获取锁,如 synchronized和ReentrantLock
- 共享锁:允许多个线程同时获取锁,如读写锁(ReadWriteLock)
- 读写锁:读读共享 读写-写读-写写是独占
6.5 哪些场景需要额外注意线程安全问题?
- 访问公共资源
- 依赖时序操作(map的remove)
- 要使用没有声明线程是安全的对象
6.6 使用线程池比手动创建线程好在哪里?
- 灵活控制线程数量, 杜绝浪费资源
- 减少频繁新建线程, 销毁线程所需资源
6.7 线程池的有哪些参数各个参数的含义?
- corePoolSize 核心线程数
- maxPoolSize 最大线程数
- workQueue 工作队列
- keepAliveTime 闲置线程存活时长
- ThreadFactory 创建线程的工厂
- Handle 处理被拒绝的任务
6.8 阻塞队列定义以及种类
- 当队列是空的,从队列中获取元素的操作将会被阻塞当队列是满的,从队列中添加元素的操作将会被阻塞
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表组成的无界阻塞队列。
- LinkedBlockingDeque:由链表组成的双向阻塞队列。
6.9 常见线程池:
-
fixedThreadPool: 传统高效的线程池
-
scheduleThreadPool: 可以执行周期性任务的线程池
-
singleThreadPool: 单线程线程池
-
cacheThreadPool: 可缓存线程池
6.10 线程池具体工作流程
添加一个请求任务时,线程池会做出以下判断:
- 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务
- 如果正在运行的线程数量大于等于corePoolSize,会将该任务放入阻塞队列中
- 如果队列也满但是正在运行的线程数量小于maximumPoolSize,线程池会进行拓展,将线程池中的线程数拓展到最大线程数
- 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程空闲时间超过给定的keepAliveTime时,线程会做出判断:如果当前运行线程大于corePoolSize,那么该线程将会被停止。最终的线程数目会收缩到corePoolSize的大小
6.11 线程池的拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:将某些任务交给提交者处理
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案
6.12 synchronized和ReentrantLock的区别
- synchronized是关键字,ReentrantLock是类
- synchronized不需要用户手动释放锁,执行完会系统会自动释放对锁的占用
- ReentrantLock需要用户手动释放锁。否则容易出现死锁现象
- synchronized不可以中断,除非抛出异常或者正常完成;ReentrantLock可以被中断
- synchronized是非公平锁
- ReentrantLock默认是非公平锁,但是构造方法可以传入boolean值,true为公平,false为非公平
6.13 ThreadLocal
- ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量,线程之间互不干涉,保证了数据安全,是一种以空间换时间的做法。在每个线程中都创建了一个 ThreadLocalMap 对象,ThreadLocalMap 中使用的 key 为 ThreadLocal,而 value 则是变量的值。在不同线程中,访问同一个ThreadLocal的set和get方法,它们对ThreadLocal的读、写操作仅限于各自线程的ThreadLocalMap,从而使ThreadLocal可以在多个线程中互不干扰地存储和修改数据。
6.14 ThreadLocal造成内存泄漏的原因与解决方案
- ThreadLocal中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后最好手动调用remove()方法
6.15 并发编程三个必要因素
- 原子性:原子,即一个不可再被分割的颗粒。原子性指的是一个或多个操作要么全部执行成功要么全部执行失败。
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,如synchronized,volatile
- 有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
八、Tomcat, Jetty, Undertow
1. 区别
- Jetty和Undertow都是基于NIO实现的高并发轻量级服务
- Tomcat是一款重量级服务器( Tomcat6以后支持NIO), Jetty和Undertow相比Tomcat轻量化
- 负载不高的情况下, 三款容器的负载能力差不多
- 负载较高的情况下, Jetty负载能力最差, Undertow强于Tomcat
2. 什么是NIO (非阻塞式输入输出)
- NIO是采用内存映射的方式处理输入输出, NIO是将文件映射到内存中, 这样就可以像访问内存一样访问文件, 传统的BIO(Blocking IO)会阻塞
- NIO和BIO的目的是一样, 但是使用方式不同, NIO是面向缓冲区, 基于通道的IO, 效率要高得多
九、JVM
1. 内存结构
JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面, class 类信息常量池(static 常量和 static 变量)等放在方法区
- 方法区: 线程共享 主要存储类信息、常量、静态变量、即时编译器编译后的代码
- 堆: 线程共享 初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要 在堆上分配
- 虚拟机栈: 线程私有 存储局部变量表、对象指针、操作栈、动态链接、方法出口
- 本地方法栈: 线程私有 主要为 Native 方法服务
- 程序计数器: 线程私有 记录当前线程执行的行号, 指向下一条要执行的指令
2. 堆里面的分区:Eden,survival (from+ to),老年代,各自的特点
- 堆里面分为新生代和老生代(java8 取消了永久代,叫元空间)
- 新生代包 含 Eden+Survivor 区
- survivor 区里面分为 from 和 to 区
- 触发youngGC时, 存活下来的对象会从from区复制到to区,然后from区和to区角色互换, 当经过一次或者多次 GC 之后,hotspot虚拟机是15次(由JVM参赛MaxTenuringThreshold决定的,默认值就是15), 存活下来的对象会被移动 到老年代
- 当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区
- 如果有特别大的对象,新生代放不下, 就会使用老年代的担保,直接放到老年代里面。因为 JVM 认为,一般大对象的存 活时间一般比较久远。
3. 新生代和老年代占用堆区比例
1: 2
4. 新生代中的伊甸园区和幸存者0区、幸存者1区的占比
8:1:1
5. GC的三种方法
复制算法 - 新生代
- youngGC
标记清除 - 老年代
- 先标记,标记完毕之后再清除,效率不高,会产生碎片
标记整理 - 老年代
- 标记完毕之后,让所有存活的对象向一端移动
现在一般都采用分代收集算法
- 新生代回回收大批量对象, 少量存活, 用复制算法, 复制少量对象就能完成新生代收集
- 老年代存活率高, 就用标记整理, 标记清除算法来收集
6. Java类加载过程
类的加载过程又分为三个步骤
-
类的装载(Loading)
将类的class文件读入内存,并为之创建一个java.lang.Class的实例对象,此过程由类加载器(负责类的加载,对应一个Class实例)完成。 -
链接(Linking)
验证(Verify):确保加载的类的信息符合JVM规范,例如:每一个class文件都以cafebabe开头,没有安全方面的问题。
准备(Prepare):正式为类中的(static)静态变量分配内存,并设置默认初始化值的阶段。这些内存都在方法区中进行分配。
解析(Resolve):虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。 -
初始化(initialization)
执行类构造器方法的过程。执行所有类中(static)静态变量和(static)静态代码块中的语句的赋值动作,这些操作都在方法中进行。
因为类的加载过程中还没有对象的存在,因而赋值操作也只能是对静态变量进行
7. 类加载机制
- 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型
8. 类加载器双亲委派模型机制
- 当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类
去加载,如果此时父类不能加载,再交给子类去完成类的加载
9. 类加载器有哪些
- 启动类加载器(Bootstrap ClassLoader)用来加载 java 核心类库,JRE中的rt.jar
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。JRE中的ext目录下的JAR包
- 系统类加载器(system class loader): 也成为应用程序加载器, 它根据 Java 应用的类路径(CLASSPATH) 来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
- 用户自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现。
十、分布式事务
1. seata
-
工作流程
- Business(业务入口)会通过注解说明这是一个全局事务, TM(事务管理器)
- Business会告诉TC(seata服务), 要开启一个全局事务, TC会生成一个XID返回给Business
- Business拿到XID后开始调用微服务
- 被调用的微服务称为资源管理者(RM), RM会在undo_log中记录自己的操作情况, 也会告诉TM执行结果
- Business收到各个微服务的结果后 , 请求TC这个XID要提交, 否则回滚
- TC收到请求后, 向XID下的所有分支事务发起相应的请求
- 各个微服务收到TC的请求后,执行相应指令,并把执行结果上报给TC
-
全局事务回滚时怎样实现的
- undo_log会记录修改前的数据, 如果需要回滚会根据undo_log中的数据组装sql执行, 提交则删除undo_log
-
RM是怎样和TC通信的
- 通过拦截JDBC, 监控到开启了本地事务, 就会自动向TC注册, 生成回滚日志, 想TC汇报执行结果
-
二阶段回滚失败会怎样
- 如果TC向该XID下的各个分支事务发送回滚的指令时有一个微服务挂了, 那么所有正常的微服务也不会回滚, 当这个微服务再次启动后, TC会再发送回滚指令
-
核心组件
- 事务协调器 TC
- 事务管理者 TM
- 资源管理者 RM
十一、Dubbo
配置优先级:
- 方法级优先 ,接口级次之, 全局配置再次之
- 如果级别一样, 则消费方优先, 提供方次之
- 注册中心宕机了, 提供者和调用方之前有掉用过可通过本地缓存调用
- 提供者全部宕机, 消费者也不能使用了
- 注册中心集群, 任意宕机一台, 将自动切换到另一台
DubboReference可以直接配置服务提供者地址, 绕过注册中心
Dubbo负载均衡策略
- 基于权重随机
- 基于权重轮询
- 最少活跃数 (看上一次哪个节点处理得快)
- 一致性hash(根据参数hash选择节点, 参数一样, 一直调用同一个服务)
RPC工作原理
- client stub接收到请求后, 将方法, 请求封装成网络传输消息体
- 将请求通过sockets发送给服务端
- server stub收到消息后解码, 根据解码信息调用本地服务
- server stub将结果封装成网络传输消息体通过sockets发送给调用方
- client stub接收到消息, 并进行解码得到最终结果