Java知识点汇-黄药师
2024-01-10 20:30:49 0 举报
AI智能生成
知识点
作者其他创作
大纲/内容
SpringBoot
spring 两个beanid一样会发生什么?
什么是BeanID重复:
两个Bean类名一致包名不一致,或者以@Bean注解声明两个Bean的方法名一致时。这两类情况Bean的ID就会出现一致的情况。
出现什么问题:
该情况下如果直接注入就会导致启动异常,一般会提示需要一个SingleBean但是找到了多个。
解决办法:
1.声明Bean的时候起别名,注入时使用@Qualifier指定Bean
2.声明Bean时使用@Primary标记哪个为主要的Bean
3.最好的方法还是规避
Bean的生命周期
BeanDefinition
Spring如何解决循环依赖问题
Spring的循环依赖已经是一个老生常谈的问题了,那我先解释一下什么循环依赖。
有这么几种情况,例如1.A依赖B,B也依赖A或者2.A依赖B,B依赖C,C又依赖A或者3.A依赖A。这些情况呢都属于循环依赖。
解决单例情况下的循环依赖呢,Spring使用到了三级缓存,核心思想实际上就是将Bean的实例化和属性注入进行分离。
其中一级缓存(SingletonObject)用于存储完整的Bean,这里的Bean是可以直接使用的。
二级缓存(EarlySingletonObject)呢用于存储半成品Bean,在二级缓存中的Bean已经实例化完成了,但是属性的依赖注入还没有完成。
三级缓存(SingletonFactory)主要是Bean工厂,用于当在一级缓存找不到Bean时候去生成原始Bean,并放入二级缓存。
三级缓存解决代理问题?
Bean的注入方式有哪些?
最常用的是基于属性注入@Autowride
基于Setter注入,在set方法上标记@Autowride
基于构造器注入(Spring官方推荐)
避免循环依赖
注入的属性可以用final修饰,不可变,保证安全
非IOC环境下可以new该对象
怎么判断一个对象是不是Bean?
注入ApplicationContext使用其getBean方法即可判断目标对象是不是Bean。
@Primary和@Qualifier作用
当使用接口注入时,如果该接口存在多个实现,此时就需要使用@Primary或者@Qualifier协助注入,否则会因为有多个实现导致注入失败
@Primary标记在Bean上,表示存在多个满足注入条件的Bean时,优先注入当前Bean
@Qualifier与@Autowired配合使用,用于注入指定的Bean
此外可以通过@ConditionalOnProperty指定符合配置文件条件的Bean进行注入,比如区分pord和dev进行数据源的注入
https://blog.csdn.net/niugang0920/article/details/115701528
Spring的Bean是线程安全的吗?如果不安全怎么处理?怎么保证并发安全?
SpringBean不是绝对意义上的安全或者不安全,需要根据具体的场景来分析,首先SpringBean根据作用域分为Singleton单例模式和Prototype原型模式。
我们用的比较多是Singleton模式,在这种情况下大多数的Bean都是无状态的,例如被@Controller、@Service、@Component、@Configuration标记的Bean,基本上都是无状态的,他们也不存在实例变量的存储,线程操作基本上也都是查询,那么就不会存在线程安全问题
https://www.apispace.com/news/post/53244.html
但是有些情况下我们会自定义的去声明一些单例Bean,他会是有状态的,里面包含一些实例变量操作,由于单例Bean全局独一份,并发环境下就会涉及到资源共享,也就会出现线程安全问题
对于这种有状态单例Bean的线程安全问题,一般我们会在设计之初就去规避它,尽量采用一些参数的形式去取代Bean里面的变量操作。
如果必须要使用变量的话,可能会用ThreadLocal去修饰这个变量,这样线程都玩自己的副本,这样去规避安全性问题。
另外还有个极端的办法是去加锁,但是已经单例了牺牲性能去保证安全有点得不偿失,这种情况下我会选择改用Prototype的Bean。
此外在WEB环境下可以控制好Bean的作用域粒度,根据实际场景选择是请求也好、会话也好给线程创建自己的Bean实例,从而去规避线程安全问题。
BeanFactory和FactoryBean的区别
BeanFactory是IOC容器的一套规范接口,他定义了获取Bean 的一系列方法,包括通过BeanID、类型、Class等方式去获取Bean,还提供了一些判断是否是单例、多例、Bean是否属于某个类型。Spring本身提供的默认实现也比较多,比如ApplicationContext接口就继承了他,还有DefaultListableBeanFactory等等吧。总的来说他就是一个工厂接口。
https://zhuanlan.zhihu.com/p/124119677
FactoryBean是一个特殊的Bean,通过实现它能够在使用xml方式定义Bean 的时候不用写一大堆属性Property标签设置,可以通过他的接口方法getObject去实现属性的设置,更灵活一些。
但是现在基本上不怎么使用了,目前我们都使用@Bean的方式取代了XML的方式。
IOC的流程和对DI的理解
IOC(Inversion Of Control)控制反转,一句话来说就是将对象创建的权利交由了Spring,而不需要我们手动的去维护,自然对象的生命周期也由Spring去管理。
举例来说就比如说咱们现在在做的事情,我想要找一份工作,搁以前我可能需要自己去线下调查市面上的公司,去看他们的招聘需求和我是否匹配,待遇我是否能接受,还得自己去约时间线下交简历,很复杂,我和各个公司间的耦合度也很高,而现在有了线上的这些招聘平台,我只需要填写我的需求和基本信息,他就能为我推荐匹配的公司,这就是IOC的思想。反过来您作为招聘方也是一样,这些招聘平台就是IOC容器Plus,他既维护着公司想要的求职者对象,也维护者求职者想要的公司对象。
https://www.zhihu.com/question/313785621/answer/637953117
DI(Dependency Injection)依赖注入,和字面理解是一样的,例如说当一个对象需要数据库的Connection时,DI可以帮助去注入Connection,至于怎么去构造的,对这个对象来说是不可见的,他也无需关心。
DI的实现方式其实也很简单就是反射。
@Transactional事务
Spring提供的事务处理机制,需要配合使用@EnableTransactionManagement启用事务
@Transaction什么时候会失效?
1.没有启动事务注解@EnableTransactionManagement,如果使用了Springboot框架的话默认是会启用事务的。
https://zhuanlan.zhihu.com/p/378172451
2.标记了事务方法的异常在方法内被捕获了
3.默认情况下@Transaction捕获Error和RuntimeException异常,所以如果抛出了一些自定义异常,比如是继承了Exception的异常,事务回滚也不会生效
4.@Transaction事务传播定义为了不支持事务Propagation=NOT_SUPPORTED或者NEVER此时事务不生效
5.@Transaction的Propagation=SUPPORTS,并且当前没有事务时,也是不生效的,除非前面有事务方法调用,则加入事务
6.外部方法调用A,A调用内部带有事务的B,B抛异常,此时事务不会回滚,因为B此时没有被AOP增强,只是当做一个普通方法调用
7.@Transaction方法A被标记为REQUIRED,方法B被标记为REQUIRED_NEW,A调用B,B的事务没有回滚,A回滚了
9.数据库本身不支持事务
10.方法内新起一个线程去操作数据库,事务失效
Spring 中Bean的作用域有哪些
5种,Singleton、Prototype、Request、Session、application(通常与Singleton作用一致,除存在多个IOC容器的情况下,application仍能保证全局独一份)
@Resource和@Autowired的区别
@Resource默认通过Name注入,@Autowired默认通过Type注入,找不到再通过Name注入,再找不到抛NoUniqueBeanDefinitionException
SpringMVC的理解
SpringMVC就是Model 模型 View视图 Controller 控制器 .
早期 是 用户请求到 jsp 再到javaBean 再到数据库,这样的话,你不仅需要后端厉害,也还是需要前端厉害,而且你代码写好了,很不好测试,需要都写完了,才可以进行测试,代码也很难进行复用.
有很多缺点, 必然会被更优秀的方法取代, 这时候一个牛皮的东西就出现了, 这就是servlet + jsp +javaBean ,这样就是HTML和业务逻辑进行分离了,有一定的优势,
就是用户的请求到了服务器,servlet把请求发到 javaBean, 把数据得到了,再给jsp展示, 这样的就是MVC模式
这里的 M就表示的模型,模型是啥?模型就是数据, 就是dao 和bean
V表示的视图, 视图是啥?简单的说就是网页, 是用来展示数据的,
C就是控制器, 控制器是什么?控制器的作用就是把不同的数据,显示在不同的视图上,就是显示在不用的页面上面, 其实Servler扮演的就是这样的角色
为了解决持久层里一直没有解决的事务问题, 还有Nosql的强势崛起,大哥spring强势整合了springMVC,解耦,实用性高!
用户的请求到达 DispatchaerServlrt 这是一个servlet,众所周知servlet是用来拦截http请求的, DispatcherServlet也不例外,DispatcherServlet拦截http请求,发送给SpringMVC的控制器,但是SpringMvc的控制器有很多,我们如何确定发送到哪一个控制器呢? 这个时候就是需要处理器映射器闪亮登场, 处理器映射器会根据请求里面的URl信息进行对控制器的匹配,匹配到正确的控制器, 控制器的就会执行我们程序员写代码获得到数据,这些数据就是模型, 当然我们这些模型数据,仅仅只是原始的数据,我们还需要友好的进行格式化,一般的都是Html, 所以这些原始信息需要发给一个视图, 通常是jsp,
这时候控制器就会做最后一件事,就是将模型数进行打包, 和需要渲染输出的逻辑视图名一起返回给DispatcherServlet,. 即使我们代码里的 return 视图名 这样把模型数据和逻辑视图名返回给DispatcherServlet控制器, 控制器既不会和特定的视图耦合,而且视图名也不是表示某个特定的jsp. 它仅仅传递到的是一个名称, 是用来查找真正的视图的, 这个视图就形式我们写在Resoucer下面的,jsp文件.
既然DispatchaerServlet已经知道哪个视图来渲染结果了, 那么请求的任务也就是基本完成了,
但是我们还是需要进行视图的实现, 视图将模型数据渲染出结果, 这个结果会通过响应对象传递给客户端
Spring Boot中自动装配机制
自动装配实际上就是把我们常用到的且是当前工程需要的一些组件在Spring容器构建的时候就已经为我们注入好了,它是由一个叫做AutoConfigure的包实现的,在这个包里面有一个Spring.factories,这个配置文件中预置了小两百种的组件,比如我们常用的redis、还有Transaction事务都在其中。只要我们引入了相关依赖,并且在application.yml中写好用到的组件的配置,就能通过各种Template类操作组件了。实际上是一种SPI的思想。
SpringBoot中自动装配会自动加载所有组件吗?
不会,只会加载我们需要的组件。例如如果我们没有引入redis-starter,那么他就不会加载redis,它是通过@ConditionOnClass({RedisOperations.class})注解实现的,只有当前环境下有RedisOperations.class的字节码他才会加载Redis的配置类。
@Component和@Bean的区别
1.用法不同
@Component用于标记类,被标记后通过@ComponentScan扫描后由Spring装配到IOC容器中
@Bean则是在配置类中标记方法,方法返回值对象装配到IOC容器中
2.灵活度不同
@Bean更为灵活,允许开发人员手动控制Bean的创建和配置过程
@Configuration的proxyBeanMethods作用
主要用于@Configuration配置类中的Bean相互调用时,是否走IOC。如果Bean之间没有依赖,建议使用false,这样可以提高性能。
默认为True走IOC,false为不走IOC直接调用
Full全模式和Lite轻量级模式
@Configuration参数proxyBeanMethods:
Full 全模式(默认):@Configuration(proxyBeanMethods = true)
同一配置类下,当直接调用@Bean修饰的方法注入的对象,则调用该方法会被代理,从ioc容器中取bean实列,所以实列是一样的。即单实例对象,在该模式下SpringBoot每次启动都会判断检查容器中是否存在该组件
Lite 轻量级模式:@Configuration(proxyBeanMethods = false)
同一配置类下,当直接调用@Bean修饰的方法注入的对象,则调用该方法不会被代理,相当于直接调用一个普通方法,会有构造方法,但是没有bean的生命周期,返回的是不同的实例。
注:proxyBeanMethods 是为了让使用@Bean注解的方法被代理。而不是@Bean的单例多例的设置参数。
@Configuration(proxyBeanMethods = false)
public class AppConfig {
//放一份myBean到ioc容器
@Bean
public Mybean myBean() {
return new Mybean();
}
//放一份yourBean到ioc容器
@Bean
public YourBean yourBean() {
System.out.println("==========");
//注意:@Configuration(proxyBeanMethods = false):myBean()方法不代理,直接调用
//注意:@Configuration(proxyBeanMethods = true):myBean()方法代理,从ioc容器拿
return new YourBean(myBean());
}
}
过滤器和拦截器有什么区别
都是WEB应用下的常用的拦截请求的工具
1.运行顺序不同
Filter>Servlet>Interceptor>Controller
Filter是在Servlet容器接收到请求之后,但在处理请求之前运行的;
Interceptor是在Servlet被调用之后,但响应被发到客户端之前运行的;
2.处理的对象不同
Filter只能处理Request和Response;Interceptor可以处理Request、Response、Handler、ModelAndView、Exception
3.Filter依赖Servlet,而Interceptor不依赖;
https://zhuanlan.zhihu.com/p/535943668
Spring监听器及事件的原理和作用
是什么:Spring的监听实际上就是发布订阅设计模式的一种实现
怎么用:1.需要定义具体的事件类,继承ApplicationEvent2.定义监听者,两种方式1是使用注解@ListenerEvent,2是监听类实现ApplicationListener接口,3是注入事件发布者ApplicationEventPublisher,在业务方法中发布事件。
这里监听者的方法也是通过反射调用的。
此外事件可以通过@Order控制触发顺序。
Spring的事件机制默认是同步的,所以多个Listener会出现阻塞,可以使用@Sync异步处理
实际场景:用户注册成功后发送短信通知、或者邮件通知等。
@Sync可以指定线程池,比如专门指定一个处理事件的线程池
https://zhuanlan.zhihu.com/p/312061401
Spring各个上下文的作用
AnnotationConfigApplicationContext、ClassPathXmlApplicationContext、FileSystemXmlApplicationContext等
application.yml和bootstrap.yml的区别
SpringCloud
各个组件介绍
首先微服务会存在众多服务,那么这些服务都需要统一的注册管理起来,那第一个组件就是这个注册中心,选型有Eureka、Nacos,通过这个注册中心呢也方便管理服务的上线、下线
当我们需要调用这些服务的时候呢,就需要从注册中心中拿到具体的服务列表,因为涉及到高可用可能一类服务有多个都注册到注册中心了,所以第二个呢,需要有一个分发策略,我们就引入了Ribbon、feign这样的组件,可以做负载均衡
第三呢,为了保证我们整个微服务的健壮性,可以引用hystrix这样的组件,他提供了一些熔断、降级和限流这样的能力
第四个呢,就是网关像Gateway,主要是由于这个后端服务越来越多,一个服务就有一个地址,如果将这些服务都暴露给前端,前端会非常的不好管理。那引用网关呢就可以统一暴露一个网关地址给前端,只需要根据配置规则网关就可以路由到具体的服务上,另外呢网关还可以做统一的鉴权,这样各个服务的职责就更加分明,只需要关注自己的业务逻辑即可
鉴权是如何做的
用的是RBAC(RoleBaseAccessControl)也基于角色的访问控制,主要涉及到用户、角色、权限三个实体,用户和角色多对多,角色和权限也是多对多的关系。
一般配合权限框架才做权限控制
前后端分离用Shiro比较多,微服务用SpringScurity
使用Shiro时不是所有的知道url的都可以访问,在shiro中可以配置URL过滤规则,一部分可以直接放行,比如静态资源或者登录接口,其他都需要进行鉴权,根据用户的角色判断是否有对应url的权限
jwt
jwt是微服务用的,是这样一种机制,原来我们的校验是在登录完成后直接返回消息,jwt呢会把用户信息、权限信息缓存起来,这样的话下次同一个用户来访问的话就可以直接通过用户的token来找到对应的jwt信息,直接解析jwt权限数据就可以了,不需要查库了
第五呢,由于服务众多,配置的管理会很复杂,如果都采用本地管理配置的话,服务多了运维成本会非常非常的高,而且效率也会很低,这个时候我们就引入了配置中心,像Nacos、Apollo,好处是能统一管理配置、动态刷新配置等等吧
PaaS
DB
SQL优化
SQL优化的技巧有非常多,最常见的为了提升查询效率,在热点字段上加索引。热点组合查询可以创建组合组合索引,但需要满足最左原则
再比如在分页查询时如果遇到深度分页,由于回表次数的增加,查询效率变得极低,这时候就需要优化原有的分页SQL了。
举个例子:select name,id,xx from A limit 1,30;此时效率很高。但当深度分页时查询从第10w条往后的30条数据,查询效率极度下滑,此时可以将语句变为:select xx from A where id > 100000 limit 30;(如果不是ID自增也可以通过子查询找到具体从哪个ID开始)这样效率就上来了,之前有个查历史交易的功能,深度分页4k多毫秒,用这个方法就变成了20几毫秒。
原因:limit语句会先扫描offset+n行,然后再丢弃掉前offset行,返回后n行数据。也就是说limit 100000,10,就会扫描100010行,而limit 0,10,只扫描10行。这里需要回表100010次,大量的时间都在回表这个上面。
方案核心思路: 能不能事先知道要从哪个主键ID开始,减少回表的次数
https://zhuanlan.zhihu.com/p/554479313
避免使用索引失效的语句
1.例如like模糊匹配时使用左模糊和全模糊
2.使用or时有一个条件没有索引(因为or是并集,需要所有条件都有索引)
3.某些数据库,如mysql8.0之前in和not in不走索引
4.字段类型和sql类型不匹配,比如varchar类型使用了int的sql,数据库会做隐式转换,导致索引失效
5.使用函数也会使索引失效
6.复合索引下需要满足最左原则,否则不走索引
7.尽量不用union,因为会全表扫去重且排序 不走索引,数据量大的时候效率很低,需要去重的场景可以用代码实现
8.in和exists需要根据外侧主表和条件数量多少来使用,例如主表多条件很少用in,反之exists
9.如果需要清表,不用delte而是truncate,truncate会删表再重建表,而delete不会清除水位线
10.尽量使用批量操作,比如insert多条数据,可以合并插入,减少与数据库IO
11.无论是分组还是多表关联,如果可以的话优先使用where过滤掉不适用的数据
12.char是定长的效率要高于varchar,所以在一些特定的场景使用char要优于varchar,如身份证
13.避免使用select *
复核索引下,最左原则测试:https://zhuanlan.zhihu.com/p/40777145
Mysql下开启索引下推功能,减少回表次数
Mysql的InnoDB索引构成:
所有数据都是存在聚簇索引中,一个表有且只有一个聚簇索引,但是可以有多个非聚簇索引;
回表:
当基于非聚簇索引(辅助索引)查询数据时,如果返回的列不满足需求时,需要从聚簇索引中去获取,这个过程就成为回表
索引覆盖:
当从二级索引中查到的数据已经满足了需求不需要回表,直接返回数据即可,这就是索引覆盖
数据库优化
区分场景
读少写少
一个主备保证高可用即可
https://zhuanlan.zhihu.com/p/61897928
读多写少
硬件上上SSD增加读写性能,索引、SQL优化、引入Redis缓存,外加读写分离
读少写多
写场景比较多,就对存储要求高,可以分库分表,像一些历史流水数据就按奇偶年AB表拆分
垂直分表:一张表的字段分成两张表去存,带来的问题是冗余以及查询次数增加
水平分表:按照某一维度如日期、条数进行水平分割,跨表操作相对较少
分久必合合久必分,大肆垂直分表后期业务复杂度会越来越高,必要还需要又合并数据宽表
分库的时候如果是Oracle还可以使用同义词
读多写多
上面说的都得上..
总之没有必要一上就考虑分库分表、读写分离 集群啥的,设计好先根据业务优化一些慢查询 > 建索引 > 引入缓存 > 上硬盘 > 集群
更换存储技术,比如数据量大、模糊查询场景多,就上Elasticsearch或者solr
上云PaaS,关注数据库设计和业务本身优化即可
表设计
表设计一般遵循第三范式
表名字段名要见名知意
表名根据业务含义或者模块设置前缀
数据类型长度定义的要合理,不能出现手机号使用varchar(2000)这样的设计
必要时的冗余设计可以减少数据表关联
存储过程、函数、触发器
连接池
Druid
高阶玩法解析SQL
c3p0
JNDI
应用程序与数据库连接进行解耦,一般用于应用中间件,weblogic、webSphere、Tomcat
MySQL索引在什么情况下会失效
1.例如like模糊匹配时使用左模糊和全模糊
2.使用or时有一个条件没有索引(因为or是并集,需要所有条件都有索引)
3.某些数据库,如mysql8.0之前in和not in不走索引
4.字段类型和sql类型不匹配,比如varchar类型使用了int的sql,数据库会做隐式转换,导致索引失效
5.使用函数也会使索引失效
6.复合索引下需要满足最左原则,否则不走索引
7.尽量不用union,因为会全表扫去重且排序 不走索引,数据量大的时候效率很低,需要去重的场景可以用代码实现
MySQL的ACID
Mysql默认的InnoDB引擎实现了ACID:
原子性(Atomicity)使用事务日志和回滚实现
一致性(Consistency)使用外键约束实现
隔离性(Isolation)支持多种隔离级别:
1读已提交2可重复读(默认)3串行化,这些隔离级别控制了对事务对其他事务的影响,从而保证隔离性
持久性(Durability)采用redo日志记录所有的修改操作,以便在系统崩溃后进行数据的恢复;采用binlog日志记录所有的事务操作,以便在主从复制场景下进行数据同步。
Mysql的事务传播特性
有7种事务传播特性
REQUIRED(有事务就加入,没有则创建)、REQUIRE_NEW(永远新建事务)、
NESTED(有事务就嵌套到当前事务执行,没有新建,类似REQUIRED)
SUPPORTS(有事务加入,没则普通方式执行)
NOT_SUPPORTED(不支持事务,当前有事务的话挂起当前事务)
MANDATORY(强制有事务,没有报错)
NEVER(不支持事务,当前有事务则报错)
Spring中也是7种事务,默认REQUIRED
MySQL如何让解决幻读问题
幻读指的是在Mysql的默认隔离级别RR可重复读下,A事务在读取某一范围数据行时,B事务插入了新的符合条件的数据行,导致A事务再次读取数据行时出现了新的幻影行。
InnoDB下使用MVCC(Multi Version Concurrency Controller)多版本并发控制解决该问题,MVCC通过乐观锁的机制,给不同的事务生成不同的快照版本,然后通过undo的版本链进行管理,在MVCC中规定了高版本可以看到低版本数据变更,低版本看不到高版本的数据变更,从而实现不同事务间的数据隔离;从而解决了幻读问题。但是如果存在当前读的情况还是会有幻读的现象,因为读的内存数据跳过了快照读。
解决办法就是尽量避免当前读,或者是引用LBCC的方式加锁来实现
MySQL的二段提交原理
InnoDB的在处理事务时,使用了二段式提交,将日志写入和日志提交分开,确保redoLog(事务日志)和binlog(逻辑变更日志)写入的数据一致性,第一阶段是prepare阶段,这个阶段Mysql会将事务操作记录到redolog中,并标记prepare状态;第二阶段是commit阶段,当事务提交时会把事务操作记录到binlog中,并将redolog的状态置为commit
MVCC是什么?
过程中会加锁吗?
MVCC(MultiVersionConcurrencyController)多版本并发控制,一句话来就就是通过多版本来实现高并发。多版本指的是多个快照,并发指的是读与写操作之间的并发。
MVCC只在Mysql的InnocDB引擎下的读已提交和可重复读两个隔离级别下生效,因为读未提交每次都能读到最新的数据,可串行化是加锁设计
MVCC是基于乐观锁实现。每个事物都读自己版本的快照。
Mysql为什么使用B+Tree做为索引结构
1. 高效的范围查询:B+树的叶子节点是按照顺序链接的,所以它的范围查询非常高效。在B+树上进行范围查询时,只需要遍历叶子节点上的连续数据即可,而不需要遍历整个索引树。
2. 适应磁盘IO:B+树的节点大小通常和磁盘页大小相等,这样可以最大程度地减少磁盘IO次数。B+树的层级较低,树的高度相对较小,这进一步减少了磁盘IO的次数。
3. 可以支持有序性查询:B+树的叶子节点是按照顺序链接的,这使得B+树非常适合支持有序性查询。对于需要按照索引顺序进行查询的场景,B+树可以非常高效地处理。
4. 支持高效的插入和删除操作:B+树的插入和删除操作相对较为简单高效。插入操作只需要找到合适的位置并进行插入,删除操作只需要删除对应的节点即可。相比于其他树结构,B+树的插入和删除操作所需的平均IO次数更少。
5. 支持多级索引:B+树的内部节点除了存储键值对外,还可以存储其他节点的指针,这使得B+树可以支持多级索引。多级索引可以减小索引的大小,提高索引的效率。
综上所述,MySQL采用B+树作为索引是为了提高查询效率、适应磁盘IO、支持有序性查询、支持高效的插入和删除操作以及支持多级索引等方面的考虑。
limit 1000000,10 加载很慢该怎么优化(慢查询)
1如果数据的ID是连续的,直接筛选出100000条后,再limit 10即可
2通过子查询中order by 主键列筛选,再limit 10000,10
3业务上限制页数,极少有场景需要翻超过100页
limit 500000,10和limit 10速度一样快吗
不管是走索引还是优化sql,limit10肯定快于limit50w,一般会针对深度分页做一些优化
(见上)
为什么数据库字段建议设置为NOT NULL
1.数据完整性,比如用户表中用户名设置为not null
2.减少代码层面空值的判断
如何做到行转列
Mysql:case when
https://zhuanlan.zhihu.com/p/421396929
SQLServer:Pivot
行/列级授权
基于视图实现
慢查询
方言(dialect)
例如SpringBoot中使用OceanBase时limt无法使用,可以配置其使用oracle方言
https://www.zhihu.com/question/504223609/answer/2602773876
Redis
Redis支持的数据类型
String
https://www.zhihu.com/question/483702040/answer/2939448367
Set
无序不重复
SortSet
有序Set,可以用作排行榜、最新消息列表
List
有序可重复,可用作消息队列
Hash
可存储对象,用于较复杂的对象查询操作场景
BitMap
2^32次方个Bit位,可以用于签到打卡或者大数据量的统计
Bloom过滤器(需要安装插件)
bf.add
GEO
用于存储地理位置信息,查询相邻的经纬度范围
Stream
主要解决消息队列无法持久化以及对离线消费者重连无法读取历史消息的问题
解决消息无法重复消费的问题
HyperLogLog
统计一个集合中不重复的元素个数(不精确但占用空间固定且小)
HyperLogLog 的统计规则是基于概率完成的,而不是准确统计,标准误算率约 0.81%。
每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间
Redis的Key过期但内存没有释放的原因
Redis的Key在过期之后会逻辑删除,但是由于Redis采用定期删除和惰性删除的机制,Key过期之后内存不会第一时间被释放,只有在定期扫描时随机扫到了过期的Key或者惰性删除机制下访问过期的Key时才会释放空间
Redis中的大Key怎么处理
首先如果出现大Key 的情况时,影响还是很大的。1.内存的占用,使得Redis的内存压力增大
2.网络传输的延迟,影响性能
3.持久化备份需要更多的空间和时间
解决办法:
1.将大Key进行拆分,比如将一个大的Hash拆分成多个小的Hash
2.搭建RedisCluster进行分片存储,使用Redis分片机制,将大Key分散存储在都个HashSlot哈希槽上,减轻单实例压力
3.如果已经存在了大Key,可以根据业务逻辑进行拆分和迁移,迁移完成后删除大Key
4.必要时可以采用压缩算法,将大Key压缩后存储,读取时再解压
5.从业务层面去分析大Key产生的原因,根据需求和访问模式进行相应的优化,选取更为合适的数据结构去存储,优化业务逻辑设计等
Redis中Keys * 命令有什么影响
Keys命令类似于SQL中的like模糊匹配,返回符合条件的Key。使用Keys命令会有一些影响:
1.Keys命令会对做全Key扫描,如果Redis中的Key数量特别庞大,会影响性能
2.Keys命令是一个阻塞操作,在Keys扫描过程中,Redis是对其他客户端会停止服务,如果是在Redis集群中出现这样的现象,还有可能会误判节点故障,从而引发故障恢复等问题
所以一般我们的生产环境中都是禁Keys命令的
Redis主从集群和cluster集群区别
主从复制的原理
一主多从,从节点会不断的从主节点同步数据。主节点提供读写功能,从节点提供读功能。一般来说会让主节点用于写操作,从节点用来读操作,读写分离减少服务器压力。
但是从节点在同步数据的时候需要时间,可能就会造成数据不一致的情况,最好是读写都在主节点,从节点用来做热备了。
由于Redis本身没有选举能力,所以引入了哨兵
Cluster
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点 存储的数据是一样的,浪费内存,并且不好在线扩容。
因此,Reids Cluster集群(切片集群的实现方案)应运而生,它在Redis3.0加入的,实现了Redis的分布式存储。对数据进行分片,也就是说每台Rdis节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个Redis实例,还提供复制和故障转移的功能。 Redis cluster使用去中心化的思想,整个集群是分布式的。
所有的redis节点彼此互联(PING- PONG机制),内部使用二进制协议Q优化传输速度和带宽。 客户端与redis节点直连,不需要中间proxy层。客户端不需要连接集群所有节点,只要连接集群中任意一个可用节点即可。
Redis的哨兵选举是如何实现的
Redis的Master-Slave集群模式本身不存在故障恢复的能力,一般引入Sentinel哨兵集群来监控Redis集群,当哨兵检测到Redis的Master宕机或者故障时,就会从Slave节点选举出新的Master节点。
哨兵选举主节点的依据是通过筛选和综合评估:
1.筛选:主要是过滤到一些不健康的节点,比如下线、断线、或者没有回复心跳、以及会分析节点过往的连接情况,一定时间内是否经常断线或断线次数超过阈值。
2.综合评估:1在健康节点中找优先级最高的,优先级可以通过slave-priorty设置;2选择数据偏移量差距最小的节点,实际上是比较于Master节点复制数据的进度差距,避免存在数据丢失过多的问题;3通过RunID,在优先级和数据量偏移量一致的情况下,RunID越小说明创建的越早,具有优先成为Master的权利
Sentinel集群如果只有一个哨兵发现Master宕机,而其他哨兵没有感知的情况下,通过共识算法Raft协议来达成一致
Redis遇到hash冲突怎么办
Redis用一张全局的Hash表来存储键值对,当出现Hash冲突时,也是和Java的HashMap一样使用链地址法来存储相同HashCode的键值对,但是Redis不会在到达一定长度后转换成红黑树,为了避免长度过长影响效率,采取对Hash表做Rehash操作,也就是增加了Hash桶,默认Redis有两个全局Hash表,一个主数据表供当前使用,另一个用于扩容,也叫备用哈希表
Redis存在安全问题吗
RedisServer本身是不存在线程安全性问题的,这是因为Redis的指令操作采用单线程去执行,能够保证指令的顺序原子性,
Redis6.0引入了多线程,但是多线程是作用在网络IO上的,指令的操作仍然是单线程。所以不存在安全性问题。
至于为什么Redis不引用多线程的指令操作,是因为可能出现性能瓶颈的点无非是网络IO、CPU、内存,但是CPU本身不是Redis的并发瓶颈点,所以没有必要引入多线程。此外如果引入多线程反而需要考虑指令的安全性,就需要加锁来保证安全,反而影响性能有些得不偿失
而RedisClient当出现多线程、多进程操作同一变量时,比如同时查询、修改和写入,因为无法保证原子性,就可能会有数据安全问题。
一般解决办法是加锁、或者使用Redis单原子操作命令、或者使用Lua脚本来实现多个指令的执行操作
Lua脚本https://www.jb51.net/article/226573.htm
Redis雪崩的理解
雪崩是指
1.同一时刻Redis中大量的Key过期导致本该命中Redis缓存的请求大量的直接请求到了数据库,由于激增的请求导致数据库压力过大导致宕机
2.或者Redis宕机导致,导致大量请求直接请求到数据,导致数据库宕机
https://zhuanlan.zhihu.com/p/108179009
但一般来说,造成雪崩的几率很小,更多的可能会是因为攻击行为导致雪崩的出现。比如攻击者大量且随机的访问一些不存在的Key,导致一直查询数据库。或者像刚刚说的大量Key同时过期。
针对这些问题一般会采取缓存预热,在系统启动时就将热点数据Load到Redis中,并且过期时间进行随机设置,避免同一时刻Key同时失效。
此外Redis做集群+哨兵保证高可用,避免单个Redis宕机导致整个缓存服务不可用的情况。
还可以在业务上走限流处理,超过限制的请求通通走降级操作,这样也可以有效控制流量。
同时,数据库一般来说也会做集群部署,抗压能力也不会那么脆弱,且可以设置连接策略,超过限制的请求会被拒绝,再配合熔断机制也能够很好的保护数据库。具体的办法还有很多,根据具体的场景选择最合适的方案即可。
Redis穿透的理解
一般是黑客攻击,比如每秒5000次的请求一个不存在的负数ID,缓存中肯定没有,直接查数据库,数据库压力就很很大。
解决办法也很简单,这种不存在的Key也缓存到Redis中设置一个特殊的值比如null,再加上过期时间,这样就可以有效避免被穿透
Redis击穿的理解
主要是针对热点Key,访问的非常频繁,突然过期了,导致同一时刻大量请求涌入数据库。
一般分场景处理,如果是基本不会变的数据,设置为永不过期;
使用分布式锁,保证同一时刻仅有少量请求能到数据库,取数后刷新缓存;
或者使用定时任务刷新这个热点Key,保证它一直存在于Redis中
Redis和Mysql如何保证数据一致性
一般来说Redis做为缓存是为了减少应用和数据库之前的IO操作,减轻数据库的压力,同时提升IO效率,但当数据库的数据发生改变的时候,Redis中数据就可能会出现与数据库数据短暂不一致的情况。
在一般场景下可以通过
1.先更新数据库,再刷新缓存
2.先删除缓存,再更新数据库,等下次查询将数据再次缓存
当然,这两种方式在极端情况下还是会出现数据不一致的情况,如果仍然要保证一致性,只能采用最终一致性的方案:MQ的可靠消息通信
RocketMQ的可靠性消息通信实现最终数据一致性
使用 Canal组件监控Mysql的binlog日志,把更新后的数据刷新到Redis中,以实现最终一致性
怎么使用redis实现一个延时队列
延时队列指的是在未来某一时间将消息发送给消费者消费,比如订单超时功能。Redis没有直接提供相关的能力,但是可以通过zSet去实现,将通过zAdd命令将消息添加到zSet中,并且将过期时间做为Score;消费者端轮训zSet使用zRangeByScore获取当前时间以前的所有数消息进行处理,然后zSet中删消息进行删除。从而实现消息延迟队列。
但是这里会有两个问题:
1.由于轮训时间的间隔,消费时间可能会大于设定时间
2.另外由于不断地轮训,可能会对Redis造成比较大的压力
另外还可以设置过期时间,监听过期事件的方式也可以实现
Redis事务
和数据库的事务一样,同样满足ACID(原子性、一致性、隔离性、持久性)
Redis事务实际上是一个命令队列。使用multi开启事务,命令入队列,exec执行命令队列
https://zhuanlan.zhihu.com/p/146865185
Redis优化策略
设置内存淘汰的算法
当内存满了时候的淘汰算法一共八种
1.内存满了不淘汰任何数据,但不提供服务,直接报错(默认)
2.对设置有过期时间的Key进行1随机淘汰2最久未使用淘汰3最少使用的淘汰4最早过期的淘汰
3.对所有Key进行1随机淘汰2最久未使用的淘汰3最少使用的淘汰
https://zhuanlan.zhihu.com/p/134314240
https://zhuanlan.zhihu.com/p/537194965
使用Pipeline批量执行命令
注意Pipeline不保证事务,只是为了提高性能的客户端行为。
避免大量Key同时过期(设置随机失效时间),引发缓存击穿
使用Redis连接池,避免重复建立连接
使用分布式架构,提升性能
Redis持久化策略
AOF(Append Only File)文件追加方式
记录有所操作命令,并以文本形式追加到文件中
AOF重写优化,AOF是一个近乎实时持久化的一个策略,所以随着时间推移AOF文件会出现过大导致的IO性能问题。重写是将相同指令进行压缩,只保留最新的。在数据恢复的时候只需要执行压缩后的指令即可。
由于重写是一个比较消耗资源的动作,Redis是开辟了一个子进程去处理重写操作,为了避免与主进程数据不一致的情况,期间主进程数据表更会写到缓冲区中,重写完成后会将缓冲区中数据写入到新的AOF文件中
https://zhuanlan.zhihu.com/p/539277765
RDB(Redis DataBase)快照
将某一时刻的内存数据,以二进制形式写入磁盘
混合持久化方式
Redis4.0之后的持久化策略,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
布隆过滤器的作用和实现
场景:
1.每天有10W条数据要入库,但是数据中不仅只有增量数据,还掺杂着一些历史数据,这些数据无需入库,且需要找出来做进一步处理。
如果从数据库去查询每一条数据是非常不可取的,每日10W条的数据数据库的数据量是非常大的,所以检索很容易造成数据库崩溃。
这时引入Redis的布隆过滤器是非常不错的选择。
2.10亿个手机号,查找10w个在不在其中
3.垃圾邮件识别,如果黑名单数以亿计,只需要将黑名单放入布隆过滤器中,每次收到邮件判断在不在过滤器中即可。
https://www.zhihu.com/question/484463241/answer/2397096607
https://zhuanlan.zhihu.com/p/174796250
本质是一个存储bit位的数组结构,每个位只表示0或者1,且每个位置只占用一个bit的大小。对比我们传统的存字符串或者数值,存储空间上要节省不少。
数据写入时,取每条数据的唯一key(ID),然后将其通过一个特定的hash函数取hash值,再用该hash值与这个布隆过滤器的数组长度取模,最后落到这个数组对应的bit位上,将该bit位置为1。
但是由于可能会出现Hash碰撞,所以一般可以对同一个key做三种Hash运算,分别求模出3个位置,在判断数据是否存在的时候,只有三个位置都为1则代表,数据存在。但仍有一定几率的不精确,就看是否满足场景即可。
特点:
1.判断元素是否存在时返回结果存在但真实不一定存在;当返回不存在时肯定是不存在
2.高效地插入和查询,占用空间少,返回的结果是不确定性的。
场景
缓存
布隆过滤器
订阅模式
分布式锁
Redis分布式锁主要靠SetNX命令来实现,只有设置成功的线程能获取到锁。
需要注意的是需要考虑到获取锁的线程异常退出而导致的死锁问题,所以需要加上过期时间。
此外当业务处理时间大于过期时间时,可能仍会出现锁被其他线程B重复获取的情况,线程A处理完成之后又会错误释放B的锁,C则又会获取到锁,循环往复造成逻辑混乱,针对这个情况可以将value值设置为UUID+线程ID的方式,只能自己释放自己的锁。
针对业务时间大于过期时间的情况,可以开辟一个子线程循环监控业务线程状态,如果一直存活则给过期时间续时长
Redisson的封装了上述的逻辑,并且在主从Redis集群的场景下,如果上锁后Master节点宕机,从节点没有同步到锁数据,也会出现线程安全问题,Redisson也提供的ReadLock来保证将所有的锁数据全部同步到了所有节点之后才算加锁完成,这样Master节点宕机也不会影响锁。Redission的watchDog看门狗也可以为锁续约。
因为进行了封装使用也很方便,调用lock/unlock
https://www.bilibili.com/video/BV1nk4y1u781/?spm_id_from=333.337.search-card.all.click&vd_source=58be2faadec5232d5cd0d2e87f889188
为什么 Redis 集群的最大槽数是16384个
这个问题类似于为什么HashMap链表的长度是8,实际上应该是通过一个科学的计算验证得出的结论,假设Redis集群中有三个节点,发送心跳包时要每个Slot中信息信息,如果Slot数很大,每个节点需要发送的slot信息数越多,心跳包也随之增大,这是一个很大的网络开销。
另外集群规模的限制,一般也不会扩展到超过1000个主节点,16385也足够用,能够保证稳定也不会造成更大的网络开销
Memcache
按场景选择使用Memcache还是Redis,例如涉及到分布式、存储到缓存数据类型(mc只支持kv结构)、是否需要对缓存数据进行复杂操作、是否需要缓存持久化等
ZK
Watch机制的原理
分布式锁
Redis
原理基于setnx
AP模型,保证可用性
封装的组件Redission
Zookeeper
基于临时有序节点,创建成功后判断自己的节点是不是最小的节点,是则抢占成功。
监控抢占成功的节点,释放则开始新的抢占
CP模型,保证一致性
对分布式的理解
从字面上来讲,将不同的业务分布在不同的服务器上,最简单的分布式就是像将数据库服务和WEB服务呢部署在不同的服务器,这是最基本的一种。那么第二种的现在比较流行的微服务架构,微服务架构呢其实更好理解了,所谓微服务架构就是把一个系统中按业务拆分成多个单独运行的服务模块,这些服务呢可以交给不同的团队去独立的技术选型、独立部署和独立运维,服务间的耦合度就没有单体项目那么高了,另外可扩展性、灵活性、可靠性、安全性、可维护性等等都相对以往单体服务有了很大的提升。这就是对分布式的一些理解。
项目管理
一个项目从零到一需要关注哪些点
方法论
TOGAF
分支主题
技术架构基于TOGAF(The Open Group Architecture Framework)企业架构方法论指导设计。TOGAF的核心是架构开发方法ADM(Architecture Development Method)。此套方法是用来指导企业如何建立和维护其企业架构的一套流程化的架构开发步骤。ADM 将架构过程看成一个循环迭代的过程,并且此迭代过程可以是分层级的,即企业可以使用一个小组负责整个企业架构的迭代开发,也可以由多个架构开发小组针对每一部份进行迭代开发,并最终归为一体。
根据TOGAF企业架构设计的过程,主要分为架构规划、架构治理两大部分,架构规划中包括架构设计(业务架构、数据架构、应用架构、技术架构)及架构路标两方面。结合中国海油审计信息化建设的特点,在TOGAF技术架构设计的基础之上设计出一套适合中国海油的架构规划,包含总体架构、技术架构、数据架构、权限体系设计、安全架构、系统集成架构、平台部署架构。
PCC
需求分析确认
确定系统的业务目标和需求。
定义系统的功能和特性。
制定项目计划及里程碑(可以以甘特图形式),包括时间表、资源分配。
技术选型
需要针对具体的环境进行技术范围的确定,比如公司的信科部门要求,外部监管要求,结合未来发展需要 比如要满足未来3到5年技术发展,技术的{各种特性,}。像我现在做的这个项目数智化审计平台,他就存在一些监管性要求,比如等级保护要达到三级2.0标准,比如满足公司的信创自主可控要求,讲国产化,那么我就需要在规定的范围内,结合公司的现状具体情况去估算出一个适合我们的技术架构,不单单是拍脑门觉得某个技术最近很火,就用它,需要做一些对比、测算、测试等工作,最终确定一套符合当前场景的技术路线。
考虑安全可靠性、先进稳定性、灵活可扩展性、易用易维护性、合规性等
选择合适的技术栈,包括语言、框架和工具。
确定系统架构,例如单体应用程序、微服务架构等。
团队组建
定义团队成员的角色和职责。
设计团队的协作和沟通流程。
制定开发规范并贯宣全组成员
系统设计
创建系统架构和设计文档。
定义数据库结构及数据库表评审机制
制定系统的安全策略和访问控制机制。
开发测试
编写代码并进行单元测试。
进行集成测试,确保各组件协同工作。
执行性能测试,以确保系统能够满足预期的负载要求。
部署运维
部署系统到生产环境。
设置监控和日志审计,以便及时检测和解决问题。
制定备份和恢复策略,确保数据的安全性。
编写手册及培训
过程中需要考虑
扩展和优化
合规性和安全性
用户体验
Java
基础
封装指的是可以将多个数据和操作封装到个一个类中,可以隐藏具体实现,只对外提供必要的接口,这样可以使程序更加模块化且增加可维护性
但如果使用了反射,实际上可以运行时破坏封装
多态:一个父类有多个子类,那么子类可以有父类进行声明,也可以由父类调用子类实现的具体方法,这种称之为多态。多态可以简化接口,提升代码复用率
JVM调优
调优的最终目的都是为了应用程序使用 最小的硬件消耗 来承载更大的 吞吐量
但一般来说JVM经过这么多年的发展和验证,整体上其实是很健壮的,在合理的配置硬件和程序设计上,个人认为99%的情况都用不到JVM调优
通常来说,JVM的参数配置大多数情况也是会遵循JVM官方的建议,比如年轻代和老年代的比例设置1:2(-XX:NewRotio=2),再或者新生代分区比例设置8:1:1(-XX:SurvivorRatio=8),再或者堆内存设置为物理内存的3/4左右等等吧;
所以调优其实就是调JVM参数值。
1.直接输入java 出来的命令带-的都是标准参数
2.java -X 开头的参数都是非标准参数
3.-XX开头 需要使用 java -XX:+PrintFlagsFinal | more (java -XX:+PrintFlagsFinal | wc -l 统计下来一共731个【当前版本】)
GC常用参数
分支主题
子主题 2
内存泄漏怎么排查解决
是有很多工具可以用来排除内存泄漏,我一般会用VisualVM,这个也是JDK自带的,通过观察堆内存面积图能判断出GC频率和堆内存的情况,正常情况来说随着时间推移GC的频率和堆内存的使用情况,在面积图中是规则的梯形状的;如果是锯齿状的,且整个堆内存的使用占比越来越高,就很可能出现了内存泄漏
这个时候就需要通过采样器和扫描器来扫描内存中各对象的占比,正常情况一般是基础类型和对象类型占比比较高,如果出现占比高的类型是Map这种那么就可能是一种异常情况,就需要去人工审查近期提交的代码了
总体来说就是通过工具+人工方式来定位泄漏点
找到具体的泄漏点后,就可以点对点的进行调整
一般出现泄露的点可能是 资源使用后未关闭(比如文件、各种链接)
还有可能大体量的单例或者内存缓存,设计不合理或者没有设置过期时间
或者是大量的静态变量引用,也可能会导致引用无法释放
最后还会是因为一些初级程序写的错误的代码导致的,比如循环中频繁创建对象
频繁FullGC如何排查
排查系统中是否大量使用了NIO、Kafka、Netty,它们使用零拷贝时候会将开辟堆外内存,堆内只有引用,使用不当时可能会触发FullGC
如何判断一个对象可以被回收
JVM为什么使用元空间替换了永久代
堆外内存
ByteBuffer.allocateDirect()就可以开辟一个堆外内存,需要显示回收
如何定位垃圾
引用计数法(ReferenceCount)
不精确,如果存在循环引用,则不会被识别成垃圾,Python用的是引用计数法
根可达算法(RootSearching)
hotspot虚拟机默认使用的垃圾回收算法,不存在因为循环引用导致漏扫垃圾的问题。
根可达算法定义了一个根集合,从这个根出发能够遍历到引用的对象(也就是可达的对象),判定为存活,否则就是垃圾
如何清理垃圾
标记清除法(MarkSweep)
将要被清理的对象打上标记,触发GC时会回收这些被标记的垃圾对象;
缺点是回收时会触发较长的STW,效率不稳定,清理后不会移动对象,会造成内存碎片化,时间长了没法装太大的对象
双色/三色标记
双色标记STW的时间比较长
三色
分支主题
标记复制法(MarkCopying)-适合新生代
会在新生代中将空间划分为8:1:1,一个Eden伊甸园区两个Survivor幸存区,每次只使用Eden和其中一个Survivor,当Eden空间不足触发GC时将存活的对象放入统一复制到另一块Survivor,如果Survivor空间不足以放下对象时,多出来的对象放入老年代,然后清空Eden和之前的Survivor
效率高,缺点是1空间利用率低,2对象存活率较高时,需要进行较多次的复制,不适合老年代
分支主题
标记整理法(也叫标记压缩)-适合老年代
将存活的对象都移动到内存的另一端然后将剩下的清除掉;
不浪费空间也不产生碎片,但缺点是效率低,标记整理是移动式的,需要移动存活对象,移动的过程需要较长时间的STW
分支主题
分代算法-JVM采用的
JVM将内存分代,不同代使用不同的算法:
新生代:每次GC都有大量对象死去,使用复制法
老年代:对象存活率高,使用标记整理法
永久代:方法区其实就是永久代,jdk1.8废除了永久代
扩展:
永久代要回收的——废弃的常量和不再使用的类(Class对象)
判断废弃常量
一般是判断没有该常量的引用。
判断不再使用的类,必须以下3个条件都满足
该类的所有实例都已被回收
加载该类的ClassLoader已被回收
该类的Class对象没有被引用
垃圾回收器
垃圾回收器一般分为两大类,分代合分区;(左侧虚线连接的都是分代,右侧都是分区)
分代
https://zhuanlan.zhihu.com/p/490928570
目前1.8默认使用的是PS+PO parallel (Scavenge+ParallelOld),当1.8PS+PO不满足的时候就可以用G1回收器了。
分支主题
STW:
业务线程全部暂停,垃圾回收线程(单线程)执行。(很多java程序卡顿的原因),serial是最古老的回收算法,程序内存小 单线程很快就跑完了。所以单线程不够用多线程(但是还不够,因为如果项目非常大,线程多也不能解决。且线程太多了线程切换消耗资源)。
stw的时间是调优的非常重要的参数。
理论上内存对应的垃圾回收器
G1号称200ms
ZGC号称10ms
CMS有个浮动垃圾上升的bug,老年代碎片化严重,经常fullGC
没有任何一个jdk默认CMS,调优时可以直接升内存 换G1即可。
ZGC完全是另一个思路。他不是在堆内存,而是在指针地址上。
JMM
内存模型
JavaMemoryModel,主要是因为在不同操作系统下,指的是多核处理器上 内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,实现让java程序在跨平台也能达到一致的并发效果。
Java内存模型规定所有的变量都存储在主内存中,比如像实例变量,静态变量,但是不包括局部变量和方法参数。然后每个线程都有自己的工作内存,工作内存主要是保存线程自己的变量和主内存的变量副本,所以线程对变量的操作都在自己的工作内存中进行,不能直接读写主内存中的变量。所以不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。如图(解释多核):
每个线程的工作内存都是独立的,线程操作数据只能在工作内存中进行,然后刷回到主存。这是 Java 内存模型定义的线程基本工作方式。
JMM定义了什么
原子性,可见性,有序性,整个Java内存模型实际上是围绕着这三个特征建立起来的。这三个特征就是Java并发的基础。
原子性指的是一个操作是不可分割,不可中断的,一个线程在执行时不会被其他线程干扰。
可见性指当一个线程修改共享变量的值,其他线程能够立即知道被修改了。Java是利用volatile关键字来提供可见性的。当变量被volatile修饰时,这个变量被修改后会立刻刷新到主内存,当其它线程需要读取该变量时,会去主内存中读取新值。而普通变量则不能保证这一点。
除了volatile关键字之外,final和synchronized也能实现可见性。synchronized的原理是,在执行完,进入unlock之前,必须将共享变量同步到主内存中。final修饰的字段,一旦初始化完成,如果没有对象逸出(指对象为初始化完成就可以被别的线程使用),那么对于其他线程都是可见的。
有序性,在Java中,可以使用synchronized或者volatile保证多线程之间操作的有序性。实现原理有些区别:volatile关键字是使用内存屏障达到禁止指令重排序,以保证有序性。synchronized的原理是,一个线程lock之后,必须unlock后,其他线程才可以重新lock,使得被synchronized包住的代码块在多线程之间是串行执行的。
JMM八种内存交互方式
lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用。
load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
as-if-serial语义
as-if-serial语义指的是:不管如何重排序(编译器和处理器为了提升并行度),(单线程)程序执行结果不能被改变。编译器、runtime、处理器都不能违反as-if-serial语义。
为了遵循as-if-serial,编译器和处理器都不会对存在数据依赖的操作重排,因为这可能会改变程序结果。但是如果操作间不存在依赖关系,那么就可能被编译器和处理器进行重排操作。如:
int a = 1; // A1
int b = 2; // A2
int c = a * b; // A3
A1操作和A2操作没有依赖关系,A3 和 A1存在依赖关系、A3 和 A2也存在依赖关系,所以编译器和处理器可以重排A1操作和A2操作的顺序,而根据as-if-serial语义不能对A3操作重排,程序执行的情况有:
① A1 -> A2 -> A3
② A2 -> A1 -> A3
as-if-serial语义把单线程程序保护了起来,遵循as-if-serial语义的编译器、runtime、处理器共同为开发单线程程序的程序员创建了一个幻觉:单线程程序就是程序顺序(编码顺序)来执行的。实际上单线程下程序员也因为as-if-serial语义的存在不必担心重排序会干扰程序执行结果,也无需担心内存可见性。
先行发生原则(happens-before)
从JDK5开始,java使用新的JSR-133内存模型,该内存模型使用happens-before的概念来阐述操作间的内存可见性。在JMM中A操作的执行结果要对B操作可见,那么A和B之间存在happens-before关系,即A happens-before B。(A和B可以是不同线程中的操作)
happens-before规则如下:
程序顺序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
监视器锁规则(Monitor Lock Rule):一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是同一个锁,而 “后面” 是指时间上的先后顺序。
volatile 变量规则(Volatile Variable Rule):对一个 volatile 域的写,happens- before 于任意后续对这个 volatile 域的读。
线程启动规则(Thread Start Rule):Thread 对象的 start () 方法先行发生于此线程的每一个动作。
线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join () 方法结束、Thread.isAlive () 的返回值等手段检测到线程已经终止执行。
线程中断规则(Thread Interruption Rule):对线程 interrupt () 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted () 方法检测到是否有中断发生。
对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize () 方法的开始。
传递性(Transitivity):如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那就可以得出操作 A 先行发生于操作 C 的结论。
volatile的作用
保证线程间的可见性,一般用作开关型的flag
在多核处理中,一旦其中一个cpu修改主内存中的变量,会通过总线(bus)写回修改后的变量值,总内存会立刻告诉总线其他线程拿到的之前的变量失效,其他线程使用该变量需要从总内存重新取值.
使用volatile会通过内存屏障防止指令重排
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令重排,一般分为以下三种:
源代码 -> 编译器优化的重排 -> 指令并行的重排 -> 内存系统的重排 -> 最终执行指令
Volatile实现禁止指令重排优化,避免了一些多线程环境下程序出现乱序执行的现象:
读写
什么事禁止指令重排
首先要讲一下as-if-serial语义,不管怎么重排序,(单线程)程序的执行结果不能被改变。
指令重排
在很多情况下,访问程序变量,比如对象实例字段、类静态字段和数组元素的执行顺序与程序员编写的程序指定的执行顺序不同。因为编译器可以以优化的名义任意调整指令的执行顺序。
在这种情况下,数据可以按照不同于程序指定的顺序在寄存器、处理器缓存和内存之间移动。
有许多潜在的重新排序来源,例如编译器、JIT(即时编译)和缓存。
重排序是硬件、编译器一起制造出来的一种错觉,在单线程程序中不会发生重排序的现象,重排序往往发生在未正确同步的多线程程序中。
重排序对多线程的影响
class ReorderExample {
int a = 0;
// 标记变量a是否被写入
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
public void reader() {
if (flag) { //3
int i = a * a; //4
}
}
}
假设有两个线程A和B,A首先执行writer()方法,随后B执行reader()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入?
答案是:不一定
原因是由操作1和操作2没有依赖关系,编译器和处理器可以对这两个操作进行重排序;同样的3和4也没有数据依赖关系,编译器和处理器也可以对这两个操作进行重排序。当操作1和操作2发生重排序时的效果(时序图):
分支主题
如图所示,线程A的操作1和操作2发生了重排序,flag先被置为了true,紧接着线程B进入判断,但此时变量a并没有被写入,在这里多线程程序的语义就被破坏了!
(图中红色虚线为错误的读操作)
当操作3和操作4发生重排序时的效果(时序图):
分支主题
在程序中,操作3和操作4存在控制依赖关系。只有操作3为真时,操作4才会正常执行。但当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程B的处理器可以提前读取并计算a*a,然后把计算结果临时存在一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作3的条件判断为真时,就把该计算结果写入变量i中。
图中可以看出猜测执行实质上对操作3和操作4进行了重排序,这里多线程的语义也同样被破坏了!
在单线程程序中,对存在依赖控制的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但是在多线程程序中,对存在依赖控制的操作重排序,可能会改变程序的执行结果!
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器就会根据内存屏障的规则禁止重排序。比如为了保证final字段的特殊语义,也会在一些场景下面加内存屏障(比如先对final字段写,再普通读):
x.finalField = v; StoreStore; sharedRef = x;
内存屏障的类型:
LoadLoad、StoreStore、LoadStore、StoreStore
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
分支主题
volitile一定能保证线程安全吗
不能一定能保证线程安全,因为他只保证可见性,但是不保证原子性
什么是逃逸分析
逃逸分析是一种编译器优化技术,主要是分析对象的作用域是否逃逸出方法或者线程的范围;逃逸也就是指对象是否被方法返回,存储到全局变量、或者做为参数传递给其他方法,如果是这些情况的话,对象的引用就有可能逃出方法或者线程的范围;
所以一个对象分析后认为他是不存在逃逸的,那么就有可能会被栈上分配,这样也就无需开辟堆内存空间以及节省了对他的垃圾回收开销。
另外逃逸分析还有锁消除的作用,也同样是通过分析后把一定不会出现资源竞争的同步方法优化掉锁。
HotSpot虚拟机在JIT编译阶段会使用逃逸分析来进行优化
Agent
Queue
比如LinkedBlockQueue是一个阻塞队列,Java里Executors创建的固定线程数线程池中的等待队列用的就是它
常见的Queue:
ArrayBlockingQueue :基于数组的有界阻塞队列,必须指定大小。
LinkedBlockingQueue :基于单链表的无界阻塞队列,不需指定大小。
PriorityBlockingQueue :基于最小二叉堆的无界、优先级阻塞队列。
DelayQueue:基于延迟、优先级、无界阻塞队列。
SynchronousQueue :基于 CAS 的阻塞队列。
add():新增一个元索,假如队列已满,则抛异常。
offer():新增一个元素,假如队列没满则返回 true,假如队列已满,则返回 false。
put():新增一个元素,假如队列满,则阻塞。
element():获取队列头部一个元素,假如队列为空,则抛异常。
peek():获取队列头部一个元素,假如队列为空,则返回 null。
remove():执行删除操作,返回队列头部的元素,假如队列为空,则抛异常。
poll():执行删除操作,返回队列头部的元素,假如队列为空,则返回 null。
take():执行删除操作,返回队列头部的元素,假如队列为空,则阻塞。
反射
运行时动态获取一个类的信息,包括属性、方法、构造方法,还可以调用方法、修改属性等
https://zhuanlan.zhihu.com/p/611294642
Java反射的优缺点
优点就是灵活,运行时有效,可以动态创建对象、调用方法、修改属性、获取类信息,常用于偏底层的框架设计
缺点就是会破坏对象的封装性,降低程序性能
场景
没有连接池前,最常见的就是用反射加载数据库驱动
Spring中大量用到了反射,AOP、注入等
调用一些私有属性,破处限制
PE框架通过反射获取目标对象,并且根据配置代理目标对象进行方法的执行
方法
Class.forName
xx.class
setAccessible(true)获取构造函数访问权限,newInstance创建实例、getDeclaredField获取属性、getDeclaredMethod获取方法、invoke调用目标方法
代理
Jdk动态代理为什么只能代理有接口的类
本身设计如此,JDK的代理需要代理类实现InvocationHandler接口,重写invoke方法,在代理目前方法前后可以做增强;
它的最大缺陷就是只能代理有接口的类,如果目标类没有实现接口,可以使用cglib
cglib
通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑,MethodInterceptor,由于 CGLIB 采用继承的方式,所以被代理的类不能被 final 修饰
Spring 中的 AOP :如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理
如何提升接口性能
该并发并发、该批量批量、该异步异步
敏感数据一般怎么加解密和传输
分场景,对安全性要求没那么高的只对必要字段加密
可以采用AES对称加密,公私钥的RSA非对称、RSA验签,必要还可以加盐值
Hash一致性算法的理解
HashMap中的hash方法为什么要右移16位异或
HashMap中的hash方法对传入的键对象的hashCode()值进行了进一步的散列操作,其中右移16位并进行异或的操作是为了是将高位的信息混入低位,增加散列的随机性,减少Hash碰撞
HashMap和HashTable的区别
HashTable线程安全,HashMap线程不安全,当然也有安全的CurrentHashMap
HashMap通常比HashTable性能更好
HashMap允许键和值都为null,HashTable不允许键或值为null
四大引用
强引用
Object a = new A()就是一个强引用,在a指向null之前,之前被指向的对象不会被回收
软引用
通过SoftReference类实现的软引用,被软引用指向的对象,在内存不足时会被回收
弱引用
通过WeekReference类实现弱引用,弱引用指向的对象只要GC就会被回收,像ThreadLocal为了避免内存泄露,也使用到了弱引用
虚引用
通过PhantomReference类实现虚引用,无法通过虚引用获取一个对象的实例,
为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
空Object对象占多大空间
在HotSpot虚拟机里面一个对象在堆内存中分三部分存储,对象头、实例数据、对齐填充padding
对象头,包括Markword、类元指针、数组长度其中Markword用来存储对象运行时的相关数据,比如hashCode、gc分代年龄等,8个字节
实例数据,存储对象中的字段信息,0
Java对象的大小需要按照8个字节或者8个字节的倍数对齐,避免伪共享问题
伪共享(False Sharing)是指多个线程同时访问不同变量或对象的不同缓存行(Cache Line),但这些缓存行在同一个缓存组(Cache Set)中。当一个线程修改其中一个变量或对象时,可能会导致其他线程不必要地失效自己的缓存行,从而引起性能下降。
为了解决伪共享问题,可以采用以下方法:
1、对齐数据:通过在变量或对象之间增加填充字节,使它们在不同缓存行中,从而避免伪共享。在Java中,可以使用@Contended注解来实现数据的对齐。
2、使用独立的缓存行:将需要共享的变量或对象放置在独立的缓存行中,以避免与其他变量或对象发生伪共享。
所以一个空的对象,在开启压缩指针的情况下,占16个字节其中Markword占8个字节、类元指针占4个字节, 对齐填充占4个字节。
Finally语句块一定会执行吗
比如调用了System.exit
发生了OOM
如何破坏双亲委派模型
双亲委派指的是类的加载会委派父类加载器去执行,这个过程会一直递归到顶级启动类加载器;主要目的是为了保证类加载的唯一性
双亲委派原则:
主要指的是java文件编译成.class文件后进入JVM运行时的第一步类加载器所涉及到的一种机制,class Load为了避免重复加载和基础类API的安全,采用了这种双亲委派的机制,类似与向顶头上司请假,上司说先问问经理,经理又说问问老板先,老板如果批了那就结束了,如果不批他告诉你请假不归我管,找经理去吧,然后经理才会给你处理。这样也很大程度上避免了有人批有人不批容易乱掉。
一般来说我们要尊从双亲委派机制,除了jvm提供的三种加载器外,我们可以继承classLoader重写findClass方法即可;
如果要破坏双亲委派机制,继承classLoader重写loadClass方法;
自定义加载器的作用?
自定义主要原因是因为jvm自带的三个加载器只能加载指定目录下的字节码文件,如果需要在比如D盘下或者网络上某个文件就需要自定义加载器了。
破坏双亲委派可以使用自定义加载器的方式,重写loadClass方法,实现自定义 类加载器
继承上下文类加载器(Context ClassLoader),也可以绕过双亲委派模型,自由选择类加载器
破坏双亲委派的场景有实现热部署,以便在运行时动态加载新的类版本
new String("abc")创建了几个对象
看常量池中当前有没有“abc”,有则在创建堆中创建一个对象,没有就是两个,常量池一个堆中一个
序列化和反序列化的理解
序列化和反序列化是将对象转换为字节流以便存储或传输,并在需要时将字节流重新转换回对象的过程。
序列化是将对象转换为字节流的过程。通过序列化,对象的状态信息被转化为字节序列,可以保存到文件、数据库或通过网络传输。在Java中,可以通过实现Serializable接口来标记一个类是可序列化的。序列化使用ObjectOutputStream将对象写入输出流中
反序列化是将字节流转换回对象的过程。通过反序列化,字节流被重新转换为对象的状态信息。在Java中,可以使用ObjectInputStream从输入流中读取字节流,并将其转换为相应的对象。被反序列化的类需要实现Serializable接口,并且保证类的版本一致性,即反序列化的类必须与序列化的类具有相同的类结构。
序列化时需要注意安全性和版本兼容性的问题。
序列化和反序列化的主要用途包括:
持久化对象:通过将对象序列化并保存到文件或数据库中,可以在程序重新启动时恢复对象的状态。
远程方法调用:在分布式系统中,可以将对象序列化并通过网络传输,以便在不同的主机上调用远程方法。
缓存和消息传递:通过将对象序列化并存储在缓存或消息队列中,可以提高系统的性能和可扩展性。
实现序列化和反序列化为什么要实现Serializable接口
反序列化时验证用,一般使用默认的,除非是用于版本控制
对象的创建过程
内存分配:在Java中,对象在运行时被分配在堆内存中。当使用关键字new创建一个对象时,Java虚拟机会在堆内存中为该对象分配一块连续的内存空间。
类加载:在对象被创建之前,需要先加载并验证对象所属的类。类加载器负责将类的字节码文件加载到内存中,并进行必要的验证,确保类的正确性和安全性。
分配内存:在堆内存中分配一块连续的内存空间来存储对象的实例变量。这个过程可能涉及到内存对齐等细节。
初始化:在内存分配完成后,Java虚拟机会对对象进行初始化。这包括对实例变量进行默认初始化(如0、null等),执行构造方法来初始化对象的状态。
对象引用:在对象创建完成后,Java虚拟机会返回一个对象引用,这个引用指向对象在堆内存中的地址。通过对象引用,我们可以访问和操作对象的实例变量和方法。
内部类和静态内部类的作用及场景
内部类和静态内部类的使用场景主要取决于是否需要与外部类的实例交互以及是否需要访问外部类的实例变量和方法。如果需要与外部类实例紧密交互或访问外部类的实例状态,则使用非静态内部类。如果不需要与外部类实例交互或访问外部类的实例状态,则使用静态内部类。
如果一个类表示汽车,而引擎是汽车的一部分,可以将引擎定义为普通内部类,以便能够访问汽车的属性和方法。
静态内部类如图书馆类和图书类,它可以有自己的属性和方法,而不依赖于外部类的实例
Java SPI是什么?有什么用?
ServiceProviderInterface简称SPI,是一种桥接模式,我理解为是一种组件动态加载的实现,核心包提供抽象接口以及
使用jdk提供的ServiceLoader对配置中的第三方Service进行扫描调用,而第三方应用只实现自己关心的接口,以及维护配置即可。
这种方式下形成了一个非常好的解耦和动态扩展
https://www.zhihu.com/question/584728871/answer/2901760947
代码小郭的回答
像SpringBoot的自动装配机制就是基于SPI实现的,他改了个名叫SpringFactoriesLoader,配置文件也改成了spring.factories
== 和 equals 区别
比较对象类型
==比较内存引用地址
equals比较内容
比较值是==是比较值
为什么重写equals()就一定要重写hashCode()方法
只重写equals不重写hashCode会导致判断对象相同但hashCode不同。当使用散列集合存储这样的对象的时候就会出现问题,同一个对象得到了两个不同的HashCode,存放位置也不同,当我们取的时候也会出现这样的悖论,所以一般约定俗成重写equals也要重写hashCode
设计模型
策略模式和观察者模式
Integer和int的区别
Integer是包装类型,允许为null,由于拆箱装箱性能低于int
Integer a1=100 Integer a2=100,a1==a2的运行结果及原因
a1==a2 的执行结果是 true 原因是 Integer 内部用到了享元模式的设计,针对-128 到 127 之间的数字做了缓存。 使用 Integer a1=100 这个方式赋值时,Java 默认会通过 valueOf 对 100 这个数字进 行装箱操作, 从而触发了缓存机制,使得 a1 和 a2 指向了同一个 Integer 地址空间。
消息队列重复消费问题
在一些用到消息队列的场景都有可能出现消息重复消费的问题,拿电商活动那个累计消费奖励来举例,活动期间消费的金额越多档次越高,奖励也不同。一般这个场景不会直接操作数据库去累计金额,特别是一些双十一大的促销日,数据库和缓存都扛不住这样的请求。
这个时候一般引入消息队列,做个削峰处理,下单成功后将消息放入队列,各个业务取消息消费,当出现某个业务系统由于网络波动啊或者bug等问题消费失败,会请求消息重发,重发时就可能出现其他业务系统重复消费的情况;
https://www.zhihu.com/question/64921387/answer/1023714489
所以问题的本质是保证业务处理的幂等性
什么是幂等,如何保证接口幂等
简单来说就是重复消费,同一个请求由于一些例如重试的机制多次发起,为了保证幂等,服务端需要识别这些请求是一个请求,并且只执行一次
保证幂等的方法有很多,总的来说根据场景去做强校验和弱校验,比如涉及到金额的我们一般采用强校验:
1.拿着订单号+业务场景ID这样的唯一标识去查流水表,如果有直接return不处理即可,没有就继续走后面的业务逻辑。这里面用到流水表也是因为主要处理金额嘛,后边如果出现什么问题,方便使用流水表进行对账
2.那么一些场景不是那么严格的地方我们采用弱校验,比如将id+场景id放到redis中的,当然也要注意失效时间,主要看场景,之后去判断key的存在与否去做相应的处理,这里比如一些短信通知啊,会议通知啊一些不是主要敏感业务的可以这么做。即使KV丢了,影响也不会很大。
怎么保证消息处理的顺序性
这种情况一般是针对同一个业务的不同操作,生产者生产消息的顺序是对的,但消费者处理消息的顺序是错的。
解决办法呢,我简单了解过使用RocketMQ的解决办法,RocketMQ的topic内的队列机制,他能保证FIFO,由于呢一个topic中有多个队列,所以首先需要保证同一个业务的处理消息在同一个队列中,那么就可以对同一业务的特征进行取模,Hash取模(比如订单ID),RocketMQ的MessageQueueSelector吧就有这样的实现,其中一个就是Hash。这样呢我们首先是保证了消息的顺序发送。那么消费者顺序消费的顺序呢也需要保证,其实也是同理,Hash取模一下,将同一个业务的处理都落到同一个消费者身上即可,这样呢就能保证在多线程情况下存在多个消费者,导致消息消费顺序会乱掉的问题
防重提
每个请求的ID,通过使用切面配合redis的setnx来防重,或者使用锁等
Arraylist与LinkedList有什么区别
结构上ArrayList使用数组,LinkedList使用链表
插入和删除数据上ArrayList首尾操作时间复杂度仅为O(1),但中间插入或删除需要移动元素,复杂度为O(n),LinkedList由于链表结构时间复杂度均为O(1)
随机查询上ArrayList通过索引查找时间复杂度为O(1),而LinkedList不支持直接的随机访问,需要从头开始遍历,复杂度为O(n),
此外内存占用上两者也有一定不同,相对于LinkedList,ArrayList需要分配连续的一段空间
所以需要根据场景选用具体的类型
如何在2G大小的文件中,找出高频top100的单词
这种TopN的场景需要考虑内存问题和性能问题,首先是2G文件不能一次性load到内存中容易OOM,所以需要先用分治的思想去拆分这2G的文件,为了提高性能可以使用多线程并发的处理所有的小文件,将文件中的单词及其出现次数存到HashMap中,这里线程还可以配合CyclicBarrier循环屏障去做,需要注意线程安全,最后在用小顶堆排序出Top词即可
https://zhuanlan.zhihu.com/p/112307089
什么是拆包和粘包?怎么解决
在TCP协议下底层数据是以流的形式传输的,数据被分割成了一个个小数据包,在发送过程中可能会出现一个小数据包拆成了两个小包,或者两个小包一起发过去了,这种现象叫做拆包和粘包
解决这两个问题最直接的办法就是
1.约定定长数据
2.标记开头和结尾
3.自定义协议在请求头中写明数据长度
TCP、UDP、HTTP
在OSI模型中HTTP属于应用层协议,而TCP和UDP属于传输层的协议,本身Http与它俩无法比较,HTTP是基于TCP的可靠数据传输协议,它本身是一种无状态的短链接,但后续引用的keepAlive和多路复用变相的拥有了长链接的能力;
https://zhuanlan.zhihu.com/p/56440577
TCP是面向连接的(有点类似于打电话需要先对方接听),而UDP是无连接的,发送数据前无需连接,这也是导致它俩一个是可靠的一个不可靠的主要原因;此外TCP只能一对一,而UDP可以一对多,并且UDP的头信息更少效率更高,但不可靠
为什么要四次挥手
为了保证可靠性,ack确认包的机制
1.客户端首先发起分手意愿包
2.服务端收到后返回ack确认包
3.服务端如果确认分手,发起分手包
4.客户端收到后确认分手
时间轮的理解
时间轮=环形数组+链表,环形数组表示时间刻度,每个刻度上都维护着一个双向链表,用于存储这一时刻要执行的任务。特别适合定时任务调度、超时处理、缓存过期等场景
LRU LFU算法
LRU(Least Recently Used)最近最少使用
LFU(Least Frequently Used)最不经常使用
这两种算法在Redis的内存优化中做为淘汰算法使用
实现
线上环境几百万的消息积压怎么处理/
如何处理消息队列的消息积压问题
通常来说消息积压是因为生产者生产数据的速度大于消费者消费的速度;遇到这个问题时,需要先排查具体的原因再做相应的处理;
首先排查是是否是BUG导致消息积压,如果不是BUG导致的,可以优化消费逻辑,比如尝试通过异步、批量的方式来处理消息;如果还没有缓解采取水平扩容的方式扩大消费端的消费能力;
如果是BUG导致的,
1. 首先解决消费端的 bug,来保证消费端的正常消息处理工作。
2. 接着把现在所有的消费端停止,然后新建一个 Topic,然后把 Partition 分区数量 调整成原来的 10 倍。
3. 接着写一个用来实现数据分发的 Consumer 程序,这个程序专门去消费现在积压 的数据,消费后不做处理,而是直接再把这些数据写入临时建立的 Topic 的 10 个 Partition 中。
4. 然后临时增加 10 倍的消费者节点来部署 Consumer,专门来消费临时的 Partition 分区数据。 通过上面这种方法,可以快速把现在堆积的消息处理完。等积压的消息处理结束后,再恢复成原来的部署架构,把临时的 Topic 和临时申请的机器释放掉。
雪花算法及时钟回拨问题
雪花算法是由时间戳+机器ID+序列号组成的,当遇到时钟倒退或跳变,有可能会出现新生成的ID与之前的ID重复的现象。
按照事前、事中、事后原则来讲,事前一般我们会尽量避免时钟出现回拨现象,选择可靠的时钟源,比如基于GPS的时钟源,要求更高的场景选择物理时钟;
事中做好时钟同步,比如通过NTP协议做好时钟同步;做好时钟回拨检测,异常处理;
事后,如果已经发生回拨,根据预先设置好的异常处理机制去做补偿处理;
如何实现断点续传
分段并不是服务端进行数据切分用多线程传输文件数据,而是客户端行为,例如迅雷为什么这么快,就是他可以开多个线程同时请求服务器下载同一个文件不同位置的数据(利用position分段);
如何实现限速下载
一般限速不在应用服务器上进行实现,可以在网关上进行控制,例如VIP用户和普通用户在下载时,控制其带宽,或者可能请求的服务器和数据库都不同;
1- 事件+定时器 记录当前下载字节数,当字节数达到某一值(例如限制100k)触发事件,1秒后再继续下
2- 线程睡眠 弊端较大:每次需计算执行的时间然后再睡眠,再唤醒,唤醒时间还不可控,不能精确控制
3- 使用Actor发布指令
Linux
CPU突然飙高,生产环境告警,怎么排查
常用指令:cd cd .. cd - mkdir rm -rf mv top ps -ef|grep kill df chmod -Rf copy find netstat
Docker
网络IO
Cookie和Session的区别
先解释一下Cookie,它是是客户端浏览器用于保存服务端数据的一种机制。当浏览器访问时可以将一些状态值以kv的形式存到Cookie中,当客户端下次访问服务端时,可以将Cookie中的数据发送到服务端,服务端可以根据Cookie中的内容识别客户端
Session是服务端的容器对象,针对每一个浏览器请求Servlet都会分配一个Session,Session本质上是一个ConcurrentHashMap,可以存储当前会话产生的数据。
由于HTTP是无状态协议,也就是服务器是不知道发送过来的多次请求属于同一个客户端,有了Session和Cookie,通过SessionID就能弥补这一点;另外Session还可以用作上下文数据传递
Java有几种拷贝方式,哪种效率最高
浅拷贝
创建一个新对象,基本类型复制值,引用类型复制引用,所以新旧对象共享相同的子对象
深拷贝
包括引用对象在内的都进行拷贝,相当于副本
零拷贝
NIO的TransferTo
TCP协议为什么要设计三次握手
首先介绍一下三次握手做了哪些事,第一次是客户端发起同步报文,第二次服务端收到后响应同步报文+ack(acknowledge),第三次是客户端发送ack,此时可以开始传输数据
三次握手主要是为了确保双方已经达到同步状态、保证连接的可靠性和稳定性,以及防止已失效的连接请求被误认为有效
Http协议和RPC协议有什么区别
RPC,可以基于TCP协议,也可以基于HTTP协议
HTTP,基于HTTP协议;
RPC由于可以使用自定义的TCP协议,可以让报文体积更小,相对效率更高,主要性能消耗在序列化和反序列化
RPC自带负载均衡,HTTP需要配置nginx等中间件
场景:RPC主要⽤于公司内部的服务调⽤,性能消耗低,传输效率⾼,服务治理⽅便。HTTP主要⽤于对外的异构环境,浏览器接⼝调⽤,APP接⼝调⽤,第三⽅接⼝调⽤等。
IO多路复用-select和epoll的区别
举个例子,select版宿管阿姨,带着你的朋友挨个房间找,直到找到你
epoll版阿姨,会先记下每位同学的房间号, 你的朋友来时,只需告诉你的朋友你住在哪个房间,无需亲自带着你朋友满大楼逐个房间找人
如果来了10000个人,都要找自己住这栋楼的同学时,select版和epoll版宿管大妈,明显epoll版的大妈效率更高;
在高并发场景中,轮询I/O是最耗时操作之一,epoll性能也是明显高于select。
WebService接口和Http接口的区别
WebService基于XML封装数据,优势在于跨平台
远程调用
httpUrlConnection
RestTemplate
SpringWeb提供的阻塞式HTTP客户端,API很简单,但是是阻塞式的,在一些异步或者高并发场景下不太适用,这种情况可以使用WebClient
WebClient(推荐)
SpringFlux提供的非阻塞式HTTP客户端,优于阻塞式RestTemplate,使用了Reactor响应式模型,用回调方式来处理响应,非常适合于高并发和异步的场景。Spring5之后推荐使用WebClient。
WebClient则是Spring WebFlux模块中的新一代异步非阻塞的HTTP客户端。它基于Reactive Streams的背压模型,并与Reactor库深度整合,提供了用于响应式编程的API。WebClient使用的是Java8中引入的CompletableFuture或Java9中引入的Flow API,它可以处理大量并发请求并利用回调式编程风格来处理响应。因此,WebClient适用于高并发和异步操作的场景,并且能更好地与其他非阻塞的WebFlux组件集成。
https://blog.csdn.net/weixin_35688430/article/details/119750922
HttpClient
Apache提供的阻塞式HTTP客户端,和RestTemplate一样只适合传统的同步模型下使用
ORM
Mybatis的缓存机制
Mybatis有两级缓存,一个是SqlSession级别的,同一个会话中共享,命中缓存则走缓存,没有命中则走数据库,且将结果存入一级缓存
二级缓存是全局缓存,默认不开启
Mybatis中#{}和${}的区别是什么
#和$都是动态SQL的占位符,比较大的区别是$可能会有SQL注入的风险,一般动态表名或排序会用上$,大多数情况下都使用#
MybatisPlus
继承AbstractMethod可以自定义一些规则生成SQL脚本
多线程
线程
线程优先级
线程的优先级默认1-10,越低则优先级越高,获取CPU时间片的概率相对更高,但是也只是一个指导机制,起不到决定性的作用,并且现在不同的系统对线程调度的策略也不同,一般不会去设置优先级,使用JVM的默认策略即可
守护线程
setDaemon(true),守护线程是一个特殊的线程,它不会阻止JVM退出,当所有非守护线程执行结束时,JVM就会自动退出,不需要等守护线程执行完毕。JVM中垃圾回收器就是守护线程。
生命周期
新建>就绪>运行>阻塞>销毁
new>start>获取CPU时间片运行run方法中的逻辑>wait/sleep>执行完成或异常退出
时间片
如何中断一个正在运行的线程
首先需要拉齐一个概念,线程的运行实际上系统层面的动作,由CPU的调度策略来决定什么时候执行和结束,JVM只是在上面包了一层;所以理论上来说想要从java层面去停止一个线程,只能类似于使用linux里的kill命令一样强制杀死;
java实际上提供了一个Stop命令,但是是不安全的,线程中的任务可能还没有执行完成被stop导致出现不正常的结果;
正确做法是在线程内部埋个钩子,至于具体要不要停止,由线程里的方法自己决定;具体是用interrupt方法配合isInterrupted方法来实现
CAS机制
CompareAndSwap,底层是通过Unsafe类直接操作内存,在操作系统层面保证原子性,有三个操作数:内存值、预期值、更新值,当内存值=预期值时,将内存值更新为更新值;
atomicInteger.compareAndSet(10,20);使用的就是Unsafe的CAS,AtomicInteger只会尝试一次,只返回true、false结果;
某些场景下我们会配合循环达到自旋的状态;
自旋是为了减少线程间切换,但是长时间自旋也会对CPU造成很大开销,一般会加上自旋次数或时间
AtomicInteger atomicInteger = new AtomicInteger(10);
int expected = 10;
int newValue = 20;
while (!atomicInteger.compareAndSet(expected, newValue)) {
// CAS 操作失败,进行自旋
expected = atomicInteger.get();
}
// CAS 操作成功后继续执行下一行代码
ABA问题
使用版本号来解决,AtomicStampedReference设置了预期值、更新值、标记值、最新标记值,只有预期值和标记值都满足的情况下才会更新
一个线程两次调用start()会出现什么问题
会出现IllegalThreadStateException异常,这里的主要原因是线程从创建到销毁一共六种状态,同一个线程多次调用start方法违反线程生命周期规范,属于一种非法操作
New(新创建)
Runnable(可运行)
Blocked(被阻塞)
Waiting(等待)
Timed Waiting(计时等待)
Terminated(被终止)
线程同步
Synchronized
ReentrantLock
JUC-CountDownLatch
JUC-Semaphore
信号量,可以用于做限流,类似于以前的QQ空间的抢车位,车位总数是定的,停满的时候外边的车阻塞,有车离开释放一个信号,外边的车开始抢,需要注意用完要释放
JUC-CyclicBarrier
循环屏障,所有线程执行完任务互相等待,且最后一个线程完成后还可以执行一个制定好的runnable任务;有点像人满发车的场景;实际中比较适合做汇总,比如excel中多个sheet页并发读取,读完汇总处理;
并且所有线程都到达屏障点后,计数器会重置,可以循环使用
https://www.zhihu.com/question/57454399/answer/3192043558
JUC-AtomicInteger
JUC-...
ThreadLocal
为什么使用ThreadLocal?
比加锁更具有效率,各个线程玩自己的数据,没有数据安全性问题
结构/原理
ThreadLocal中有个静态内部类ThreadLocalMap,所有的set、get、setInit方法其实都是操作ThreadLocalMap中的数据;
ThreadLocalMap中有一个Entry数组,每个Entry中存着ThreadLocal(key)和Value,值得注意的是key(ThreadLocal)是弱引用;
ThreadLocalMap是Thread的成员变量,这也是变量副本的关键;
为什么不使用Thread做为key而是ThreadLocal
有些场景下一个线程中不止定义一个ThreadLocal,如果使用Thread做为Key无法正常取值,这也是为什么Thread中维护了一个ThreadLocalMap的原因
为什么Entry中的KeyThreadLocal设计成弱引用?
因为如果是强引用的话,当ThreadLocal变量生命周期走完了,由于key对ThreadLocal是强引用,如果使用了线程池,那么他会一直存在,永远不会被GC,以至于会产生内存溢出;
弱引用时,当有其他ThreadLocal变量调用get、set、remove任一方法都会触发自动清理机制
ThreadLocal会出现内存泄漏吗
会,不remove导致的泄露
如何解决ThreadLocal内存溢出的问题
每次使用完在finally里调用一下remove方法,这样也能避免在使用线程池情况下,产生脏数据影响下次线程的调用
ThreadLocal是如何定位数据的
get、set、remove方法都有一句获取entry数据下标的代码:
int i = key.threadLocalHashCode & (len-1);
相当于用key和entry长度求模算出位置;
ThreadLocal如何解决Hash冲突
一直遍历entry数据循环找下个坐标,形成了一个环,主要方法是nextIndex
父子线程如何共享数据
Thread中除了维护了ThreadLocalMap,还维护了一个InheritableThreadLocal;
使用InheritableThreadLocal可继承的ThreadLocal,他会在创建子线程时将数据拷贝给子线程inheritableThreadLocals
线程池中如何共享数据/线程池间通讯
如果只是父子线程间的一次性数据传递,使用JDK自带的InheritableThreadLocal即可;
但是如果是线程池间的数据通信,可以使用安全共享变量的方式或者阿里的TransmittableThreadLocal,可以实现实时的共享变量传递
使用场景
1.Spring的事务中保证一个线程下,一个事务的多个操作使用的一个Connection
2JDK8之前解决SimpleDateFormat安全问题也可以使用ThreadLocal
3获取当前登录用户上下文信息
4使用MDC保存日志信息
通信与数据共享
共享内存
线程间通过读写内存中的公共状态来进行隐式通信。主要是通过Volatile关键字实现
消息传递
主要是通过wait、notify、notifyAll和join方式
管道流
通过PipedInputStream和PipedOutputStream交换数据
线程池
线程池是如何实现线程复用的(基于阻塞队列的生产者消费者模型)
线程池怎么知道一个线程任务已经执行完成
JUC
CountDownLatch
Semaphore
CycleBarrier
AQS的原理
AbstractQueuedSynchronizer是一个JUC下的一个非常重要的同步框架,像常用的ReentrantLock的公平锁/非公平锁、Semaphore的公平锁/非公平锁、CountDownLatch都是基于它实现的,AQS里有几个很重要的属性,这也是同步的关键,比如它维护了一个独占持有者的变量,标记当前持锁的线程,还有一个用
volatile修饰的保证可见性的state状态值,基于这个值判断当前锁是否被别的线程持有,有则进入一个等待队列挂起,等待释放时唤醒
值得注意的是,针对这个state的操作都是Unsafe的CAS保证安全性
https://www.zhihu.com/question/483996441/answer/2333128650
公平锁时按照顺序去获取锁,非公平时谁设置成功state谁获取锁
AQS为什么使用双向链表而不是单向链表
优势1:双向链表提供双向指针可以在任何一个节点方便向前或向后进行遍历,这种对于有反向遍历需求的场景来说非常有用
优势2:双向链表可以在任意节点位置实现数据的插入和删除,并且这些操作的时间复杂度都是O(1),不受链表长度的影响。这对于需要频繁对链表进行增删操作的场景非常有用
一、存储在双向链表的线程当出现异常不需要再竞争线程时,需要从列表中删除,只需要找到这个节点的前驱节点即可,如果采用单向列表需要从头节点开始遍历,时间复杂度就会变成了O(n)
二、新加入到链表的线程在阻塞之前会判断前驱节点的状态,只有前驱节点是Sign状态的时候才会让当前线程阻塞,所以这里会涉及到前驱节点的查找,采用双向链表能够更好的提升查找效率
三、线程在加入链表之后会通过自旋的方式竞争锁,为了保证竞争锁的公平性需要判断当前线程所在节点的前驱节点是否是头节点,同样使用双向链表能够提高查找效率
总结:采用单向链表不支持双向遍历,而AQS中存在很多需要双向遍历的场景来提升线程阻塞和唤醒的效率
锁机制
Synchronized
介绍
保证原子性、可见性、有序性、可重入性
Synchronized是java关键字,不可被继承,也就是说父类的方法如果是被synchronize修饰,而子类覆盖父类方法默认是非同步的,如果需要保证线程安全,需要显示的在子类方法上使用关键字进行修饰才行。
当然也可以在子类中调用父类的同步方法,这样虽然子类不是同步的,但是调用了父类的同步方法,因此也相当于同步了。
Synchronized不可修饰构造方法,可以修饰代码块;
Synchronized不可修饰接口方法;
用法
可以修饰方法、静态方法、代码块、类(类.class)
* 1. 如果锁的是普通方法和代码块,那么锁的就是该对象。
但是当锁的是静态方法或者类的时候,那么锁的就是整个类,该类的所有方法都是同一把锁。
2. 每个对象都只有一个锁,谁持有谁就可以控制
3. 实现同步的代价相对很大,控制不好还会造成死锁的发生,所以尽量避免无谓的同步控制。
缺陷
当多个线程操作同一个被Synchronized修饰的类或方法时,持锁的线程在没有执行完释放锁之前,其他线程只能阻塞等待。
而Synchronized释放锁的方式只有两种情况:
1.当持有锁的线程执行完该方法之后,自动释放锁
2.执行时发生异常,jvm让该线程释放锁
3.(弃用)Thread有一个stop的方法可以直接杀死线程,但是副作用很大,它杀死持有ReentrantLock重入锁的线程时,并不会调用ReentrantLock的unLock()方法,这样会导致其他线程再也获取不到重入锁了。
*所以如果当持有锁的线程因为IO操作阻塞时,那么其他的线程只能眼巴巴的阻塞等待,无法获取锁。并没有等一段时间不等了、获取尝试去获取一下锁、中断阻塞等待(interrupt)的操作。
Lock
介绍
Lock不像Synchronized是java关键字,而是一个concurrent包下的一个接口;
除了lock()获取锁之外,还可以tryLock()当即或超时尝试获取锁,返回布尔类型判断是否获取到锁,不用一直阻塞等待,获取不到可以处理别的事情。
用法
lock锁需要手动上锁和解锁,一般配合try catch finally使用,会在finally中手动unLock,因为在抛异常时,lock并不会像synchronized一样自动释放锁,如果不手动finally 抛出,则可能会发生死锁的情况。
lockInterruptily() 该方法同样可以获取锁,比较特殊的是他多了一个功能是使用该方法获取锁的线程可以在阻塞等待时候被interrupt 中断阻塞过程。该方法抛出了interrupt异常,所以需要try 或抛出处理。
*interrupt方法并不能中断获取锁并且正在执行的线程,只能是阻塞等待的线程,但是被synchronized修饰的话,线程即使是在阻塞等待,也是不能被interrupt的,只能一直等下去。
ReentrantLock
介绍
Reentrant Lock为lock接口的实现,但是比synchronized更为灵活,他们都是可重入锁和悲观锁,默认情况下ReentrantLock和synchronized一样都是非公平锁(sync只有非公平锁)非公平锁可能会造成线程饿死情况,lock可在创建时参数设置为公平锁,虽然公平锁可以保证队列中的线程能够保证不被饿死,但是吞吐量会大幅下降,主要是因为除了第一个线程,其他的线程都处于阻塞状态,CPU唤醒开销大。
优势
更为灵活,获取锁的过程可控(可中断),支持超时获取,尝试获取等
需手动获取、释放锁
Condition
lock的Condition和object的wait/notify类似,但更为灵活,主要体现在两个方面:
1.condition支持超时等待
2.condition支持指定唤醒持有特定condition的线程
实现的主要功能实际上和object的wait/notify一致
死锁
产生的四大条件
互斥使用,即当资源被一个线程占用时,别的线程不能使用
不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有
循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路
Java自带的jps和jstack工具可以检测DeadLock
避免死锁和诊断
尽可能使用无锁编程,使用开放调用的编码设计
尽可能的缩小锁的范围,防止锁住的资源过多引发阻塞和饥饿
如果加锁的时候需要获取多个锁则应该正确的设计锁的顺序
使用定时锁,比如Lock中的tryLock()
悲观锁
介绍:每次在拿去数据时都会认为有人会来更新数据,所以每次取之前都会把数据上锁,取完数据后释放。在锁释放前,别的线程都会阻塞等待。
特点:会阻塞事务
场景:写比较多的场景
悲观锁和乐观锁并不是特指某一种锁,java中并没有悲观锁和乐观锁这个类,只是一种策略
乐观锁
介绍:很乐观,认为每次读数据时候都不会有人来更新数据,所以不会上锁!! 也就是说其实乐观锁根本就不是锁。在想更新数据前会记录当前的数值,要更改时判断当前数值是否与记录值相同,相同则继续操作,否则回滚重试。
特点:不上锁
场景:很少有数据写入、发生数据冲突的场景。如果是写比较多场景使用乐观锁的话,多线程情况下cpu开销会很大,一直重试,不如使用悲观锁。
CAS算法(比较与替换 无锁算法)
这个是native方法java指令,基本原理就是上面说的,备份旧值、比较、替换
ABA问题
解决办法:加版本号
公平锁
打比方说去肯德基买炸鸡,买的人很多,新来买的人发现前面有人 都很有素质自觉的去排队,那么这就是公平锁。
非公平锁
还是去肯德基买炸鸡,新来的人就是很霸道去插队,导致先来的怂小伙子总是被挤到后面买不到或者很久才买到,这就是非公平锁,所以非公平锁可能会造成饿死(一直获取不到线程)的情况,但是因为不需要排队,系统吞吐量更大一些。
Synchronized和ReentrantLock默认都是非公平锁,但是ReentrantLock可以通过构造参数改变为公平锁,Synchronized没有办法。
可中断锁
前言:java并没有提供直接中断某个线程的方法,而是有相关的机制...当A线程持有锁很久了,线程B一直在等,不想等了B自己给自己发一个中断请求(或者别的线程给B发了中断申请),但是什么时候中断由B自己决定,有点像我妈让我天冷多穿衣服,我就非不穿就要漏脚脖哈哈哈
*Synchronized是不可中断锁,一旦等待就死等...Lock是可中断锁
可重入锁
可重入锁指的是一个线程多次获取同一把锁,如果在一个递归里一直重复上锁操作,不会阻塞自己那么这个锁就是可重入锁,所以可重入锁也叫递归锁。
java里以Reentrant开头的都是可重入锁,Synchronized也是也重入锁,非重入锁没有,只能自己去实现,基本上用不到。
偏向锁
偏向第一个获取锁的线程,当初次执行到同步代码块的时候就出现了偏向锁,
1-当锁没有释放、
2-第二次到达同步代码块时、
3-没有竞争线程、
4-发现持锁的还是自己(保存的有自己的ID信息),
就省去重新获取锁的动作继续执行,性能极高省去开销
轻量级锁
一旦有线程来竞争了(锁竞争:指的是不顺利的获取锁,发现锁现在有人持有,自己尝试获取不到只能等待),偏向锁就会升级为轻量级锁,此时第二个线程会发生自旋(有点类似while true),但是一直空转是很耗费资源的,默认自旋10次(默认10,可以在虚拟机参数更改),超过10次则升级重量级锁。
重量级锁
当成为重量级锁时,后边的线程会直接挂起,等待唤醒。
锁升级
JDK1.6之前Synchronized是重量级锁,如果第一个线程使用了他,下一个线程想要使用就得他释放了才能使用。如果发生阻塞就得一直等下去,所以效率低下。
JDK1.6之后有了锁升级的过程:
升级流程:偏向锁-》轻量级锁-》重量级锁
特点:锁的等级只能升不能降
自旋
lock和synchronized的区别
Synchronized像是自动挡,日常的行驶都能满足。
Lock类似于手动挡,可以漂移骚操作。
TLAB(ThreadLocalAllocationBuffer)
TLAB是一个为线程开辟的私有的内存区域,主要用于在对象分配时优先尝试将对象分配到TLAB,而不是直接分配到堆内存,这样可以避免多线程环境下堆内存的空间开辟和分配以及垃圾对象的回收开销;
场景
生产者消费者模型下的并发消费
为什么使用多线程比多线程比单线程快?
1、一个很大的原因是现代服务器都是多核CPU,多线程可以更充分利用多核CPU的资源,通过获取到的CPU时间片进行并行任务处理,这样很大的解决了单线程阻塞的问题
2.通过并行处理能够很大的提升整体系统的响应速度,像一些应用中间件都引入了多线程来提升系统的并发处理能力,比如Tomcat会将多线程和网络IO结合起来,提升网络IO的并发处理能力
另外文件IO也一样可以使用多线程来提升效率
3.引入多线程可以将一些任务进行分解,这样可以做到一定的解耦和提升灵活性
总的来说在允许的场景下正确的使用多线程是很好的一个选择。既能提升资源利用率 又能提升系统响应速度,还能做到灵活解耦;
多线程带来了哪些问题?
合理的使用多线程确实能够带来很大的收益,但是同样的风险和收益并存,主要体现在多线程在涉及到共享变量时,会有安全性问题,另外多线程的异常不易排除,再者不恰当的滥用多线程反而会对系统造成副作用。
针对安全,略
针对异常排除,略
线程池,略
单线程底层的结构是什么样的?
单线程的底层结构比较简单:
1.程序计数器,记录当前线程运行指令的位置
2.堆栈,主要存储线程存储方法调用和局部变量
3.
分布式事务
分布式事务和普通事务一样,概括成一句话就是要么全部成功要么全部失败,事务都满足ACID(原子性、一致性、隔离性、持久性)唯一不同的点在于普通事务是在单机下产生,分布式事务是在多个服务间产生的。举例来说,像电商中一个下单流程吧,所涉及到的环境可能会很多,如果下单这个动作成功了,但是优惠券扣除失败了,或者是积分新增失败了,都是问题,前者电商平台被薅羊毛,后者消费者不爽,所以所有相关业务都需要保证成功执行,这种情况下就需要使用到分布式事务了
https://www.zhihu.com/question/64921387/answer/1023714489
分布式事务策略有很多
我们用seta的二阶段提交和最终一致
2pc(两段式提交)
有点像媒婆(MQ),最简单的分布式事务实现,当两个事务都先告诉媒婆这个协调者自己准备好了的时候,媒婆给大家发通知,都可以提交事务了
https://zhuanlan.zhihu.com/p/183753774
但是会存在弊端,比如A服务提交成功了,B服务在提交阶段由于网络波动等原因提交失败了,那就出现了破坏事务的情况。这个时候引入最终一致性是比较好的处理方式
3pc(三段式提交)
3PC 用超时机制,解决了协调者故障后参与者的阻塞问题,但与此同时却多了一次网络通信,性能上反而变得更差,也不太推荐。
TCC(Try、Confirm、Cancel)
TCC 对业务的侵入性很强,一般是不采用,分别指 Try、Confirm、Cancel ,一个业务操作要对应的写这三个方法。
以下单扣库存为例,Try 阶段去占库存,Confirm 阶段则实际扣库存,如果库存扣减失败 Cancel 阶段进行回滚,释放库存。
TCC 不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过则 Cancel 来进行回滚补偿,这也就是常说的补偿性事务。
原本一个方法,现在却需要三个方法来支持,可以看到 TCC 对业务的侵入性很强,而且这种模式并不能很好地被复用,会导致开发量激增。还要考虑到网络波动等原因,为保证请求一定送达都会有重试机制,所以考虑到接口的幂等性。
最大努力通知
和最终一致性一样也是这个柔性事务思想的实现,特别适合调用结果进行异步通知的业务中,特别是与第三方系统进行对接的过程中,比如像电商平台和银行系统,支付成功后,银行系统通过MQ尽最大努力通知电商服务
第三方银行系统的支付服务完成订单款项的支付,由于这里支付结果是异步获取的。所以需要等待银行系统在完成相关支付业务后,通过回调接口来通知我们系统的支付结果。但很多时候由于存在网络异常、回调接口发生异常等意外因素,第三方为了尽最大努力进行结果通知,往往会将相关结果通过MQ投递到通知服务,以便单独进行重复、多次的结果通知
但如果我们从第三方系统的角度考虑,如果调用回调接口一直失败,总不能一直这么重试下去啊。所以在最大努力通知的方案中,不仅需要通知的发起方(即这里的第三方银行系统)提供结果通知的重试机制,还需要给通知的接受方(即这里的电商平台)提供一个用于主动进行结果查询的接口。这样即使当银行系统的通知次数达到阈值,不再调用回调接口进行结果通知时,我方服务也可以在之后通过银行系统的查询接口获取相应结果
本地消息表
最终一致性的一种典型实现方案吧,只是在数据库中再增加一张本地消息表
最终一致性
最终一致性呢,其实就是基于消息中间件的两阶段提交,需要引用可靠消息队列,将本地事务和发消息放在同一个事务里,保证本地操作和发送消息同时成功。图:
分支主题
自由主题
收藏
0 条评论
下一页
为你推荐
查看更多