问:并发事务带来了哪些问题?

 并发事务可能会出现三种问题。

1.脏读:事务1读取到事务2未提交的数据。

2.不可重复读:事务2先后读取事务1中的某合个数据,两次的结果不一样。

3.幻读:一个事务在按条件查询数据时没有查到数据吗,但是插入操作时,又发现该数据已经存在。因为其他事务在这个过程中插入数据(选答)

怎么解决解决这些问题?

通过设置过隔离级别来解决。隔离级别包括:

1.读取未提交,无法解决并发事务带来的问题。

2.读取已提交,可以解决脏读。

3.可重复读,可以解决脏读,不可重复读。

4.串行化,事务只能一个一个执行,可以解决脏读,不可重复读,幻读,隔离级别最高,效率最低。

MySQL默认的隔离级别是什么?

Mysql默认使用的隔离级别是:可重复读。

问:undo log和rado log有什么区别?

redo log:用于记录数据页的物理变化,当服务宕机的时候进行数据同步操作。保证了事务的持久性。

undo log:记录逻辑日志,就比如:当做插入操作时会在日志中记录逆向的操作也即是删除,在事务回滚的时候会执行逻辑日志中的指令。保证了事务的持久性和原子性。

问:隔离级别是怎么实现的?

排他锁+MVCC实现的。 

问:说说你对MVCC的理解吧?

多版本并发控制。维护一个数据的多个版本,使得读写操作没有冲突。

mvcc主要有三个重点:

1.隐藏字段:trx_id(事务id):记录当前事务的id,其为自增的。 roll-pointer:指向上一个版本的事务记录地址。

2.undo log:回滚日志,存储老版本的数据,版本链:多个同时修改某条记录,产生多版本的数据,通过rool-pointer指针形成链表。

3.readview:解决一个事务查询选择版本的问题。

根据readView的匹配规则和当前事务id找到对应的版本信息。(问规则时答:1.判断是事务id是否为当前事务的id。2.是否是活跃事务id。3.判断事务是否是在readview创建后开启的,也就是事务id大于当前事务。4.判断事务中的数据是否已提交,事务id小于最小的事务id。)

不同的隔离级别快照读是不一样的,最终的访问结果也是不一样的。(问时答:当前读:读取的是最新的数据并且会加锁。快照读:读取的是记录数据的可见版本,不会加锁。)

读已提交:在每次快照读的时候都会生成readview。

可重复读:在有在第一次快照读的时候才会生成readview,后续的快照读都是使用该readview的复制,保证数据的一致性。

  • 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

    需要全套面试笔记及答案【点击此处即可】免费获取

问:隔离级别中可重复读有什么缺点呢?

1.无法解决幻读的问题,当我们事务去读取一定范围的数据,且在该过程中其他的事务修改了该范围的数据,此时两次读取就会出现查询结果不一致的情况,最终导致幻读。

2.无法读取到最新的数据,因为可重复读在每次读取的时候都使用同一个readView,且readView并非当前最新的数据,最终导致无法读取到新的数据。

 问:MySQL的主从同步有了解过吗?

Mysql主从同步的核心就是bin log(二进制日志),这个日志中主要记录 DDL(表的操作),DML(表中数据的操作)。

85cb28ed41fc4ecfa01cb051297579bd.png

1.master中事务提交数据后,会将修改的数据保存到bin log中。

2.slave有个iothread线程会监控的bin log的变化,并将变化写入relay log中。

3.slave有个SQLthread线程会监控relay log,将改变的数据写入slave中。

问:你在项目中有使用过分库分表吗?

在物流项目中的订单服务的数据非常庞大,请求数多且业务累计大。差不多单表的数据有100w条,这时我们就使用分库分表。

分库分表有四种策略:

1.水平分库:通过将一个库中的数据拆分到多个库中,解决海量数据存储和高并发的问题。主要通过sharing-sphere和mtycat实现。

2.水平分表:解决单表存储和性能的问题。

3.垂直分库:根据业务来拆分库中的表,在高并发的情况下提高磁盘IO和网络连接数。每个微服务都有自己的表。

4.垂直分表:冷热数据分离,多表不会相互影响。就比如:表中字段为id,name,des,将id,name和des分离,id和name都是热数据而des为冷数据,访问频率较低。

框架八股文
问:Spring中的设计模式有哪些?

我们目前主要了解的就是工厂模式,代理模式,单例模式,策略模式,责任链模式。

工厂模式:常见的工厂模式主要就是BeanFactory(延迟注入)和ApplicationContext(完全注入),我们无需知道类如何创建,直接从工厂中获取即可。

单例模式:ioc默认情况下就是一个单例模式,每次获取相同恶的类的时候,获取的对象是同一个。不会就行多次的创建。单例模式还分为饿汉式(主动创建单例)及懒汉式(需要时再创建单例)。

代理模式 :aop就是采用代理模式来实现的。当要代理的对象实现接口的时候,我们就会使用JDK Proxy来生成代理对象。如果代理的对象没有实现接口的话,我们就回使用CGLIB生成一个被代理对象的子类作为代理对象。

策略模式:在我们编写项目的时候就有使用到策略模式,因为我要控制分布式锁的失败策略,我们会在枚举类编写一个抽象方法,编写多个内部方法去重新该方法,不同的内部方法就是不同的策略。
责任链模式:stram流就是使用该模式的,按照对应的顺序去执行方法,并向下传递对象。起到解耦合的作用。

问:spring框架的单例bean是线程安全的吗?

在spring框架中有个注解叫@Scope可以设置bean的状态,默认就是singleton也就是单例。

bean进行注入的时都是无状态的,其不会被修改的。所以没有线程安全的问题。但是如果bean中有成员变量时就可能会有线程安全的问题,因为该成员变量可能会被多个线程修改,为了解决这个问题我们可以加锁或将bean设置为多例。(@Scope设置为prototype)

问:什么是AOP?

面向切面编程,将那些于业务无关的复用性比较高的代码快抽取出来,较低代码的耦合度。

问:在你的项目中有使用过AOP吗?

在我的云盘项目中就使用到AOP,在记录日志的时候,我创建有个自定义注解,aop的切面就是这个注解,使用环绕通知在方法中,我们通过传入的参数(joinPoint)获取对应的类和方法,从而获取前端传来的参数和其他主要信息,实现记录日志的效果。

问:Spring中的事务是怎么实现的?

本质就是通过AOP实现的,通过环绕通知对应方法进行前后拦截,在方法执行前开启事务,在执行后提交事务,会对此过程进行try/catch,如果报错直接回滚。(就是@transactional)

问:spring中事务失效场景有哪些?

1.在出现异常后,方法中try/catch了该异常并且没有主动抛出异常,这时候就会导致事务失效。解决方法:在方法try/catch异常后手动的抛出异常。(就会导致事务不知道出现异常了)

2.抛出检查异常时会导致事务失效,spring中的事务只会对runtime异常进行回滚。就比如:Not found Exception就是检查异常。解决方法:在@transactional中设置属性 rollbackFor = Exception.class。使得事务会对所有的异常进行回滚。

3.非public方法会导致事务失效。解决方法:将方法的作用域该为public。

4.在非事务方法中调用了事务的方法,此时就会导致事务失效。(就比如某给没有加事务注解的方法调用了加了事务注解的方法)

5.回滚异常类型不匹配。(我们可能会设置需要进行回滚的异常,就是rollback的值,如果抛出的异常类型不匹配就会导致事务失效)

6.事务的传播行为错误。(就比如在事务方法中调用了其他的事务方法,初始化如果其他的时候方法设置开启新事务的话,在其事务成功后,就不会参与外部事务的回滚操作)

61b16202cfc74859952c5a156f16e7ae.png

问:事务的传播性有哪些?

主要就是三个:Propagation_Required,Propagation_Required_new,Propagation_nested。

  1.   Propagation_Required:如果B为A的子方法,并且都为该传播类型,那么它们都会在同一个事务中。如果主方法中没有开启事务的话就会开启新事务。
  2.   Propagation_Required_new:如果B为A子方法,B会创建一个独立的事务,B不会受A事务的影响。
  3.   Propagation_nested:如果B为A子方法,B会创建一个事务内嵌到A的事务中,如果A中没有事务的话,就单独开启一个事务。
问:@Transactional的原理有了解过嘛?

事务注解是基于AOP来实现的,也就是在执行方法前开启事务,如果try/cath捕获到异常的话就会进行回滚,在方法执行完成后就会进行提交。那AOP实现涉及到动态代理的流程。如果被代理目标对象实现了接口的话,就会使用JDK Proxy生成代理对象。如果代理对象没有实现接口的话,就会使用CGLIB Proxy生成被代理对象的子类作为代理对象。

问:说说CGLIB的执行流程?

问:spring中bean的生命周期有了解过吗?

结构图:

c2b1dcf71aaf44e5a14ed868a1a95c82.png

生命周期的流程为下:

1.通过BeanDefinition获取bean的定义信息。

 2.通过构造函数创建bean,可以将当前的bean理解为一个空壳。

3.进行依赖注入,对bean中的属性进行赋值。

4.处理Aware接口,也就是一些以Aware结尾的接口,就比如:beanNameAware,beanFactoryAware,applicationContextAware。如果实现了个接口的话,我们需要重写一些方法。

5加载BeanPostProcessor对象,并执行处理器的前置初始化方法(PostProcessorAfterInitiazilation方法)。

6.执行初始化方法,包括 IntializingBean和自定义的初始化方法。

7.加载BeanPostProcessor对象,并执行处理器的后置初始化方法(PostProcessorBeforeInitiazilation方法)。在此后置处理器中我们可以通过aop对原始的bean做增强也就是进行代理,代理就包括 JVM代理和CGLIB代理。

8.将bean进行销毁。

问:说说CGLIB动态代理的执行流程吧?

aop就是采用代理模式来实现的。当要代理的对象实现接口的时候,我们就会使用JDK Proxy来生成代理对象。如果代理的对象没有实现接口的话,我们就回使用CGLIB生成一个被代理对象的子类作为代理对象。

问:有了解过bean的循环依赖吗?

循环依赖也就是循环引用,两个或以上的bean同时相互依赖对方,最终形成闭环。就比如:A依赖B,B依赖A。

spring提供了解决方案:三级缓存。

1.一级缓存:单例池,存储已经初始化完成的单例bean。

2.二级缓存:缓存早期的bean单例对象,就bean只完成到执行构造方法。

3.三级缓存:缓存创建bean的factory,这些factory用于创建代理对象和普通对象。

11fa6615f5f9475e8aff8cf805f13bc0.png

三级缓存的解决流程(当前问题为A,B相互依赖):

 实例化A,并将生成的A的objectFacttory将其存入三级缓存,因为A依赖B,B也会去实例化,创建对应的objectFactory存入三级缓存。

此时B依赖A,就会通过三级缓存中A的objectFactory生成早期A实例,并将该A实例存储到二级缓存中,将这个早期的A注入B中,此时B就创建完成了,将B实例存储到一级缓存中。

将完整的B注入A中,将A实例存储到一级缓存中,并将二级缓存中A的实例删除。

问:构造方法中出现循环依赖怎么办?

在构造函数中如果存在循环依赖我们在函数的参数上添加@Lazy就可以解决循环依赖的问题,保证bean在需要的时候才去加载。

问:可以只使用二级缓存来解决循环依赖吗?

在没有Aop的时候就可以使用一级和三级缓存来解决依赖。但是呢,如果存在aop的问题,那就必须使用三级缓存,因为在二级缓存中可以保证即使存在多个半成品bean的引用时,始终会返回相同的代理对象。避免Bean有多个代理对象。

问:除了使用三级缓存解决循环依赖,还有什么方法吗?

使用@Lazy就行懒加载,就比如A,B相互依赖。但是呢A添加了@Lazy注解,此时会创建一个B的代理对象,而非B对象,会将这个代理对象注入到A中,完成A的创建。之后会做B的初始化和实例化,会将创建好的A注入到B,最终完成B的创建。

问:SpringMVC的执行流程有了解过吗?

4e3c2253202c4e2aaf71386ab85f530e.png

jsp版本:

1.用户发送请求到DispatchServlet。

2.dispatchServlet调用处理器映射器HandlerMappering,处理器就会去Controller中找到对应的方法通过映射的路径,然后将处理器执行链返回给dispatchServlet。

3.dispatchServlet调用处理器适配器HandlerAdaptor找到对应的处理器,该处理器就会处理对应的参数和处理返回值,最终会返回ModelAndView给DispatchServlet。

4.Dispatch调用ViewResolver视图解析器,并将ModelAndView传入,最终返回View给dispatchServlet,通过View渲染视图。(就比如:JSP)

前后端分离版本:

1.用户发送请求搭配DispatchServlet。

2.DispatchServlet调用处理器映射器HandlerMappering,会去Controller中找到对应的方法,最终处理器返回执行链给DispatchServlet。

3.DispatchServlet会调用处理器适配器HandlerAdaptor找到对应的处理器,该处理器就会处理对应的方法的参数和处理返回值,在方法上添加了@ReponseBody。

4.通过HttpMessageConverter将数据转为Json返回

问:有了解过springboot的自动装配原理吗?

在启动类上有个注解的叫@SpringbootApplication,在该注解中包含springbootConfiguration表示该类为配置类,还有个注解叫@EnableAutoConfiguration,这个就是实现自动装配的核心注解。

在@EnableAutoConfiguration中使用注解@Import引入了一个自动自动装配的选择器。

该选择器会到jar中的 /META-INF/spring.factories中按照条件加载对应的类,并将该类配置到ioc中。这里面添加的注解就包括:@ConditionOnClass表示当某个类存在时才进行类加载。@ConditionOnMissingBean表示当某个bean不存在的时候才进行类加载。

问:spring中有那些常见的注解?

在Spring中主要注解有:@Componment,@Controller,@Service,@Repository将类配置到ioc中,@AutoWired(根据class),@Qualifier(根据名字)实现依赖注入。@scope设置bean的作用范围。@Configration设置配置类。@ComponetScan主键扫描。@Bean将某个方法的返回值配置到ioc中。@Import将某类导入ioc中。@Before,@After,@Aspecr,@Around,@Pointcut切面编程的注解。

0d7b1337a2e049859be8aae3469ef44e.png

 在SpringMVC中就是一些关于请求的注解,@RequestMappering,@ReponseBody,@RestController,已经参数的注解 @RequestParam,@PathViriable,@RequestHeader。

fd9109dffa3a484889062a3d2f240448.png

在springboot的注解中就包括:@SpringBootConfiguration,@EnableAutoConfiguration自动装配注解,@ComponentScan之间扫描。

6a87f5d8eeb0460186ad22060402a65f.png

问:能说说你对Mybatis的执行流程的理解吗?

 1.读取mybatis-config.xml文件,里面就是数据库的配置和mapper的地址。

2.创建SqlSessionFactory。

3.通过SqlSessionFactory创建对应的SqlSession,就是项目和数据库的会话,SqlSession包含执行sql语句的所有方法。

4.执行操作数据库的接口,Executor执行器,同时负责缓存的维护。

5.在Executor执行器中的MapperStatement对象,当操作数据库的时候会将Java的类型转为数据库的类型,当输出结果的时候会将数据库的类型转为Java的类型。

cbcaf3572b934c9e8ef56b60dec4da5a.png

 问:Mybatis支持延迟加载吗?

1.延迟加载就是当需要使用数据的时候才去加载数据,不用数据得时候不会主动加载。

2.Mybatis支持一对一关联对象和一对多关联集合的延迟加载。

3.在mybatis的配置文配置文件中的LazyloadEnabled设置为true就开启了全局延迟加载。

问:Mybatis延迟加载的底层有了解过吗?

1.通过GCLIB的代理实现的。

2.当调用目标函数的时候,会调用拦截器的invoke方法,如果发现目标方法中的值为null,则进行sql查询,在获取数据后通过set设置属性值,在后续调用目标方法时就有值了。

问:有了解过Mybatis的一级缓存和二级缓存吗?

一级缓存:基于PrepetualCache的HashMap的本地缓存,是默认开启的,其作用域就是Session,当Session进行了Close,Flush后该Session中的缓存会被全部清空。

二级缓存:其作用域是namespace和mapper,其默认是不开启的,是基于prepetualCache,hashmap存储的。通过在mybatis的配置文件中设置CacheEnabled设置为true,在对应的mapper中添加标签Cache。

问:Mybtis的二级缓存什么时候会清理?

当某个作用域(一级缓存(session)/二级缓存(namespace))中进行了增删改操作时就会清除掉select中的缓存。

问:Mybtis中#{}和${}的区别?
  1.    #{}主要就是做占位符的替换的,在mybatis中会使用?占位,而${}主要就是做文本替换的。替换${}中的文本。
  2.   #{}数据的替换发生在DBAS之中,而${}数据替换发生在DBAS之外。
  3.   #{}在做数据替换的时候会自动添加'',而${}数据替换的时候不会自动添加''。
  4.   #{}可以防止SQL的注入,而${}不可以。
微服务

问:springCloud的五大主件有了解过吗?

springCloud: 

注册中心:Eureka。

远程调用:Feign。

负载均衡:Ribbon。

服务保护:Hystrix。

网关:Gateway。

springCloud alibaba:

注册中心:Nacos。

远程调用:Feign。

负载均衡:Ribbon。

服务保护:Sentinel。

网关:Gateway。

f1586c6e868047929759e474a4bfce97.png

问:服务的注册与发现是什么意思?SpringCloud是如何实现服务的注册和发现的?

在我的项目中使用nacos实现服务的注册与发现。那我就以eureka为例吧。

服务的注册:每一个服务的提供者会将自己的信息注册到eureka中, eureka会储存提供者的信息,包括:服务名,ip,端口等等。

服务的发现:消费者会通过eureka拉取对应的服务列表,服务可能会搭建集群,所以会存在多个服务,这时候消费者会根据负载均衡的算法选择一个服务并进行调用。

服务的监控:服务者会每隔30秒发送心跳,保证自己的健康状态,当注册中心发现某个服务90秒都没有发送心跳,则会将该微服务移除。

67475811158941dbbeaf900187ce5ca8.png

问:我看你项目中使用nacos,你能说说nacos和eureka的区别吗?

相同点:

1.都支持服务的注册和发现,整个流程也是非常相似的。

2.都支持服务者向注册中心发送心跳,监控监控状态。

不同点:

1.nacos的注册中心支持自动查询服务者的状态,如果服务者是临时实例时则使用心跳模式,如果服务者是非临时实例时就采用自动检测模式。

2.临时实例如果发现心跳不正常时就会移除对应的服务,而非临时实例即使有异常也不会被移除。

3.nacos的注册中心支持向消费者发送更新后的服务列表,更新更及时。

4.nacos的临时实例使用的AP模式(高可用),eureka页使用AP模式,非临时实例使用CP模式(强一致)。

5.nacos还支持作为配置中心,而eureka则不支持。

 9a8ed08f133b4e17bafeef561746a3c5.png

问:你们项目中的负载均衡是怎么实现的?

通过Ribbon实现的,在我们feign调用服务时的负载均衡就通过Ribbon实现的。

问:Ribbon的负载均衡策略有哪些?

 1.轮询策略。

2.权重策略:按照权重的大小选择服务器,响应的时间越长其权重就越小。

3.随机策略。

4.区域敏感策略:根据消费者区域按照就近原则选择服务器,如果没有区域这个概念,其效果就和轮询策略效果一样。

问:如何自定义负载均衡的策略?

1.通过实现接口IRule设置对应策略并设置到ioc中 ,其的作用域是全局的。

2.通过yml配置文件进行配置,需要指定对应的服务者的名字,其作用域是局部的。

问:什么是服务的雪崩呢?怎么解决这个问题呢?

服务的雪崩:当某个服务调用失败后,导致后续调用的服务全部失败,造成服务雪崩发生。

解决方案

服务的降级:如果某个服务调用失败后直接返回自定义的降级信息。通过实现Feign接口,并在Feign接口的@FeignClient中设置fallBack为实现类。

服务熔断:可以通过Hytrix或Sentinel实现服务熔断,那我就以Hytrix为例吧,其默认是没有开启的,需要我们在启动类上添加注解@EnableCircuitBreaker进行开启。当调用某个服务10s时,发现有50%以上的调用失败就会触发熔断机制(开启),每隔5秒会放行一个请求去请求服务(半开),当发现服务调用失败了就会继续熔断,如果请求成功则会解除熔断(关闭)。

 问:你们微服务是怎么做监控的?

在我们的项目中主要是用Skywalking完成监控的。

Skywalking主要监控监控,服务的状态。特别是在做压力测试的时候就能够找到对应的慢接口,并对接口进行修改。在Skywlaking中还有服务间调用的拓扑图,方便我去理清调用关系。

我们可以在Skywalking中设置警告规则,当我们的项目上线后,发生错误就会向我们设置的管理者发送短信和邮箱信息,保证第一时间进行修改。

问:你们的项目中有没有使用过限流?是怎么实现的?

之前要学习过抢票的业务就使用到限流,因为当时的QPS到达2000左右,为了保证正常的运行我需要做限流,通过限流将QPS控制在1000左右。

实现方案:

Nginx

1.控制速率:使用漏桶算法实现。可以存储一定数量的请求,按照一定的速率处理请求,如果进入漏桶的个数大于最大值则会直接丢弃。

439efb33e3e44b06ae96b6cacae24830.png

2.控制并发数:控制单个ip的链接数和并发链接的总数。

网关

通过GateWay中的提供过滤器RequestRateLimiter实现,其思想就是令牌桶思想,每个请求需要携带令牌才能进行访问。我们可以设置令牌桶的容量和令牌每秒生成的个数,在GateWay的配置文件中进行配置。

f16263a8417e4fe3a9d05a0d5ec1da42.png

 问:限流的算法有哪些?有什么区别呢?

主要就是漏桶算法和令牌桶算法。不同于漏桶算法按一定速率处理请求,当令牌桶中还有令牌,且被处理的请求数大于生成令牌的速率时,执行的请求个数就会大于每秒生成的令牌数。

问:能解释一下CAP和BASE吗?

CAP:一致性(c),可用性(a),分区容错性(p)。

1.分布式系统的节点都是通过网络连接的。所以一定会出现分区的问题(因为网络的问题)。

2.因为分区的出现我们就不能同时满足可用性和一致性。

3.cp为强一致性(nacos的非临时实例就是采用此模式),ap为可用性(eureka和nacos的临时实例就是常用此模式)

BASE

基本可用:分布式出现故障的时候,允许损失部分的可用性,保证核心可用。

软状态:允许出现中间状态。比如:临时不一致的情况。

最终一致性:在软状态结束后,保证最终的一致。

解决分布式事务的思想和模型

最终一致性:各个分支的事务都进行提交,彼此结果不一样就执行相反的操作(插入->删除)保证最终的一致性。
强一致性:各个分支的事务在执行完业务之后不会提交,都在等待彼此的结果,最终一起提交或回滚。

问:你们是采用哪种分布式事务来解决问题的?

在我们的物流项目种主要使用RabbitMQ来实现分布式事务的。

MQ 

在我们调用服务A的时候,做完一系列的判断操作后就去执行数据库的操作,此时也会调用消息队列发送消息到其他的服务去,此时消息队列操作和数据库操作是在同个事务中的。因为其是异步的,所以性能比较好。(可以拿做缓存举例子:双写一致性)

Seata

XA模式:强一致性(cp),各个分支事务不会提交事务,都在等待彼此的结果,最终一起提交或回滚。

TA模式:最终一致性(ap),各个分支事务都会进行提交,当彼此结果不一样的时候就会通过undo(回滚日志)进行逆操作,保证数据最终的一致性。 

TCC模式:最终一致性,类是TA模式,性能比较好。但是需要人工编码,耦合度比较高。

问:你们分布式服务中接口的幂等性性时怎么设计的?

在我们物流项目的订单支付模块就需要考虑幂等性的问题,防止用户重复支付。保证多次调用订单支付的接口和调用一次的结果时一样的。我们主要通过分布式锁和交易单状态的判断来保证接口的幂等性。(将模式官引导到物流项目中)

在我们项目中判断交易单的状态有:已结算,免单,支付中,取消订单,挂账(累计结算)。

接口会传入支付状态。

1.如果是已结算和免单直接返回错误信息。

2.如果是支付中,则需要判断支付方式是否改变,如果改变则生成新的交易单作为新的数据,如果没有改变则直接返回错误信息。(传入订单Id,去匹配对应的交易单信息,如果没有就创建)

3.如果是取消订单和挂账则生成新的交易单作为新的数据。

解决方案(以支付为例)

1.redis + token,token通过UUID生成,在生成订单的时候生成token,将token存到redis中,请求会携带这个token进行,先判断token是否存在于redis中,如果存在就进行支付然后删除redis中的token,最终保证幂等性。(但是没有考虑更换支付方式的问题)

2.分布式锁,保证每次只有一个支付请求,性能比较低。

问:xxl-job的路由策略有哪些?

1.轮询:轮流被选择。

2.故障转移:通过心跳机制,找到第一个健康实例。

3.分片广播:通过广播触发触发定时任务。适用于任务数量多的时候。在我们的物流项目中定时任务就是使用分片广播。(引导面试官到你的项目中)

问:xxl-job任务执行失败怎么解决?

故障转移策略 + 设置重试次数 + 在设置邮箱告警。

问:如果大量的任务需要被执行,怎么解决?

使用分片广播,在代码编写上我们可以获取当前xxl-job节点的索引和节点的总数。通过取总数模来确定执行任务的节点。

在我们的计算运力模块中,司机执行运力计算时就需要执行定时任务。通过去模的方式选择对应的节点来执行计算运力。那再这个过程中我们为了保证用户的体验,我们需要给在计算运力的时候添加分布式锁,保证同一个用户的商品尽可能的在同一辆车上。

消息中间件

问:RabbitMq如何保证数据的不丢失?

在我们物流项目中的支付模块中的支付方式的模板做了缓存需要达到mysql,redis双写一致性,我们使用RabbitMq来实现的,此时我们就需要把证Rabbit中数据的不丢失。

我们需要从三个方面进行考虑:

1.开启生产者的确认机制,成功返回ack,失败则返回nack,包括publisher-confirm机制和publisher-return机制,前者作用在生产者发送消息到交换机和消费者消费消息,后者作用在交换机发送消息到消息队列中。

2.开启持久化功能,设置交换机(在创建交换机的函数中可以设置为持久化),消息队列(通过queueBuilder中设置持久化),消息的持久化(可以在创建消息时设置其模式)。

3.开启消费者的确认机制,设置消费者的确认机制的模式为auto,在spring处理消息成功后返回ack(此时就会将消息从消息队列中删除),反之返回nack,我们可以通过spring的retry机制设置重试次数,在我们的项目中设置为三次,如果三次都失败了,则直接将消息发送到异常队列中,人工去处理异常的消息。

  • 篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

    需要全套面试笔记及答案【点击此处即可】免费获取

问:RabbitMq中的消息重复消费问题怎么解决?

之前在写项目的时候就遇见过这种问题,因为网络的问题,消费者没有及时的发送ack,然后此时消费者服务又宕机了,在服务重启后就会重复消费消息。

解决方案:

1.为每条消息设置一个唯一的Id标识,当消费者在消费消息的时候会先判断id在数据库中是否存在,如果不存在就消费信息,如果将id存储数据库中,如果存在则就不消费消息。

2.使用分布式锁,其性能比较低。

 问:RabbitMq的死信队列又了解过吗?(RabbitMq的延迟队列有了解过吗?)

在我们的物流项目中,快递员的上门取件就使用延迟队列,当前时间大于取件时间的前两小时时就会做发送延迟消息。

解决方案:

延迟队列的实现方式就是:死信队列 + TTL

1.在我们的消息队列中通过属性 dead-letter-exchange设置死信交换机。自定义死信交换机和绑定对应的自定义的死信消息队列。给需要延迟发送的消息设置ttl。

2.当消息队列中的消息过期时,消息被拒接消费时,队列满之后,消息就会进入私死信队列中。

3.我们就可以设置一个消息队列用于存储需要延迟的消息,当消息过期后直接进入死信队列中,消费者去消费死信队列中的消息达到延迟处理的效果。我们项目中就是这么实现的。

下载延迟队列插件(问还有什么解决方案时答)

这个我只了解过,其使用就是在配置交换机的时候设置属性delayed为true,然后在创建消息的时候添加头信息 x-delay头,值为过期时间。

问:RabbitMq的消息堆积该怎么解决呢?(如果有100万的堆积在RabbitMq的消息队列中该怎么办)

在项目中目前没有遇到过这种情况,但还是有有了解过解决方案的。

1.消息堆积的原因是因为消费信息的速度小于生成消息的速度,所以我们可以多增加几个消费者。

2.在消费者中开启线程池加快消费速度。

3.找到消息队列的容量。通过间消息队列设置为惰性队列,造创建队列的时候使用lazy方法实现。

惰性队列的优点:存储空间大。缺点:因为消息都是存储在磁盘上的,所以需要做IO操作,效率比较低。

问:RabbitMQ高可用机制有了解过吗?

在我们的生产环境下,当时采用时镜像模式搭建的集群,总共三个主节点,所有的操作都是在主节点上完成的,并会将数据同步到镜像节点上。在主节点宕机后会有镜像节点作为新的主节点。

此时存在一个情况,当主节点操作完成时,就给宕机了,数据没有即使同步,导致数据丢失。

问:那出现这种数据丢失该这么解决呢?

我们可以使用仲裁队列,它和镜像模式一致,也是一种主从的模式,其遵循Raft协议,具有强一致性,其配置起来是很简单的,在创建队列的时候调用可以设置仲裁队列方法(quorum方法),一个q开头的方法,名字现在忘了。

问:kafka是如何保证消息不丢失的?

b55f441c07ef40d2a4f9c96c00591d83.png

1.服务者发送消息到broker中时可能会出现数据丢失的情况。

解决方案:在我们异步发送消息的时候会重写一个回调方法,这个方法有个次数就异常类,我们可以判断该异常是否存在进行重写发送消息。可以给kafka设置重试次数的参数配置。

2.消息在broker中可能会出现消息丢失的情况。

解决方案:确认发送就是ack机制,我们可以设置ack的值来防止消息的丢失,ack=all就是保证消息存储到主从节点后才进行确认。但在我们的项目中 一般设置ack=1,也就是leader节点存储数据后就进行确认。

3.消费者从broker中获取的消息丢失。当存在多个消费者时,每个消费者分区进行消费,每隔5秒都会自动提交已消费的各个的偏移量(offset,此时就可能出现提交偏移量后消费和消费后提交偏移量)。情况1:当某个消费者在消费完消息后未提交偏移就宕机了,此时其他的消费者就会使用就得偏移量消费信息,就出现重复消费的情况。情况2:当提交完偏移量后还没消费完服务就宕机了,此时其他消费者就用新的偏移量来消费消息,就会出现消息丢失的情况。

解决方案:关闭主动添加,使用手动提交。提交发送使用 同步 + 异步提交。(先在消费消息后异步提交,在最后再进行同步提交)

问:kafka是如何保证消费的顺序性的?

在kafka存储消息时会将消息存储到不同的分区中不能保证消息的顺序性。

解决方案:

1.为需要顺序性的消息设置相同的分区,通过kafkaTemplate在发送消息时进行设置。

2.为需要顺序性的消息设置相同的key,通过hash函数计算到相同的位置,将信息散列到同一个分区中,通过kafkaTemplate在发送消息时进行设置。

问:kafka的高可用机制有了解过吗?

kafka的高可用机制就是集群和副本复制机制。

集群:在kafka集群中会有多个broker,即使有某个broker宕机也不会服务的使用。

副本复制机制:一个topic会有多个分区,一个分区会有多个副本,副本分为leader和follower,副本会存储在不同的broker上,当副本中的leader挂掉后就会选择一个follower作为新的leader。

问:能解释一些复制机制中的ISR吗?

ISR(in-sync-replica):同步复制。也就是此时的副本需要leader的同步复制,而不同的副本则是异步复制,因为异步复制可能会出现数据丢失的情况,所以ISR副本的数据更加接近leader,在leader挂掉后会优先使用ISR副本作为新的leader。(我们可以在broker的配置文件中设置ISR的个数)

问:kafka的消息清理机制有了解过吗?

59bea442177f47f08c7d0d7b092317c6.png

先说说数据的存储机制吧。

每个topic会有多个分区,每个分区又有多个分段segment,分段主要以索引文件和日志文件存储在磁盘中的,其优点就是减少单个文件的内容大小,加快查询速度和消息清除的速度(索引和日志文件的清除)。 

数据清除策略

1.根据消息的保留时间进行清除,当消息的保留时间超过指定的时间时就将其清除。默认的指定时间是7天。

2.根据topic存储的大小进行清除,当topic存储的大小到达阈值时就会触发清除,删除掉存储最久的消息,默认是不开启的。(在broker配置文件中开启)

问:kafka实现高性能的设计有了解过吗?

1.消息分区:消息不再局限于存储到单个服务器上。可以处理更多的数据。

2.顺序读写:磁盘采用顺序读写,速度更快。

3.页缓存:把磁盘数据先缓存到内存中,在读取数据时将磁盘读取变为内存读取,速度更快。

4.零拷贝:减少数据的拷贝,加快速度。原本的:系统资源->页缓存->kafka->socket缓存区->网卡最终发送给消费者此时为四次拷贝,新的:系统资源->页缓存->网卡最终发送给消费者,新的拷贝比原来少两次。速度更快。

4b838200d78843b3be9df85efc37794f.png

4e20c1923fef4b369df0a104a38f53da.png

5.消息压缩:减少磁盘io和网络io。

6.分批发送:将消息打包批量发送,减少网络开销。

集合

问:数组的索引为什么从0开始而不是从1开始呢?

数组的寻址公式就是 数组的头地址 + 索引 * 节点的大小。

如果此时我们使用1作为索引头,则公式就变成了 数组的头地址 + (索引 - 1) * 节点的大小。

在公式上多一个减法操作,效率没有从0开始高。

问:ArrayList底层是怎么实现的呢?

1.ArrayList是基于动态数组实现的。

2.在开始的时候ArrayList的长度为0,第一次提交数据的时时候就会将扩容到10。

3.在后续我们做新增的时候会先判断size + 1是否大于当前的数组容量,如果大于的话就将容量扩容到原来的1.5倍,并对数组进行拷贝。

4.新数据就存储到索引为size的位置。最终返回boolean值。

问:new ArrayLIst(10)扩容了几次?

当构造方法传入的是长度的时候,其底层实现就是创建一个object数组,并没有进行扩容。

问:如果实现数组和List之间的转化?

数组转List使用Arrays.asList方法,List转数组使用ArrayList.toArray方法。

问:用Arrays.asList方法后,修改了数组中的内容,List会受影响吗?

 在Arrays类中的定义了getArrayList就是将传入的数组进行包装了一下,还是对该数组的引用,所以在数据修改时会受影响。

5c9c7fa8598d4090a2e247e9a5fd7106.png

问:用ArrayList.toArray方法后,修改List中的值,Array会受到影响吗?

返回的数组就是ArrayList中动态数组的拷贝, 因此不会受影响。

76a0f448678a47dbb02375bcbf0100b4.png

问:ArrayList和LinkedList的区别是什么?

1.ArrayList的底层是一个动态的数组,而LinkedList的底层是双向链表。(底层区别)

2.ArrayList的存储空间是连续的存储空间,而LinkedList的每个节点都需要存储值和两个指针,在存储空间上比ArrayList大。(存储空间)

3.查询时:索引条件已知的时候,ArrayList的时间复杂度为O(1),而LinkedList需要遍历查找,其时间复杂度为O(N)。在索引未知的情况下,都需要遍历查找,它们的时间复杂度都是O(N)。

增加和删除时(索引已知):ArrayList的头尾节点的增加和删除时间复杂度为O(1),而其他节点为O(N)。LinkedList的头尾节点增加和删除时间复杂度为O(1),其他节点的时间复杂度为O(N)。(时间复杂度)

4.ArrayList和LinkedList都是线程不安全的,所以我们在使用的时候,将其设置为局部变量。

或者通过使用Collections.synchronizedList方法进行包装,虽然线程安全但是效率比较低。

问:说一下hashMap的原理?

HashMap的底层是通过散列表 + 链表  + 红黑树实现的。

通过调用hash函数计算出值得索引从而进行数据得存储。

当位置有只值得时候:如果key的值相同则进行覆盖,如果不同就使用链表或红黑树进行存储。当然只有当链表的长度大于等于8且HashMap的长度大于等于64的时候才会使用红黑树进行存储。

问:HashMap在JDK1.7和JDK1.8有什么区别?

在JDK1.7时,HashMap在解决hash函数冲突的时候使用拉链法。当链表上存储的大量值得时候其时间复杂度就从O(1)变成了O(N)。

在JDK1.7时,HashMap在解决hash函数冲突得时候使用链表和红黑树解决,当链表的长度大于等于8且HashMap的长度大于等于64时会将链表转化为红黑树,此时它的时间复杂度为O(logN)。

问:说一下hashMap的扩容机制?

1.在进行扩容的时候使用函数resize方法进行扩容,并且在第一次初始化的时候容量为16,容量阀值12,当容量大于12时就会触发扩容。

2.每次扩容都是用来的两倍。

3.扩容之后,会创建新的数组,将旧数组中的数据按照规则(计算索引位置)复制到新的数组中。

没有哈希冲突的时候直接复制到e.hash&(newCap -  1) (旧hash值取新容量的的值,写成&运算效率更高)索引的位置上。 

如果是红黑树,直接做红黑树的添加。

如果是链表,则遍历该链表,此时链表中的数据可能会存储在不同的新节点上。通过e.hash&oldCap表达式判断存储的位置,要么存储到原始位置上,要么存储到原始位置 + 旧容量的位置上。

问:说说hashMap的寻址算法的理解?

通过计算数据的hashCode的值,并将值右移16位做异或操作,计算出新的hash值,做二次hash的目的主要让数据分布的更均匀。在存储数据的时候将该hash值做取模的操作,但是在源码中使用(capacity - 1)& hash其是等价取模的,因为与运算效率更高。

问:为什么hashMap的大小要设置为2的N次幂?

在计算索引的时候效率更高,只有当大小为2的N次幂,与运算才会等价于取模操作。

在扩容是重新计算索引的效率更高,当hash & oldCap == 0时,就存储到原来的位置,反之旧存储到原来的位置 + 旧容量的位置上。

问:有了解过hashMap1.7时的线程死锁的问题吗?

因为hashMap造1.7时使用拉链法,且在扩容的时候使用头插法,在数据迁移的时候就可能出现死循环的情况。

e424a20cd43e43349338a278da9dc98e.png

6d93a62381054f9eb0b759342722ec17.png

16e12689dc5445518bb7e68041ab2c5e.png

就比如说当前有两个线程:

1.在线程1进行数据迁移的时候,线程2先进行了数据迁移,此时因为使用头插法的原因,顺序会变成原来的倒叙(就比如A->B变为B->A)。

2.在线程2完成迁移后,线程1会进行数据的迁移。此时将A,B插入(最终又变成A,B的顺序),因为线程2的原因B的下个节点为A,所以会继续将A做头插法,并且让A指向B,此时就出现 B->A->B的死循环,最终导致线程死锁的问题。

在JDK1.8之后使用尾插法, 就解决了死锁的问题。

并发编程
问:多线程间进行操作时,如果不加锁的话,为什么会导致数据错误呢?

 那我们就来举个例子:在一个类中存在一个静态变量a,有一个方法就是对a做++操作,有两个线程同时做调用方法操作,但是二者获取的是未被对方修改的数据,所以就会导致数据错误。

问:在JUC中常用的类有哪些呢?

我比较理解的就是三个:

  1.   CountDownLatch:计数,等待另一线程执行完任务在执行任务。重要就是通过 wait进行等待,countDown进行计数,并且计数器不能重置,一般使用一次。
  2.   CyclicBarrier:作用和CountDownLatch差不多,大但是呢,它的计数器可以进行重置达到循环的效果,在每次调用wait方法后就会将计数器+1,在数量到达设定值的适合就会释放屏障,然线程执行任务。
问:线程和进程有什么区别?

1.一个程序的运行就是一个进程,在一个进程中会包多个线程。

2.进程间是不共享内存的,二在一个进程中的线程是共享内存的。

3.线程间的上下文切换(线程之间的切换)的成本比进程的上下文切换(进程之间的切换)成本低。

问:并发和并行有什么区别?

就以我们的网络请求为例:

1.并发就是服务同时可以应对多个请求的能力。

2.并行就是一个服务可以同时处理多个请求的能力。(我们服务可能会搭建集群)

问:线程创建的方法有哪些?

1.通过继承Thread实现。


  1. //继承Thread

  2. public class MyThread extends Thread{

  3. @Override

  4. public void run() {

  5. System.out.println("创建线程成功!");

  6. }

  7. public static void main(String[] args) {

  8. MyThread myThread = new MyThread();

  9. myThread.run();

  10. }

  11. }

2.通过实现接口Runnable。


  1. //实现接口Runnable

  2. public class MyRunnable implements Runnable{

  3. @Override

  4. public void run() {

  5. System.out.println("创建线程成功!");

  6. }

  7. public static void main(String[] args) {

  8. MyRunnable myRunnable = new MyRunnable();

  9. Thread thread = new Thread(myRunnable);

  10. thread.run();

  11. }

  12. }

3.通过Callable指定对应的泛型,通过FutureTask异步的获取最终的结果(获取call方法的返回值)。


  1. import java.util.concurrent.Callable;

  2. import java.util.concurrent.FutureTask;

  3. //通过实现Callable,需要指定call返回的类型,调用FutureTask获取对应的数据,创建线程

  4. public class MyCallable implements Callable<String> {

  5. @Override

  6. public String call() throws Exception {

  7. return "创建线程成功!";

  8. }

  9. public static void main(String[] args) throws Exception {

  10. MyCallable myCallable = new MyCallable();

  11. FutureTask<String> stringFutureTask = new FutureTask<>(myCallable);

  12. Thread thread = new Thread(stringFutureTask);

  13. thread.run();

  14. System.out.println(stringFutureTask.get());

  15. }

  16. }

4.通过创建线程池,需要参数Runnable的实现类,最终创建线程。


  1. import java.util.concurrent.ExecutorService;

  2. import java.util.concurrent.Executors;

  3. public class MyPool implements Runnable{

  4. @Override

  5. public void run() {

  6. System.out.println("线程创建完成!");

  7. }

  8. public static void main(String[] args) {

  9. ExecutorService pool = Executors.newFixedThreadPool(3);

  10. //创建线程

  11. pool.submit(new MyPool());

  12. //关闭线程

  13. pool.shutdown();

  14. }

  15. }

问:Runnable和Callable有什么区别?

1.Runnable的run方法没有返回值,而Callable的call方法则有返回值,且返回的类型是传入的泛型。

2.Runnable的run方法不能抛出异常(可以捕获异常),而Callable中的Call方法则可以抛出异常。

问:线程中的run方法和start方法有什么区别?

Start方法:用于启动线程。通过调用run方法,执行run中的代码,且start方法只能背调用一次。

Run方法:封装要被线程执行的代码,可以多次被调用。

问:线程包括哪些状态?

NEW(新建),RUNNABLE(可执行状态),TERMINATED(终止状态),WAIT(等待状态),TIME_WAIT(计时等待状态),BLOCKED(阻塞状态) 

c0f0f9437b764616bf2fbb7f7bb91ae6.png

问:线程之间的状态是怎么变化的?

1.在创建线程的时候就是新建状态。

2.在线程调用start方法后就是可执行状态。

3.在线程运行完之后就是变成了终止状态。

4.在线程处于可执行状态时:

  • 线程需要上锁,且此时获取锁失败,那线程的状态就会变成阻塞状态。
  • 线程调用wait方法,就会进入等待状态,在其他线程没有调用notify方法时就会一直处于这种状态。
  • 线程调用了sleep方法,就会进入计时等待状态。

问:如果创建了T1,T2,T3三个线程,如果保证按照它们的顺序执行?

可以使用对应线程的join方法,在某个线程执行之前,先调用对应上一个线程的join方法,保证执行完上个线程再执行当前线程。

问:notify方法和notifyAll方法有什么区别?

notify方法:随机唤醒某一个在等待(wait)的线程。

notify方法:唤醒所有的在等待(wait)的线程。

问:sleep方法和wait方法的区别?

相同点:都会让当前的线程放弃CPU的使用权。

不同点:

1.归属类不同:sleep方法是Thread的静态方法,wait方法则是Object都有的方法。

2.唤醒的时机不同:sleep(time)和wait(time)方法都会等待对应时间后醒来,而wait()则会一直等待下去,wait方法可以被唤醒通过notify或notifyAll方法。

3.wait方法需要配合synchronized使用,而sleep方法则不需要。

3.释放锁的不同:

  • wait方法在执行后会释放对象锁,也就是在睡眠中会释放锁,允许其他的线程获取对象锁。(放弃CPU的使用权,允许其他线程使用)
  • sleep方法在执行后则不会释放对象锁,也就是在睡眠中不会释放锁。(放弃CPU使用权,不允许其他线程使用)

问:如何停止一个正在运行的线程?

1.使用退出的布尔标识来停止线程,线程里会做一个循环的操作判断这个标识的值(true时成立),在需要停止时将布尔值设置为false,就会停止线程。

2.使用stop方法强行停止线程。(已淘汰不推荐使用)

3.使用interrupt停止线程:

  • 当使用interrupt方法停止堵塞的线程(sleep,wait,join方法)时,线程会抛出InterruptException异常。
  • 单使用interrupt方法停止正常的线程的时候,在线程中会循环的判断线程是否中止(状态,通过thread.isInterrupt方法来获取布尔值)来判断是否停线程。(类是布尔标识)

问:Synchronized关键字的底层原理有了解过吗?

synchronized采用互斥的方式至多只有一个线程以获取对应的对象锁。

线程在进入synchronized代码块的时候会去关联Monitor。

Monitor有三个属性:

1.ower:在ower中的线程就是当前获取锁的线程。当ower中没有值的时候,则当前线程直接获取对象锁,并写入ower。

2.enrtyList:在entryList中的线程就是处于堵塞的线程。

3.waitSet:在waitList中就是处于等待的线程,一般是线程调用了wait方法。

问:Mointer是重量级锁,有了解过锁的升级吗?

对象的存储结构中对象头的MarkWord存储的就是锁的状态。线程在获取锁后会在线程栈中存储锁记录,通过CAS指令来修改锁的状态。

重量级锁:基于Monitor实现的,涉及上下线程的切换,效率比较低下。

轻量级锁:通过CAS指令修改MarkWord中的锁状态,出现锁重入的时候,创建的锁记录中的的记录地址为设置为null,此过程还是使用到CAS指令,保证原子性,在解锁锁的时候通过CAS指令来恢复锁的状态(锁记录指针为null时,则将锁记录中锁对象的地址设置为null)。相比于重量级锁性能更好。(在没有线程竞争且线程交替使用时使用轻量级锁)

偏向锁:类似轻量级锁,在第一次获取锁的时候使用CAS指令,而在锁的重入的时候则会直接判断MarkWord中的线程id和当前线程id是否相同即可。相比于轻量级性能更好。(只有一个线程在使用锁时使用偏向锁)

在线程冲突的时候都会使用重量级锁

问:你谈谈对JMM的理解?

1.JMM(java Momery Model),它规定了共享内存中多线程的读写规则,保证多线程读写的正确性。

2.JMM把内存分为了两个部分,每个线程独有的内存块(私有内存),线程间共享的内存块。

3.每个内存之间是相互隔离的,主要是通过共享内存进行交互的。

问:CAS有理解过吗?

1.CAS:比较后再交换,采用的是乐观锁的思想,在线程进行操作的时候保证数据的原子性。

2.CAS使用的场景很多,就比如我们在做重量级锁升级的时候会使用,在轻量级锁和偏向锁中使用。

3.CAS采用的是自旋锁思想的,在修改共享变量时做一个无限循环判断线程获取的共享变量和当前的共享变量是否相同(共享变量可能在线程获取后,被其他线程修改,所以就会导致两次的变量值不一样,此时我们就需要让线程重新获取共享变量),如果相同则进行修改,反之则重新获取共享变量的数据。(就是一个乐观锁的思想)

4.CAS的底层使用的是unsafe类中的方法,这些方法都是本地系统的方法,是由其他语言实现的。

问:乐观锁和悲观锁有说明区别?

CAS使用的就是乐观锁,不会真正的上锁,多个线程可以同时操作,在修改数据前会匹配版本号,即使修改失败也会进行重试。

synchronized使用的就是悲观锁,每次只有一个线程能够进行数据的操作,其他的线程会处于堵塞状态。

问:谈谈你对Voliate的理解吧?

1.保证线程之间的可见性 

在线程中做共享变量的判断循环且循环过大时,可能会被JIT优化为死循环,此时其他线程修改共享变量,该线程也不会读取到。通过对共享变量添加Voliate解决。(做共享变量判断的循环被JIT优化为死循环,导致共享变量修改值读取不到)

2.防止指令进行重排序

当线程数量过多的时候为了效率,指令的顺序可能会进行重排序优化。给共享变量添加volatile,就会给共享变量在读写操作时添加不同的屏障,防止其他读写操作越过屏障,从而防止指令的重排序。

dc1e4f2698c54bbda4e66cdf5daab816.pngc6a70b0edcd84c53aba2ae0e45ace4e4.png

当对共享变量添加Volatile后:在写操作时,阻止共享变量写操作的上方的其他写操作往下走。

在读操作时,阻止共享变量读操作的下方的其他读操作往上走。

3.Voliate不是线程安全的。

 因为Voliate是不符合原子性的,就比如我们做++操作的时候,线程之间可能都没有获取到++后的值,最终就会导致结果超过+1的值,不符合原子性,所以不是线程安全的。

使用Volatile的技巧:

1.写操作时,让Volatile修饰在最后一个做写操作的共享变量上。

2.读操作时,让Volatile修饰第一个做读操作的共享变量上。

问:什么是AQS?

1.AQS是多线程中的队列同步器。

2.AQS的内部是一个先进先出的双向队列,线程在队列中进行排队。

3.AQS中的属性state为0时就是无锁状态(默认),当一个线程将state设置为1的时候就获取了该锁。

4.state属性的修改主要使用CAS,保证多线程数据修改的原子性。
c4ae5973109e43fb8da684d91bf7576b.png

问:AQS是公平锁还是非公平锁?(解释一下两个锁)

AQS既可以实现公平锁也可以实现非公平锁。

1.当state=0时,外来的线程会和队列头线程竞争锁,这时候就是一个非公平锁。

2.当state=0时,外来的线程会进入AQS的队列中按顺序进行排队,此时队头的线程就可以获取锁。这时候就是一个公平锁。

问:ReentrantLock的实现原理?

1.ReentrantLock是基于: CAS + AQS实现的,ReentrantLock实现的公平锁和非公平锁都是基于AQS接口实现的。(可以解释一下AQS)

0bb2fdc5117343798dbd9f3a393cab67.png

2.ReentranLock是支持锁重入的,在调用Lock方法后可以再次调用lock方法进行锁重入。

3.ReentrantLock是支持公平锁和非公平锁,有两个构造方法,无产构造方法可以创建一个非公平锁,有参构造方法可以创建公平锁或非公平锁。

cb698f261f7846f98b84d7b5a844edc1.png

问:Synchronized和Lock有什么区别?

1.在语法方面:Synchronized支持执行完同步代码后自动释放锁,而Lock则需要使用unlock方法手动释放锁。

2.在功能方面:二者都是采用悲观锁,都支持互斥和锁重入的功能。

Lock还支持 公平锁(通过队列按顺序获取锁),可打断(在线程进行等待的时候可以被打断),可超时(通过tryLock方法获取锁,可以在方法中设置时间,如果获取锁超时则获取锁失败),多条件变量。(可以通过条件来唤醒线程通过signal方法,lock.newCondition方法,通过条件变量来唤醒某些线程)

3.在性能方面:在没有线程竞争的时候,Synchronized性能更好,因为Synchronized支持锁升级,包括轻量级锁和偏向锁。在线程竞争激烈的时候使用Lock性能更好,因为其使用AQS实现的。(引导模式官讲AQS的原理)

问:死锁产生的条件是什么?

当在多线程的时候,每个线程需要获取多把锁,此时可能就会出现死锁的情况。(线程1:先获取A锁再获取B锁。线程2:先获取B锁再获取A锁,此时就会出现死锁的情况)

问:排查死锁的方案有哪些?

1.可以使用JDK自带的工具 jps + jstack来排查。

先通过jps获取对应死锁的进程,再通过jstack出现对应进程中线程的堆栈信息,进行代码修复。

2.通过可视化工具排查死锁,包括 jconsole,visualVM来排查死锁问题。

问:聊一聊ConcurrentHashMap?

1.结构方面:

  • 在JDK1.7之前,采用的是数组+链表的方式实现的。
  • 在JDK1.8之后,采用数组+链表+红黑树的方式实现的。(就是JDK1.8HashMap的结构)

2.在加锁方面:

  • 在JDK1.7之前,采用的是segment的分段锁,底层是通过ReentrantLock实现的,因为每个segment又对应一部分的存储位置,锁住的范围是比较大的。

c63fd5cc6ddc49daab688dd820be2c44.png

  • 在JDK1.8之后,采用CAS(自旋锁)添加新的节点,会使用Synchronized对链表或红黑树的首地址进行加锁,锁住的范围比较小,效率更高。

问:导致并发程序出现问题的根本原因是什么?(Java程序中如何保证多线程的执行安全?)

1.原子性:包保证操作都成功或者都失败。(就比如:通过加锁,防止超卖的问题)

2.可见性:在线程中做共享变量的判断循环且循环过大时,可能会被JIT优化为死循环,此时其他线程修改共享变量,该线程也不会读取到。通过对共享变量添加Voliate解决。

3.顺序性:当多线程操作时,系统为了效率可能会对指令进行重排序。我们可以通过对共享变量添加Volatile来解决。

问:说一下线程池的核心参数?(线程池的执行原理知道吗?)

1.corePoolSize:线程池中核心线程的个数。

2.maximumPoolSize:线程池中线程的总数。(线程总数=核心线程数 + 救急线程数)

3. keepAliveTime:救急线程的存活时间。(救急线程空闲时的存活时间。)

4.unit:存活时间的单位。

5.BlockingQueue:阻塞队列,存储哪些没有被核心线程执行的任务。

6.ThreadFactory:线程工厂,用于创建线程。

7.RejectExecutionHandler:拒绝策略。(当任务在队列中放不下后,执行的策略)

45e4ac8327c7454fb5e6a7ee41022949.png

拒绝策略

AbortPolicy:直接抛出异常。(默认策略)

CallRunsPolicy:直接调用主线程执行对应的任务。

DiscardOldestPolicy:丢掉在堵塞队列中存储最久的任务,将新的任务存储到队列中。

DiscardPolicy:直接丢弃任务。

执行原理

1.在新的任务进来后,先判断核心线程是否已满,如果没满,直接创建个核心线程去执行任务。

2.如果核心线程满了,就判断阻塞队列是否已满,如果没有满,直接存储到阻塞队列中。

3.如果阻塞队列满了,就判断线程的总数是否大于核心线程的总数,如果大于,创建救急线程去执行任务。(当救急线程和核心线程处于空闲的时候就会去执行堵塞队列中的方法)

4.如果小于,就去执行对应的拒绝策略。(默认直接报错)

f6bb91e6299d4971a3c938c71ba209c3.png

问:线程池中常见的阻塞队列又哪些?

LinkedBlockQueue和ArrayBlockQueue。都是先进先出的结构。

问:LinkedBlockQueue和ArrayBlockQueue又什么区别?

1.LinkedBlockQueue默认是无界的(默认的无参构造设置的大小为Integer的最大值),可以设置为有界的,ArrayBlockQueue默认是有界的。

78ac8a2f92f24dbbac736e2764f3c5a7.png

2.LinkedBlockQueue就基于链表实现的,而ArrayBlockQueue是基于数组实现的。

3.LinkedBlockQueue是惰性的,在新增操作的时候才会创建节点,而ArrayBlockQueue则是在最开始的时候就创建节点。

4.LinkedBlockQueue在加锁的时候是对队头和队尾进行加锁,而ArrayBlockQueue是队整个数组进行加锁。(在效率上LinkedBlockQueue比较高)

3a3d657505c7479da3b4783646d34f5e.png

问:如何确定核心线程的个数?

假设计算机的CPU核数为N。

查看计算机的CPU核数


  1. public static void main(String[] args) {

  2. //查看计算机的核数

  3. System.out.println(Runtime.getRuntime().availableProcessors());

  4. }

1.并发高,任务时间短的时候,设置核心线程的个数为 N + 1。减少线程上下文的切换。

2.并发不高,任务时间长的时候:

  • 当是io密集型任务时(不会大量消耗cpu的运力,就会导致cpu空闲),所以使用 2N + 1(线程数多设置一点)。
  • 当是计算密集型任务时(此时需要消耗大量的cpu计算运力),所以使用 N + 1(线程数少设置一点)。

3.并发高,任务时间长时,先从缓存的优化考虑,再考虑服务器的优化,最后在考虑线程池的优化,分析任务的类型来设置核心线程数。

问:线程池的种类有哪些?

1.固定线程数的线程,核心线程数等于总线程数。适用于任务量已知且任务耗时长的时候(不考虑创建救急线程)。

897c30b643f747009421600e3c5ad3f1.png

2.单线程的线程池,就只有一个核心线程。按先进先出的策略进行。适用于按照顺序执行的任务。

43ece80cfd41406ca0c828dfc43fc0f0.png

3.缓存化线程池,没有核心线程,执行任务都是通过救急线程。当没有线程可以执行任务时就会创建救急线程,可以灵活的控制线程的个数,并且堵塞队列中是不存任务的。适用于任务数比较密集,任务执行时间短的时候。

2099af07ef684a65a2edfd02647bc0a3.png

4.计划线程池,可以执行延迟任务的线程池,按周期去执行任务。

76535cd44ded4b22b9ff983ce5ed522f.png

2617112ca1194402be1fb5b0ac4e2d87.png

问:为什么不推荐适用Executor创建线程池?

1.Executor创建的 固定线程数的线程池和单线程的线程池使用的堵塞队列都是LinkedBlockQueue,并且其初始化长度为Integer.MAX_VALUE,也就是无界的。会出现MMO的情况(内存溢出)。

2.Executor创建的缓存化线程池的线程总数Integer.MAX_VALUE,会出现MMO的情况(内存溢出)。

所以在开发时推荐使用 ThreadPoolExecutor来创建线程池,可以设置具体的参数。

问:你的项目中有用到线程吗?

1.批量导入:如果数据量过大时,我们直接一次性导入会导致MMO的问题。可以使用线程池 + CountDownLatch来实现。

2.异步线程:为了避免上一个方法的运行影响当前方法,可以使用线程池来执行方法,可以大大提高效率,从串行关系变为并行关系。(可以说说用户下单的模块,通知快递员取件这个流程,可以说在开始的时候使用异步线程来实现,但是效率还是太低了,最后使用MQ来实现异步。有消费者确认机制,如果消费失败也就是返回nack,消息会返回消息队列中的原来位置上,可以进行重试,这是多线程实现异步没有的,这是我们项目中使用mq实现异步调用的原因)

问:如何控制某个方法允许并发访问的线程数量?

多线程中提供了一个工具类 Semaphore,可以控制并发访问的线程数。

使用方法:

1.创建一个Semaphore,需要固定信号量的大小进行初始化。

2.每次请求获取线程的时候调用acquire方法,信号量- 1。(如果没有信号量了,请求就不能进行访问)

3.每次请求使用完线程后要调用release方法,信号量 + 1。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐