面试考点梳理(初步

梳理

1.Springboot

- SpringAOP的底层实现?

  • 静态代理作用:通过代理对象访问目标对象(即被代理的对象),在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理类 即目标类(原始类) +加上额外功能 因为要屏蔽实现细节,所以它是和目标类实现的是相同的接口

  • 但是,静态代理有一个缺点就是如果我想为每一个目标类的接口扩展相同的额外功能,那么就需要为每一个原始类,手动创建一个代理类,会造成代码冗余

  • 动态代理,通过反射生成代理类

    要扩展一个类有常见的两种方式,继承父类或实现接口,所以常见的有:JDK 动态代理和 CGLIB 动态代理

    • 先来说 JDK 动态代理:JDK 的动态代理是基于拦截器和反射来实现的。JDK代理是不需要第三方库支持的,只需要 JDK 环境就可以进行代理,jdk动态代理实现:
      • 目标对象必须要实现接口
      • 必须实现 InvocationHandler 接口,并重写其 invoke 方法,在 invoke 方法中可以利用反射机制调用目标类的方法,并可以在其前后添加一些额外的处理逻辑。参数 proxy:代表的是代理对象, method:原始方法(增加额外功能的那个原始方法), args:原始方法的参数
      • 使用 Proxy.newProxyInstance 产生代理对象,newProxyInstance()
        方法帮我们执行了生成代理类,获取构造器,生成代理对象这三个操作,参数有第一个参数为借用的类加载器(任意类都可以) 第二个参数为原始类实现的接口,通过获取原始对象的class再获取接口(userService.getClass().getInterfaces()) 第三个参数为InvocationHandler接口,在此接口的invoke方法中书写额外功能,因为代理类没有字节码文件,所以JVM没有给它生成它的类加载器,需要借用其他类的类加载器,所以Proxy.newProxyInstance的第一个参数为其他类的类加载器(原始类或者其他类都可以)。
      • JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是委托类中有自己私有的方法,而接口中没有的话,该方法就不能进行代理调用。

    CGLIB通过字节码技术生成一个子类,并在子类中拦截父类方法的调用(这也就是为什么说 CGLIB 是基于继承的了),植入额外的业务逻辑。关CGLIB 引入一个新的角色方法拦截器,让其实现接口 MethodInterceptor,并重写 intercept 方法,这里的 intercept 用于拦截并增强目标类的方法,最后,通过 Enhancer.create() 创建委托类对象的代理对象。

- spring bean自动装配

  • spring容器依据某种规则(自动装配的规则,有五种),自动建立对象之间的依赖关系。

  • 1)no:
    不支持自动装配功能。

    2)default:
    表示默认采用上一级标签的自动装配的取值。如果存在多个配置文件的话,那么每一个配置文件的自动装配方式都是独立的。

    3)byName:
    当一个bean节点带有 autowire=“byName” 的属性时:
    ①将查找其类中所有属性。
    ②去配置文件中查找是否存在id名称与属性名称相同的bean。
    ③如果有,就取得该bean对象,并调用属性所在类中的setter方法,将找到的bean注入;找不到注入null。
    注意:不可能找到多个符合条件的bean(id唯一)

    4)byType:@Autowired:spring注解,默认是以byType的方式去匹配类型相同的bean
    当一个bean节点带有 autowire=“byType” 的属性时:
    ①将查找其类中所有属性。
    ②去配置文件中查找是否存在bean类型与属性类型相同的bean。
    ③如果有,就取得该bean对象,并调用属性所在类中的setter方法,将找到的bean注入;找不到注入null。
    注意:找到多个符合条件的bean时会报错。

    5)constructor:
    使用构造方法完成对象注入,其实也是根据构造方法的参数类型进行对象查找,相当于采用byType的方式。即:根据构造方法的参数的数据类型,进行 byType 模式的自动装配。

-spring解决循环依赖

  • 循环依赖:多个 bean 中,相互依赖对方,导致在创建的时候无法加载,比如现在有两个类AB相互依赖
    • 在Spring中,一个对象并不是简单new出来了,而是会经过一系列的Bean的生命周期,spring在为A属性赋值时,如果此时BeanFactory中不存在B对应的Bean,则需要生成一个B对应的Bean,然后给b属性赋值,b又依赖于A,需要构建a对应的bean,此时就产生了循环
  • spring 首先从一级缓存 singletonObjects(存的是实例化和属性填充都已完成的 bean )中获取
    如果获取不到,并且对象正在创建中,就再从二级缓存 earlySingletonObjects(实例化完成但属性填充未完成的 bean)中获取
    如果还是获取不到且允许 singletonFactories 通过 getObject() 获取,就从三级缓存 singletonFactory(缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的)中获取,如果获取到了则:从 singletonFactories 中移除,并放入 earlySingletonObjects 中。其实也就是从三级缓存移动到了二级缓存
  • 为什么使用缓存,在依赖注入之前,将原始对象放入缓存中提前暴露,其他对象想要注入这个对象时就可以从缓存中获取了,虽然是原始对象,但是没有关系,原始对象在后续的生命周期中在堆中位置是没有发生变化的
  • 为什么需要三层缓存,如果A的原始对象注入给B的属性之后,A的原始对象进行了AOP产生了一个代理对象,此时就会出现,对于A而言,它的Bean对象其实应该是AOP之后的代理对象,而B的a属性对应的并不是AOP之后的代理对象,这就产生了冲突。所以有了第三级缓存singletonFactories,ObjectFactory,得到原始对象A经过AOP之后的代理对象,然后把该代理对象放入earlySingletonObjects中,然后从earlySingletonObjects取A类对象
  • 在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后直接把本bean放入singletonObjects中即可

-bean生命周期

  • Spring 只帮我们管理单例模式 Bean 的完整生命周期,对于 prototype 的 bean ,Spring 在创建好交给使用者之后则不会再管理后续的生命周期。

  • bean是由Spring IoC容器实例化、组装和管理的对象。

  • 对于普通的 Java 对象,当 new 的时候创建对象,然后该对象就能够使用了。一旦该对象不再被使用,则由 Java 自动进行垃圾回收。Spring 中的对象是 bean,bean 和普通的 Java 对象没啥大的区别,只不过 Spring 不再自己去 new 对象了,而是由 IoC 容器去帮助我们实例化对象并且管理它,Spring Bean 的生命周期完全由容器控制。

  • singleton bean: 唯一 bean 实例,Spring 中的 bean 默认都是单例的。

  • 生命周期

    • 1.bean定义:通过xml或注解加载读取bean的元信息并定义成一个BeanDefinition对象
    • 2.bean注册:将BeanDefinition对象根据相应的规则放到缓存池map中
    • 3.实例化:根据BeanDefinition实例化真正的bean,即调用构造函数
    • 4.依赖注入:调用setter方法进行属性赋值,即是依赖注入(DI)
    • 5.初始化: 初始化是用户能自定义扩展的阶段,检查Aware的接口(拿到 Spring 容器中的一些资源),BeanPostProcessor接口(做前置后置处理),InitializingBean接口(初始化方法),是否配置自定义初始化方法init-method
    • 6.销毁: 销毁是用户能自定义扩展的阶段,检查是否实现DisposableBean接口,是否配置自定义的destory-method 方法

    请添加图片描述

    1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
    2. 实例化后利用依赖注入完成 Bean 中所有属性值的配置注入
    3. 检查Aware的接口并设置相关依赖(Aware 类型的接口的作用就是让我们能够拿到 Spring 容器中的一些资源。
    4. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法对Bean 进行加工操作作前置处理。
    5. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。 如果在配置文件中通过 init-method 属性指定了初始化方法,则调用该初始化方法。
    6. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法Bean 进行加工操作做后置处理。
    7. 如果在 <bean> 中指定了该 Bean 的作用范围为 scope=“singleton”,则将该 Bean 放入 Spring IoC 的缓存池中,将触发 Spring 对该 Bean 的生命周期管理;如果在 <bean> 中指定了该 Bean 的作用范围为 scope=“prototype”,则将该 Bean 交给调用者,调用者管理该 Bean 的生命周期,Spring 不再管理该 Bean。
    8. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
    9. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,• 如果在配置文件中通过 destory-method 属性指定了 Bean 的销毁方法,该方法也会被调用。

-springboot启动原理

  • Springboot启动时,是依靠启动类的main方法来进行启动的,而main方法中执行的是SpringApplication.run() 方法,而 SpringApplication.run() 方法中会创建spring的容器,并且刷新容器。而在刷新容器的时候就会去解析启动类,然后就会去解析启动类上的@SpringBootApplication 注解,而这个注解是个复合注解,这个注解中有一个@EnableAutoConfiguration 注解,这个注解就是开启自动配置,这个注解会去扫描我们所有jar包下的 META-INF 下的 spring.factories 文件,并将对应配置项用springFactorLoader通过反射加载到IOC容器中,这些配置里面都是有条件注解的,通常自动装配的类是@configuration注解修饰的类。

  • spring开启事务

    • @Transactional
      • 在调用声明 @Transactional 的目标方法或类时,Spring 默认使用 Aop,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,由代理对象决定声明 @Transactional 的目标方法是否被拦截器 TransactionInterceptor 来拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建或者加入事务,并执行目标方法的逻辑,,最后根据执行情况是否出现异常,利用事务管理器 PlatformTransactionManager 操作数据源 DataSource 提交或回滚事务
  1. spring的理解?对springboot的理解?为什么有spring还要有springboot?
    1. 1Spring是一个轻量级的 基于IOCAOP 容器的开源框架,其根本目的是为了简化Java开发。Java开发分为三层,controller层调用service层接口处理客户端请求,service层调用dao层实现业务逻辑,dao层与数据库交互,有strus2框架,mybatis框架都是只专注于一层的问题,而spring可以有的springmvc处理controller层问题,spring的AOP,IOC模块处理service层问题,与与各持久层框架整合解决DAO问题
    2. 自动配置,不需要手动配置很多东西
  2. 依赖注入是什么?
    1. 反转控制:将对象的创建和对象成员变量赋值的控制权由调用方反转到容器中
    2. 当一个类需要另一个类时,就意味着依赖,一旦出现依赖,就可以把另一个类作为本类的成员变量,最终通过Spring配置文件进行注入,注入:通过Spring的工厂及配置文件,为对象(bean、组件)的成员变量赋值。

springBoot 定时任务的使用

https://blog.csdn.net/weixin_38192427/article/details/116357609

springBoot 的定时任务需要两个注解 @EnableScheduling@Scheduled

  • @EnableScheduling:作用在启动类上,开启基于注解的定时任务
  • @Scheduled:作用在方法上,表示该方法为定时方法

2.SpringMVC

  • mvc

请添加图片描述

M 代表 模型(Model)

模型代表一个存取数据的对象

  • V 代表 视图(View)

视图代表模型包含的数据的可视化

  • C 代表 控制器(controller)

控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。

  • springmvc 处理请求过程

      ![请添加图片描述](https://img-blog.csdnimg.cn/32c780937d754b18b4903fbee65912bd.png)
    
      
      1、DispatcherServlet 接收到客户端发送的请求。
      
      2、DispatcherServlet 收到请求调用HandlerMapping 处理器映射器。
      
      3、HandleMapping 根据请求URL 找到对应的handler 以及 多个处理器拦截器,返回给DispatcherServlet
      
      4、DispatcherServlet 根据handler 调用HanderAdapter 处理器适配器。适配器容易支持多种类型的处理器
      
      5、HandlerAdapter 根据handler 执行处理器,也就是我们controller层写的业务逻辑,并返回一个ModeAndView
      
      6、HandlerAdapter 返回ModeAndView 给DispatcherServlet
      
      7、DispatcherServlet 调用 ViewResolver 视图解析器来 来解析ModeAndView
      
      8、ViewResolve  解析ModeAndView 并返回真正的view 给DispatcherServlet
      
      9、DispatcherServlet 将得到的视图进行渲染,填充到request域中
      
      10、返回给客户端响应结果。
    
  • SpringMVC防止表单重复提交https://blog.csdn.net/weixin_38192427/article/details/121584020

    • 网络延迟的情况下,户有时间点击多次 submit 按钮,表单提交后用户点击浏览器的【刷新】或者后退等按钮会造成表单重复提交。
    • 接口如果重复提交,可能会造成脏数据,前端可以有限制操作,但是为了防止被其他的系统调用,还需要在后端对接口做防重处理
    • 实现思路:
      • 自定义拦截器拦截请求,将上一次请求的 url 和参数和这次的对比
      • 判断,是否一致说明重复提交并记录日志

3.Mybatis

  • Mybatis如何将接口和xml连接

    • dao接口的工作原理是什么?

    Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。

    Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MapperStatement对象。然后将其添加进 mappedStatements 缓存中,mappedStatements 数据结构是一个 StrictMap,此处就是 Mapper 接口方法为什么不能重载的答案,• 将解析过的 mapper.xml 文件的 namespace 的绑定类型 type 放到 knownMappers 缓存中

    Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。

    • 如果有两个XML文件和这个Dao建立关系,岂不是冲突了?

      不管有几个XML和Dao建立关系,只要保证namespace+id唯一即可

4.Mysql

-mysql一条插入语句,服务端保存数据的流程?

  • 首先执行器根据 MySQL 的执行计划来查询数据,先是从缓存池中查询数据,如果没有就会去数据库中查询,如果查询到了就将其放到缓存池中 在数据被缓存到缓存池的同时,会写入 undo log 日志文件 更新的动作是在 BufferPool 中完成的,同时会将更新后的数据添加到 redo log buffer 中 完成以后就可以提交事务,在提交的同时会做以下三件事

• 将redo log buffer中的数据刷入到 redo log 文件中
• 将本次操作记录写入到 bin log文件中
• 将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记

-锁

  • https://blog.csdn.net/weixin_38192427/article/details/115208126
  • 共享锁(读锁)与排他锁(写锁)
    • 共享锁:一个事务获取了共享锁,其他事务也可以获取共享锁,但不能获取排他锁,其他事务可以进行读操作,不允许任何线程对该行记录进行修改
    • 排他锁: 数据加上排他锁后,则其他事务不能再对 其加任何类型的锁,得到排他锁的事务既能读数据,又能修改数据,对于 mysql 的 InnerDB 来说,会自动的给 update,insert,delete 语句加排它锁
  • 乐观锁 和悲观锁
    • 乐观锁:通过为数据库表增加一个数字类型的 version 字段来实现
    • 悲观锁:排他锁
  • mysql表锁和行锁的使用场景
    • 表锁:对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低

    • 行级锁锁住的是表中的一行数据,行锁是开销最大的锁策略。行锁虽然开销大,锁表慢,但高并发下相比之下性能更高,发生锁冲突的概率最低,行级锁并不是锁记录,而是锁索引

      • 行级锁导致的死锁

        死锁导致原因:当两个事务同时执行,一个锁住了主键索引,在等待其他相关索引。另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁

    • 绝大部分情况使用行锁,但在个别特殊事务中,也可以考虑使用表锁

      • 事务需要更新大部分数据,表又较大。若使用默认的行锁,不仅该事务执行效率低(因为需要对较多行加锁,加锁是需要耗时的);而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度
      • 事务涉及多个表,较复杂,很可能引起死锁,造成大量事务回滚。
  • 聚簇索引和非聚簇索引的区别是什么。(索引
    • 对于非聚簇索引表来说,表数据和索引是分成两部分存储的,主键索引和二级索引存储上没有任何区别。
    • 对于聚簇索引表来说,表数据是和主键一起存储的,除主键索引之外,其他的都称之为非主键索引,主键索引的叶结点存储行数据(包含了主键值),二级索引的叶结点存储行的主键值。
  • 覆盖索引
    • sql 语句的所查询字段和查询条件字段全都包含在一个索引或者(联合索引)中,那么就可以直接使用索引查询而不需要回表,这就是覆盖索引
    • • 联合索引:也称复合索引,就是建立在多个字段上的索引。联合索引的数据结构依然是 B+ Tree,一颗 B+ Tree 只能根据一个索引值来构建,所以联合索引使用 最左 的字段来构建 B+ Tree。之所以会有最左前缀匹配原则,是和联合索引的索引构建方式及存储结构 是有关系的,联合索引是使用多列索引的第一列(最左)构建的 B+ Tree

- redo log, undo log, bin log 的区别和顺序

  • Undo log 记录的是数据操作前的样子 用于实现事务原子性
  • redo log 记录的是数据被操作后的样子(redo log 是 Innodb 存储引擎特有,实现事务持久性)事务提交后一次性写入,物理日志
  • bin log 记录的是整个操作记录(这个对于主从复制具有非常重要的意义)不断在写入,属于逻辑日志
  • 查询数据,数据被缓存到缓存池的同时,会写入 undo log 日志文件 ,同时会将更新后的数据添加到 redo log buffer 中,完成以后就可以提交事务,在提交的同时将redo log buffer中的数据刷入到 redo log 文件中,并将本次操作记录写入到 bin log文件中,最后将 bin log 文件名字和更新内容在 bin log 中的位置记录到redo log中,同时在 redo log 最后添加 commit 标记

-优化

  1. 最左前缀匹配原则,非常重要的原则,mysql会一直向右匹配直到遇到范围查询(><betweenlike)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
  2. =和in可以乱序,比如a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式。
  3. 尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1,而一些状态、性别字段可能在大数据面前区分度就是0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join的字段我们都要求是0.1以上,即平均1条扫描10条记录。
  4. 索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)。
  5. 尽量的扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。
    回到开始的慢
  • 有什么索引类型?
    • B+Tree 索引
    • 哈希索引
  • B+树和B-树作为索引的优缺点
    • B树节点存的是数据,而B+树节点存的是索引,因此b+树能一个节点存更多的数据,因此树的高度会更小,IO的次数就会越小
    • B+树的叶子节点连接成一个链表可以用于范围查询,而b树不能
  • mysql中有hash索引吗?
    • InnoDB引擎不支持hash索引
  • bitmap索引有了解吗?
    • 用位图表示的索引,对列的每个键值建立一个位图。 所以相对于b-tree索引,占用的存储空间非常小,创建和使用非常快。 缺点是修改操作锁粒度大,不适合频繁更新。
  • bitmap压缩是什么?
    • 在位图索引中,每一个bitmap往往都包含了大量的“0”,这个特性使其非常适合压缩。一个好的位图索引压缩算法需要达到两个目标:(1)提高按位逻辑操作的速率;(2)减少查询响应时间。有行程长度压缩编码(Run-Length Encoding,RLE)”和“差分压缩编码(Delta-Encoding,DE)”算法。行程长度压缩编码的主要思想是将连续多个相同的数值压缩成“数量 x 数值”的形式,例如:000000->6(*)0;而差分压缩编码的主要思想是将存储一个序列中的数据转换为存储此序列中数据间的差值

什么是MVCC,越详细越好。

  • 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以MVCC可以为数据库解决在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
  • 如何实现mvcc(全局发号器)实现机制
    • 隐藏字段:当前操作该记录的事务ID;一个回滚指针,用于配合undo log,指向上一个旧版本
    • undo log:记录版本线性表,链首就是最新的旧记录,链尾就是最早的旧记录
    • ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。
      这些记录都是去undo log 链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。
    • 已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView
      。readview活跃事务 ID 列表是怎么获取到的?
      • 每条记录都有隐藏字段 保存该记录的事务ID,通过查找undo log链查找获取

5.Redis

  • redis中 set()命令如何处理长度不一致的情况 (应该是SDS)

    底层用SDS存String类型,空间预分配(len,减少分配次数,对字符串进行空间扩展的时候,扩展的内存比实际需要的多)和惰性空间(alloc,)释放(对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用也减少了分配次数比如缩减长度后又拼接)空间不够,扩容加倍现有的空间

-redis的基本数据类型

  • 字符串

    • 二进制安全(不会妄图已某种特殊格式解析数据。
    • 一个 Redis 中字符串 value 最多可以是 512M
    • 简单动态字符串 (Simple Dynamic String, 缩写 SDS),是可以修改的字符串,内部结构实现上类似于 Java 的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.扩容都是加倍现有的空间
    • 使用 INCR 系列中的命令将字符串用作原子计数器incr decr incrby INCRDECRINCRBY
    • 使用APPEND命令附加到字符串。
    • 使用字符串作为带有GETRANGE SETRANG GETRANGESETRANGE和的随机访问向量。
  • list

    • Redis用双端链表实现List
    • 实现最新消息排队
    • 在社交网络中为时间线建模,使用 LPUSH以在用户时间线(头)中添加新元素,并使用LRANGE以检索一些最近插入的项目。
    • 您可以将lpush ltrim(修剪现有列表) LPUSHLTRIM与一起使用来创建一个永远不会超过给定元素数量的列表,而只记住最新的 N 个元素。
    • lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop(阻塞列表弹出,当没有任何元素可以从任何给定列表中弹出时,它会阻塞连接)=Message Queue(消息队列)
  • set

    • String 类型的无序集合。集合中不能出现重复的数据
    • spop
      • 从位于 的设置值存储中删除并返回一个或多个随机成员key
      • 此操作类似于[SRANDMEMBER](https://redis.io/commands/srandmember),从集合中返回一个或多个随机元素但不删除它。
      • 默认情况下,该命令会从集合中弹出一个成员。当提供可选count参数时,回复将由最多count成员组成,具体取决于集合的基数
    • sdiff
      • 求交集 返回由第一个集合和所有后续集合(并)之间的差异产生的集合成员。
    • sunion
      • 并集
    • • 标签(tag),给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。SINTER 求交集
    • 点赞,或点踩,收藏等,可以放到set中实现
  • hash

    • 特别适合用于存储对象。相比string更节省空间
  • Zset

    • 有序集合 每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。分数(score)却可以重复
    • 排行榜:有序集合经典使用场景
      • 每次提交新分数时,您都使用ZADD对其进行更新。您可以使用ZRANGE(默认索引,分数ZRANGE zset (1 5 BYSCORE “(”为不包括)轻松检索排名靠前的用户,还可以在给定用户名的情况下使用ZRANK(获取分数从高到低排序的元素的排名)返回其在列表中的排名。将 ZRANK 和 ZRANGE 一起使用,您可以向用户显示与给定用户相似的分数。一切都很快
  • HyperLogLogs(基数统计)

    • 省内存的去统计各种计数 使用少量固定的内存去存储并识别集合中的唯一元素
  • Bitmap

    • 即位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态。
  • geospatial (地理位置)

  • 基本数据类型底层实现

请添加图片描述

- 简单动态字符串 - sds
    - Redis的默认字符串表示
    - 二进制数据的一种结构, 具有动态扩容的特点.
    - 内部结构`len` 保存了SDS保存字符串的长度 `buf[]` 数组用来保存字符串的每个元素+\0(复用c中函数) `alloc`分别以uint8, uint16, uint32, uint64表示整个SDS, 除过头部与末尾的\0, 剩余的字节数(`buf中未用字节长度`). `flags` 始终为一字节, 以低三位标示着头部的类型, 高5位未使用
    - 为什么使用sds,为什么不用c的字符串
        - 获取字符串长度的时间复杂度从n变为1 len属性
        - 字符串拼接,之前可能会造成缓冲区溢出,现在会根据len判断,然后扩容
        - 之前修改需要重新分配内存,空间预分配(len,减少分配次数)和惰性空间(alloc)释放(不立即回收,也减少了分配次数比如缩减长度后又拼接)
        - 二进制安全,c是用空字符判断结尾(有些文件内容包含空字符),sds是len

- 压缩列表 - ZipList——hash
    - 提高存储效率的双端列表,可以存整数和字符串,整数存的二进制而不是字符串,每次增删需要重新分配内存ziplist的内存
    - 
        
        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILmbGHq9-1657541132500)(%E6%A2%B3%E7%90%86%202cbc4705c785478fb728537c33385252/Untitled%205.png)]
        
    - entry 结构
        - 一般结构 `<prevlen>:前一个entry的大小 <encoding>当前entry的类型和长度 <entry-data>`存储entry表示的数据
        - `<prevlen> <encoding>` 整数,data存在encoding里面
        - prevlen :小于254(255用于zlend)的时候,prevlen长度为1个字节,值即为前一个entry的长度,如果长度大于等于254的时候,prevlen用5个字节表示,第一字节设置为254,后面4个字节存储一个小端的无符号整型,表示前一个entry的长度;
    - 为什么压缩空间
        - 普通的数组,那么它每个元素占用的内存是一样的且取决于最大的那个元素**所以增加encoding字段**,针对不同的encoding来细化存储大小;**增加了prelen字段用于遍历**
    - 缺点
        - 不预留内存空间,每次增删都要重新分配空间,如果第一个结点改变,之后prevlen都要改变
- 快表 - QuickList——List
    - ziplist为结点的双端链表结构
    - 
    
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5q2IF7nG-1657541132500)(%E6%A2%B3%E7%90%86%202cbc4705c785478fb728537c33385252/Untitled%206.png)]
    
    - 指针字段会耗费大量内存
- 字典/哈希表 - hashTable—→ hash或者set
    - 由数组 table 组成,元素是dictEntry,还有size(数组大小), masksize (求索引,-1),used(结点数量
    - entry 有键有数值还有指向下一个的指针,数值可以是指针,可以是无符号整数,整数
    - 链地址法
    - 哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩
        - 根据原哈希表已使用的空间扩大/缩小一倍创建另一个哈希表
        - 重新利用上面的哈希算法,计算索引值,然后将键值对放到新的哈希表位置上。
        - 所有键值对都迁徙完毕后,释放原哈希表的内存空间。
        - 扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。
        - 触发扩容的条件:
            - 服务器目前没有执行 BGSAVE(BGSAVE 异步保存到磁盘) 命令或者 BGREWRITEAOF 命令(指示 Redis 启动[Append Only File](https://redis.io/topics/persistence#append-only-file)重写过程),并且负载因子大于等于1。
            - 服务器没有执行,且负载因子大于等于5
        
        ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。
        
- 整数集 - IntSet   —> set
    - 当一个集合只包含整数值元素,并且这个集合的元素数量不多时
    - 当在一个int16类型的整数集合中插入一个int32类型的值,整个集合的所有元素都会转换成32类型。删除不会降级
    - `encoding` 表示编码方式,的取值有三个:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64
    - `length` 代表其中存储的整数的个数
    - `contents` 指向实际存储数值的连续内存区域, 就是一个数组;整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值得大小**从小到大有序排序**,且数组中不包含任何重复项。(虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值)
- 跳表 - ZSkipList —>有序列表 (最多多少层,怎么分配层 跳表及查询的时间复杂度
    - 增删查改,在对数期望时间内完成,比平衡树更优雅,做范围查找的时候,平衡树比skiplist操作要复杂,实现简单且达到了类似效果(skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
    - 缺点就是需要的存储空间比较大
    - 多级索引,每层都是一个链表,上层结点指向下层值相同的结点,
    - 最多logn层,第一层是n个结点,第二层是n/2个结点,第k层是n*(1/2的k-1次方)查询复杂度logn
    
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yoy2CJ7v-1657541132501)(%E6%A2%B3%E7%90%86%202cbc4705c785478fb728537c33385252/Untitled%207.png)]

- 如何保证缓存与数据库的双写一致性https://blog.csdn.net/weixin_38192427/article/details/115313934

  • 先更新数据库,后删除缓存

    • 更新数据库数据
    • 数据库会将操作信息写入
    • 订阅程序提取出所需要的数据以及 key
    • 另起一段非业务代码,获得该信息
    • 尝试删除缓存操作,发现删除失败
    • 将这些信息发送至消息队列
    • 重新从消息队列中获得该数据,重试操作
  • 过期策略
    1.1.1. 定期删除 + 惰性删除是如何工作的
    redis 默认会每隔 100ms 检查是否有过期的 key,如有过期的 key 则删除。需要说明的是,redis 不是每隔 100ms 将所有的 key 检查一次,而是随机抽取进行检查(如果每隔 100ms 对全部的 key 进行检查,那 redis 岂不是卡死)

    因此,如果只采用定期删除策略,会导致很多 key 过期了,而没有删除。于是,惰性删除派上用场。也就是说在你获取某个 key 的时候 redis 会检查一下,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除(并不是 key 到了过期时间,就会被删除掉。而是在你获取查询这个 key 时,redis 再惰性的检查一下)

    1.1.2. 定期删除 + 惰性删除就没问题了么
    不是的,如果定期删除没有删除 key。然后你也没即时去请求 key,也就是说惰性删除也没生效。这样,redis 的内存会越来越高。那么就应该采用内存淘汰机制

- 高并发常见问题如何处理?缓存穿透、缓存击穿、缓存雪崩如何解决

  • 缓存穿透:磁盘和redis都没有查询的数据,导致每次查找这个数据都要去磁盘找

    • 检查不合法查询(id>0)

    • 数据库和redis都没有,设置值为key-null (设置较短过期时间,防止正常状态 的使用)

    • 布隆过滤器
      布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求

      布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在,查询元素:通过所有的hash函数计算元素的下标,每个下表存储的值都为1,则有可能存在,只要有一个不为1就不存在。

  • 缓存击穿 :缓存没有,数据库有(一般是缓存key过期) 并发多时,同时查询数据库(一条数据多个查询),会给数据库很大压力

    • 热点数据永不过期
    • 加互斥锁(一个一个查询数据)
  • 缓存雪崩 : 缓存数据大批量过期,导致数据库压力过大

    • 数据过期时间随机,避免同时过期 在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低
    • 热点数据永不过期
    • 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存
    • 分布式部署redis
  • 缓存污染:访问一两次后不再访问但一直存在缓存中,浪费空间 空间满了后要根据淘汰策略删除一些数据

redis (1)

6.JVM

JVM 是运行在操作系统之上的,与硬件实现无关,是Java跨平台的基础

-JAVA类加载过程?

  • 加载:在本地磁盘查找并加载类的二进制数据 到jvm内存中,并利用字节码文件创建一个 Class 对象,
  • 验证:被加载的类的正确性 ,
  • 准备: 为类的静态变量分配内存,类变量会分配在方法区中,并将其初始化为默认值 ,
  • 解析: 把类中的符号引用转换为直接引用 ,
  • 初始化,为类的静态变量赋予正确的初始值。
  • JAVA类加载器有哪些?为什么要有双亲委派机制?new 了一个类什么时候进行类加载的?
    • **启动类加载器,**主要加载的是 JVM 自身需要的类,即包名为 javajavaxsun 等开头的类
    • 扩展类加载器,ext 目录下或者由系统变量 -Djava.ext.dir 指定位路径中的类库
    • **应用程序类加载器,**负责加载系统类路径 java -classpath-D java.class.path 指定路径下的类库
    • 自定义类加载器
    • 定义:
      • 除了顶层的 Bootstrap 类加载器外,其余的类加载器都应当有自己的父类加载器。如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
    • 意义:
      • Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载
      • 保证Java 核心 API 中定义类型不会被随意替换,比如用户自定义了一个oject类,如果不用双亲委派机制,则会出现不同类加载器产生的oject对象,会使java最基本的行为也无法保证,产生错误
    • 首先查看这个对象是否被加载到了内存,如果没有的话,则需要先进行该类的类加载

-对象创建

  • 遇到 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,执行相应的类加载
  • 类加载检查通过之后,为新对象分配内存(内存大小在类加载完成后便可确认)。在堆的空闲内存中划分一块区域(指针碰撞-内存规整’或‘空闲列表-内存交错’的分配方式)
  • 内存空间分配完成后会初始化为 0(不包括对象头),接下来就是填充对象头,把对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息存入对象头
    执行 new 指令后执行 init 方法后才算一份真正可用的对象创建完成

-Java运行时数据区划分?

  • 线程私有
    • 程序计数器:存放指令地址
    • 栈:主要存局部变量,每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在,在这个线程上正在执行的每个方法都对应一个栈帧,栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息,JVM 直接对栈的操作只有压栈和出栈,遵循先进后出原则,如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
  • 线程公有
    • 堆:存放对象实例和数组
    • 方法区:包含运行时常量池,存放常量,静态变量,类型信息

Java垃圾回收算法

  • 标记 - 清除:将存活的对象进行标记,然后清理掉未被标记的对象。
    • 不足:标记和清除过程效率都不高;会产生大量不连续的内存碎片,导致无法给大对象分配内存。
    • 优点:实现简单效率高
  • 标记 - 整理(老):让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
    • 效率不高,消耗大
    • 优点:减少内存碎片化,注重吞吐量
  • • 标记-复制(新):将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
    • 减少内存碎片化,内存利用率不高,只用了一般

-判断对象回收

  • 引用计数:有循环引用问题
  • 可达性:GCRoots 向下搜索引用链,无引用链的就回收
    • GCRoots
      • 栈中的局部变量
      • 方法区,静态变量或者常量引用的对象
      • JVM内部引用的对象
      • 同步锁synchronized 描述的对象

-CMS的流程?

https://cloud.tencent.com/developer/article/1624694

  • CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是基于“标记-清除”算法实现的,并且常见的应用场景是互联网站或者B/S系统的服务端上的Java应用
  1. 初始标记:标记GCroots之间联系的对象
  2. 并发标记,与用户线程同时运行;从直接联系对象遍历到整个对象图
  3. 重新标记,修正用户进程产生的变动
  4. 并发清除,删除标记阶段判断要死亡的对象
  • 缺点:
    • 对cpu资源敏感,并发时GC线程占用CPU资源
    • 无法处理并发时用户新产生的对象
    • 空间碎片化

7.Java基础

-泛型

  • 本质是:参数化类型
  • 泛型**适用于多种数据类型执行相同的代码,**提供了编译时类型安全,确保把正确类型的对象放入集合中)
  • 分为限定通配符和非限定通配符
    • 非限定通配符,参数可以是任意类型
    • 限定通配符分为两种,
      • 设定上限,<? extends T> 必须是t或者t的子类
      • 设定下限,<? super T> 必须是t或者t的父类
  • “类型擦除”(Type Erasure),在编译时将所有的泛型表示都替换为具体的类型就像完全没有泛型一样。之前没有实现泛型,为了兼容问题所以使用类型擦除

-反射

  • 定义: 运行状态 中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java
    语言的反射机制

  • 反射的优点就是比较灵活,能够在运行时动态获取类的实例。

  • 不过反射也存在很明显的缺点:

    1)性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。

    2)安全问题:反射机制破坏了封装性,因为通过反射可以获取并调用类的私有方法和字段。

- 多态

  1. 多态的定义:指允许不同类的对象对同一消息做出响应
  2. 实现多态的技术称为:动态绑定(dynamic binding),是指在运行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
  3. 多态的作用:消除类型之间的耦合关系。产生修改时只需要在子类重写的地方进行修改
  4. 多态的表现:父类引用调用子类重写的方法
  5. 重写是父类与子类之间多态性的一种表现,重载是一类中多态性的表现

-接口和抽象类

  • 抽象类
    • 包含一个或多个抽象方法的类就是抽象类,抽象方法即没有方法体的方法
      ,抽象方法和抽象类都必须声明为 abstract
    • 除了抽象方法之外,普通类有的抽象类也可以有,抽象类还可以包含具体数据和具体方法,不能多继承
    • 抽象类不能被实例化
    • 子类想要实例化必须实现抽象类中的所有抽象方法,否则还是抽象类
    • 继承必须确保超类所拥有的性质在子类中仍然成立(里氏替换
  • 接口
    • 抽象类是能够包含具体方法的,而接口杜绝了这个可能性,在 Java 8 之前,接口非常纯粹,只能包含抽象方法,也就是没有方法体的方法。而 Java 8 中接口出现了些许的变化,开始允许接口包含默认方法和静态方法,
      • 默认方法冲突的问题
        • 超类优先
        • 接口冲突,如果一个父类接口提供了一个默认方法,另一个父类接口也提供了一个同名而且参数类型相同的方法,子类必须覆盖这个方法来解决冲突。
    • 接口中是允许出现常量的,变量默认为public static final,必须赋初值,不能被修改
    • 一个类可以实现多个接口
    • 接口强调特定功能的实现,而抽象类强调所属关系。
    • 为什么要有默认方法
      • 假设没有默认方法这种机制,那么如果要为接口增加一个新的方法,那么所有实现了该接口的类,都需要重写这个方法。但是引入了默认方法后,原来的类,不需要做任何改动,并且还能得到这个默认方法通过这种手段,就能够很好的扩展新的类,并且做到不影响原来的类。
    • • 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
    • 在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
  • HashMap
    • 底层:数组+链表+红黑树
    • 数组默认大小为16,负载因子是0.75
    • 当put时判断数组存放的数量+1是否小于数组长度乘以负载因子,不小于,需要扩容为原来的两倍
    • 怎么算hashcode,正常利用哈希函数算出hash值然后再于高16位异或,这样增加了随机性使得分布更加均匀
    • put的流程:首先对key做hash运算得出index下标,判断是否发生碰撞,没碰撞放入数组,碰撞后放入链表,如果链表长度超过阈值则链表转换为红黑树,第二步判断key值是否重复,重复则替换原来的值,最后判断是否需要扩容
    • 比较元素相同时,首先需要比较hash值是否相同,然后==和euals都相同才相同
  • Arrays.sort()
    • 基本类型:快排

      • 当待排序的数组中的元素个数较少时,源码中的阀值为7,采用的是插入排序。尽管插入排序的时间复杂度为0(n^2),但是当数组元素较少时,插入排序优于快速排序,因为这时快速排序的递归操作影响性能。

      • 2)较好的选择了划分元(基准元素)。能够将数组分成大致两个相等的部分,避免出现最坏的情况。**例如当数组有序的的情况下,选择第一个元素作为划分元,将使得算法的时间复杂度达到O(n^2).

        源码中选择划分元的方法:

        当数组大小为 size=7 时 ,取数组中间元素作为划分元。int n=m>>1;(此方法值得借鉴)

        当数组大小 7<size<=40时,取首、中、末三个元素中间大小的元素作为划分元。

        当数组大小 size>40 时 ,从待排数组中较均匀的选择9个元素,选出一个伪中数做为划分元。

    • 对象:归并,快速排序是不稳定的,对象数组中保存的只是对象的引用,这样多次移位并不会造成额外的开销,但是,对象数组对比较次数一般比较敏感,有可能对象的比较比单纯数的比较开销大很多。归并排序在这方面比快速排序做得更好,这也是选择它作为对象排序的一个重要原因之一。

    • equals() 与 == 的区别

      • ==
        • 基础类型:比较值
        • 引用类型 :比较内存中地址。即是否为同一对象
      • equals
        • 没有重写,比较的是内存中的地址
        • 重写后,比较的是对象的内容

-ArrayList

  • 在物理内存上采用顺序存储结构,即数组。因此可根据索引快速的查找元素,还具有基于索引操作元素的一套方法,允许 null
    元素的存在
  • 开始是空数组,第一次为 ArrayList 添加元素的时候,扩容默认初始容量最小为 10
  • 动态扩容机制
    • add()方法调用时才会进行扩容
    • 判断是否需要扩容的条件是,当前集合拥有的元素个数+1是否大于数组长度,还有长度溢出判断
    • 扩容倍数是1.5,1.5容易用移位进行计算,太小会导致扩容频繁,大于等于2会使新增的空间大于之前分配的空间总大小,不利于空间重用
    • 扩容方式:新初始化一个新数组,然后将旧数组的元素拷贝到新数组中,并修改原数组的引用指向这个新建数组
  • remove()
    • 将index+1后面的列表对象前移一位,该操作将会覆盖index以及之后的元素,相当于删除了一位元素
  • LinkedList
    • 底层使用了 双链表 实现的,线程不安全,增删快,查找慢
  • CopyOnWriteArrayList:线程安全
    • CopyOnWriteArrayList 底层使用了独占式的可重入锁 ReentrantLock + volatile
    • 底层数组加volatile 修饰,来保证可见性
    • 读操作不加锁,对所有写操作,在加锁之后先复制一份新数组,在新数组上面写,并修改原数组的引用指向这个新建数组,最后解锁

-ConcurrentHashMap

  • ConcurrentHashMap 是线程安全的
  • ConcurrentHashMap 不允许为 nullkey 或者 value
  • 底层的数据结构:数组 + 单链表 + 红黑树
  • 默认的初始容量:和 HashMap 相同都是 16
  • 创建数组时机:当执行第一次插入时才会创建数组
  • 1.7 分段锁Segment,容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率,但是并发率和分段个数有关。Segment用的是ReentrantLock锁
  • 1.8 用CAS + Synchronized + volatile 代替分段锁
    • 对于锁的粒度,调整为对每个数组元素加锁
    • 如果没有 hash 冲突,就直接 CAS 插入
    • 如果产生 hash 冲突时,先使用 synchronized 加锁,在锁的内部处理 hash 冲突与 HashMap 是相同的,即使用 keyequals() 方法进行比较
    • 链表插入是尾插法,防止产生循环链表
    • 为什么用 synchronized 来代替 ReentrantLock
      • 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized 并不比 ReentrantLock 差,在粗粒度加锁中 ReentrantLock 可能通过 Condition 来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition 的优势就没有了
      • 而且基于 JVM 的 synchronized 优化空间更大,使用内嵌的关键字比使用 API 更加自然在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存
    • put()保证线程安全
      • 使用了 volatile 修饰 table 变量,并使用 Unsafe 的 getObjectVolatile() 方法拿到最新的 Node
      • CAS 操作:如果上述拿到的最新的 Node 为 null,则说明还没有任何线程在此 Node 位置进行插入操作,说明本次操作是第一次
      • synchronized 同步锁:产生了 hash 冲突;此时的 synchronized 同步锁就起到了关键作用,防止在多线程的情况下发生数据覆盖(线程不安全),接着在 synchronized 同步锁的管理下按照相应的规则执行操作:
        • 当 hash 值相同并 key 值也相同时,则替换掉原 value
        • 否则,将数据插入链表或红黑树相应的节点

-HashMap

  • HashMap
    • 底层:数组+链表+红黑树
    • 数组默认大小为16,负载因子是0.75
    • 当put时判断数组存放的数量+1是否小于数组长度乘以负载因子,不小于,需要扩容为原来的两倍
    • 怎么算hashcode,正常利用哈希函数算出hash值然后再于高16位异或,这样增加了随机性使得分布更加均匀
    • put的流程:首先对key做hash运算得出index下标,判断是否发生碰撞,没碰撞放入数组,碰撞后放入链表,如果链表长度超过阈值则链表转换为红黑树,第二步判断key值是否重复,重复则替换原来的值,最后判断是否需要扩容
    • 比较元素相同时,首先需要比较hash值是否相同,然后==和euals都相同才相同
  • 通过 hash 值跟数组长度取与来决定放在数组的哪个位置如果出现放在同一个位置的时候,优先以链表的形式存放,在同一个位置的个数又达到了 8 个以上,如果数组的长度还小于 64 的时候,则会扩容数组。如果数组的长度大于等于 64 了的话,在会将该节点的链表转换成树
  • put()
    • 当创建 HashMap 集合对象的时候,在 jdk1.8 之前,是在它的构造方法中创建了一个默认长度是 16 的 Entry[] table 的数组来存储键值对数据的。而从 jdk1.8开始,是在第一次调用 put 方法时创建了一个默认长度是 16 的 Node[] table 的数组来存储键值对数据的
      数组创建完成后,当添加一个元素(key,value)时,
    • 首先计算元素 key 的 hash 值,以此确定插入数组中的位置。但是可能存在同一 hash 值的元素已经被放在数组同一位置了,这时就添加到同一 hash 值的元素的后面,他们在数组的同一位置,这就形成了单链表,同一各链表上的 Hash 值是相同的。当链表长度大于阈值 8 时,数组的长度小于 64则扩容,数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储,这样大大提高了查找的效率
    • 判断key是否相同,相同则覆盖
    • 判断是否需要扩容
      在转换为红黑树存储数据后,如果此时再次删除数据,当红黑树的节点数小于 6 时,那么此时的红黑树将转换为单链表结构来存储数据
  • 扩容机制
    • 时机:put()判断,数组中元素个数小于 数组长度*负载因子
    • 为什么是0.75,平衡空间和时间的选择,数学考虑(0.75 时容器中节点的频率遵循参数为0.5的泊松分布),0.75乘二次幂大多是整数
    • Hashtable 与 HashMap 的比较
      • 底层的数据结构:Hashtable 是数组 + 链表,而 HashMap 是数组 + 链表 + 红黑树
      • 默认的初始容量:Hashtable 是 11,而 HashMap 是 16
      • 扩容大小:Hashtable 扩容后的大小为原来的 2 倍 + 1,而 HashMap 是原来大小的 2 倍
      • 数组的懒加载:Hashtable 在初始化时就创建了数组,HashMap 对底层数组采取的懒加载,即当执行第一次插入时才会创建数组
      • 键和值是否允许为 null:Hashtable 不允许,HashMap 中键和值均允许为 null
      • 线程安全:Hashtable 是线程安全的,而 HashMap 是线程不安全的
      • Hashtable 是线程安全的Hashtable 处理线程安全问题过于简单粗暴,是将所有的方法都加上了 synchronized 关键字,在竞争激烈的并发
        场景中性能就会非常差。
      • HashMap 为何重写了 hashCode() 和 equals()
        1. 如果两个对象 hashCode() 不相等,则 equals() 一定不相等,不重写hashcode ()会造成当equals()判断相等时hashcode不相等
        2. 如果两个对象的 equals() 相等,那么它们的 hashCode() 值一定相同,不重写equals()会造成equals()判断不相等时hashcode相等

8.操作系统

-任务调度的算法

  • 先来先服务算法,实现简单,有利于长作业,不有利于短作业,减少了系统的吞吐量。
  • 短作业优先,平均等待时间少,有利于短作业,不利于长作业,
  • 优先级算法,优先级高的先执行,保证紧迫性作业能得到及时处理,但未考虑性能
  • 高响应比优先调度算法,优先级=等待时间+运行时间/运行时间,优先级随着等待时间的延长而增加,这将使长作业的优先级在等待期间不断地增加,兼顾长短作业,计算会消耗资源
  • 时间片轮转调度算法,根据 FCFS 策略,将所有的就绪进程排成一个就绪队列,并可设置每隔一定时间间隔(如30 ms)即产生一次中断,激活系统中的进程调度程序,完成一次调度,将CPU分配给队首进程,令其执行。当该进程的时间片耗尽或运行完毕时,系统再次将CPU分配给新的队首进程(或新到达的紧迫进程)。由此,可保证就绪队列中的所有进程在一个确定的时间段内,都能够获得一次CPU 执行。时间片的大小对系统性能有很大的影响。时间片小频繁切换消耗资源,太大退化成先来先服务算法了

-浮点数和定点数有什么区别?底层实现?

  1. 浮点数,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到
  2. 定点数,在固定bit 下,约定小数点的位置

-L1,L2,L3缓存是所有CPU所有核心共享的嘛?既然L1,L2缓存属于不同核心私有那不同核心L1,L2缓存如何通信?

  • L1,L2缓存属于不同核心私有,L3 则是所有CPU 核心共享的内存,
  • 通过监控独立的loads和stores指令来监控缓存同步冲突,并确保不同的处理器对于共享内存的状态有一致性的看法。

-NIO是什么?优点是什么?底层实现?

  1. 同步非阻塞NIO:应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。
  2. NIO的优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。
  3. NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

IO多路复用模型,,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核kernel能够通知程序进行相应的IO系统调用。
IO多路复用模型的基本原理就是select/epoll系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。

9.网络

-三次握手和四次挥手中,双方服务器的具体状态

请添加图片描述
请添加图片描述

  • 为什么需要四次挥手

    • TCP 是全双工通信的。所以双方都可以主动断开连接,讲四次挥手过程
  • 为什么是三次?

    • 防止客户端发送连接请求报文段没有准时到达服务器端,导致重传,然后建立连接后,延迟的连接请求报文段又到达了服务器端,服务器收到后会一直等客户端发送数据,会造成服务器资源的浪费,有了三次连接后就不会出现以上的情况了。
  • 为什么要等2MSL关闭

    • 防止客户端发送确认丢失,B重传时A收不到的情况发生
    • 让延迟到达的报文段能消失在网络中,防止出现在新的连接里。
  • 如何作流量控制?可靠传输

    • 用滑动窗口,发送窗口由接受方窗口决定,接受方窗口由接受缓存决定

      • 发送方解析接收方的响应数据包,根据接收方的接收窗口大小调整自己的发送窗口大小;
      • 发送方通过向前滑动发送窗口的方式移除已确认被正确接收的数据,并将他们从缓冲区删除;
      • 发送方只发送自己发送窗口内的数据
      • 发送窗口向前滑动的前提是发送窗口的数据确认被正确接收
      • 出现错误,将接受出口中最后一个有序的确认的数据帧之后所有都重新发送,
    • 拥塞控制和流量控制的区别

      拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:( 1 )慢开始、拥塞避免( 2 )快重传、快恢复。

      流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。

  • TCP是双工通信吗?什么是双工通信

    • 是,通信的双方可以同时发送和接收信息
  • TCP链接IP可以被篡改吗

    • 没有了解过,但是tcp是可靠传输,应该是不可以的吧
  • TCP/UDP区别?UPD数据包一次可以发送多少大小?

    • udp无连接不可靠运输,简单,面向报文
    • tcp面向连接可靠运输,面向字节流
  • 如果已经建立了连接,但是客户端突然出现故障了怎么办
    TCP 还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为 2 小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔 75 秒钟发送一次。若一连发送 10 个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接

-浏览器的地址栏中输入网址,回车之后

  • 浏览器分析url
  • 浏览器向DNS请求解析Ip
  • 域名系统DNS解析文本所在服务器的Ip地址
  • 浏览器和服务器建立TCP连接
  • 浏览器发出取命令get+路径
  • 服务器响应,将文件发给浏览器
  • 发送完毕后关掉TCP连接
  • 同时浏览器显示文本

10.并发

-多线程如何顺序执行(线程通信

https://www.cnblogs.com/rever/p/14768553.html

  • 使用Thread.join()实现,join() 方法将挂起 调用线程 的执行任务,直到 被调用的对象 完成它的执行任务
  • 单线程线程池
  • volatile关键字修饰的信号量,volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它上锁
  • 使用Lock和信号量实现
  • wait() 和 notify()
  • condition的原理

-线程和进程的区别

  • 进程是资源分配的基本单位,进程切换需要回收和分配资源,开销大,不同进程之间地址空间是独立的,进程通信需要同步和互斥手段
  • 线程是调度的基本单位,线程切换不需要回收和分配资源,开销小,同一进程的不同线程之间共享地址空间,线程通信只需要直接读取,线程依赖于进程而存在。

-JMM

  • 作用控制jvm中数据私有区和数据共享区数据交互
    • Java 内存模型定义了八个流程步骤来实现,数据私有区称为工作内存,数据共享区称为主内存

      lock:锁定。作用于主内存的变量,把一个变量标识为一条线程独占状态
      read:读取。作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中
      load:载入。作用于工作内存的变量,从主内存中读取的变量值放入工作内存的变量副本中
      use:使用。作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
      assign:赋值。作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
      store:存储。作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中
      write:写入。作用于主内存的变量,它把从工作内存中一个变量的值写入到主内存中
      unlock:解锁。作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

-进程之间的通信

  • 共享内存:多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
  • socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。
  • 消息队列:具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点
  • 信号量:它是一个计数器,可以用来控制多个进程对共享资源的访问,基于操作系统的 PV 操作
  • 管道 : 面向字节流可以一边写入,另一边读出的一个共享文件

-线程池

  • 线程池内部的实现原理(worker)?

    Java线程池实现原理及其在美团业务中的实践

    https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

    • 线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的处理:(1)直接申请线程执行该任务;(2)缓冲到队列中等待线程执行;(3)拒绝该任务。线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
    • 线程池为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker,Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。
  • 线程池7大核心参数

    • corepoolsize:空闲线程个数
    • workQueue:工作队列
    • maxmunPoolSize:线程池最大线程数
    • keepAliveTime:空闲存活时间
    • threadFacotry:ThreadFactory来创建的线程
    • unit :时间单位
    • RejectedExecutionHandler:拒绝策略
  • ThreadPoolExecutor 类中提交任务的方法
    void execute(Runnable command)
    Future submit(Callable task)
    Future<?> submit(Runnable task) :虽然返回 Future,但是其 get() 方法总是返回 null
    Future submit(Runnable task, T result)

  • Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

  • 线程池大小设置多少,根据什么考虑的?

    cpu密集型,cpu个数+1

    IO密集型:cpu个数乘以cpu利用率乘以总时间除以计算时间

    请添加图片描述

  • 线程池什么时候创建线程?

在创建了 ThreadPoolExecutor 线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来(执行了 execute() 方法)时才创建线程去执行任务

  • 线程使用方式

    有三种使用线程的方法:

    • 实现 Runnable 接口;需要实现 run() 方法。
    • 实现 Callable 接口;与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。
    • 继承 Thread 类。也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。

-线程有哪些状态?

请添加图片描述

锁变化的过程?

一共有四种状态:无锁偏向锁轻量级锁重量级锁,它会随着竞争情况逐渐升级。
请添加图片描述
请添加图片描述
请添加图片描述

-对synchronized 的理解?底层实现?https://blog.csdn.net/weixin_38192427/article/details/117125806

是一个并发关键字,是一种抢占式非公平锁,• 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;每个实例都对应有自己的一把锁(this),不同实例之间互不影响,• synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁

加锁和释放锁的原理底层用的指令码,保证可见性的原理:内存模型和happens-before规则,可重入原理:加锁次数计数器

-AQS

  • 抽象的队列式的同步器,AQS 定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的 ReentrantLock,ReentrantReadWriteLock,CountDownLatch
    在 AQS 中,主要有两部分功能:一部分是操作 state 变量,第二部分是实现排队和阻塞机制
  • AbstractQueuedSynchronizer 被设计为一个抽象类,它使用了一个 volatile 来修饰 int 类型的成员变量 state 来表示同步状态,通过内置的 FIFO (先进先出)双向队列来完成资源获取线程的排队等待工作。通常 AQS 的子类通过继承 AQS 并实现它的抽象方法来管理同步状态
    AQS 自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法,AQS 既可以支持独占式地访问同步状态( 如ReentrantLock),也可以支持共享式地访问同步状态(如 CountDownLatch),这样就可以方便实现不同类型的同步组件
  • AQShttps://blog.csdn.net/weixin_38192427/article/details/117028828

-reentranklock

ReentrantLock 来自于 jdk 1.5,位于 JUC 包的 locks 子包,独占(互斥)式可重入锁。Synchronized 的功能他都有,并且具有更加强大的功能
实现了 Lock 接口,具有通用的操作锁的方法。内部是使用 AQS 队列同步器来辅助实现的,重写了 AQS 的获取独占式锁的方法,并实现了可重入性
ReentrantLock 还具有公平与非公平两个获取锁模式,

  • 可重入性
    ReentrantLock 是一个可重入的互斥锁。顾名思义,互斥锁表示锁在某一时间点只能被同一线程所拥有。可重入表示锁可被某一线程多次获取。当然 synchronized 也是可重入的互斥锁

  • synchronized 关键字隐式的支持重进入,比如一个 synchronized 修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁
    ReentrantLock 虽然没能像 synchronized 关键字一样支持隐式的重进入,但是在调用 lock() 方法时,已经获取到锁的线程,能够再次调用 lock() 方法获取锁而不被阻塞。主要的实现是在实现 tryAcquire(int acquires) 方法时考虑占有锁的线程再次获取锁的场景
    在 ReentrantLock 中, AQS 的 state 同步状态值表示线程获取该锁的可重入次数

  • 在默认情况下,state 的值为 0 表示当前锁没有被任何线程持有
    当一个线程第一次获取该锁时会尝试使用 CAS 设置 state 的值为 1 ,如果 CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程
    在该线程没有释放锁的情况下,第二次获取该锁后,状态值被设置为 2 , 这就是可重入次数
    在该线程释放该锁时,会尝试使用 CAS 让状态值减 1,如果减 1 后状态值为 0,则当前线程成功释放该锁

  • ReentrantLock 是基于 AQS 实现的独占式可重入锁,同时还有公平模式和非公平模式

  • 非公平模式
    从源码中可以看出来,非公平锁的非公平之处在于:在头节点释放锁唤醒后继节点线程的时候,如果有新来的线程在尝试获取锁,那么新来的线程在和后继节点中的线程的竞争中可能获取锁成功,有两个地方都有支持这样的可能性

在 NonfairSync 类实现的 lock() 方法中,使用 CAS 方式获取到锁(只有在当前锁未被任何线程占有时才能成功,通过 state 是否等于 0 来判断)
在 NonfairSync 类实现的 tryAcquire(int acquires) 方法中,使用 CAS 方式获取到锁(只有在当前锁未被任何线程占有时才能成功,通过 state 是否等于 0 来判断)
如果被新来的线程获取到了锁,那么此时获取到锁的线程,不需要加入队列。而队列中被唤醒的后继节点,由于获取不到锁只能继续等待获得锁的节点线程释放了锁之后继续尝试

只要是进入同步队列排了队的节点线程,就只能按照队列顺序去获取锁,而没有排队的线程,则可能直接插队获取锁。如果在上面的两处获取锁都失败后,线程会被构造节点并加入同步队列等待,再也没有先一步获取锁的可能性

可以看出来,非公平模式下,同步队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待唤醒,并且还要检查前驱节点是否为头节点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁,这也就是非公平模式,在高并发情况下会出现的饥饿问题

但是非公平模式会有良好的性能,直接获取锁线程不必加入等待队列就可以获得锁,免去了构造节点并加入同步队列的繁琐操作,因为加入到队列尾部时也是需要 CAS 竞争的,可能会造成 CPU 的浪费,并且还可能会因为后续的线程等待、唤醒,造成线程上下文切换,非常耗时

  • 公平模式
    即使是公平锁也不一定能保证线程调度的公平性,公平模式下,后来的线程调用 tryLock() 方法同样可以不经过排队而获得该锁。因为 tryLock() 方法内部是直接调用的 nonfairTryAcquire() 方法,这个方法我们说了它不具备公平性

ReentrantLock源码解读_桐花思雨的博客-CSDN博客

-java中终止线程的方式

  • 线程调用interrupt()方法,仅仅是在当前线程中打一个停止的标记,并不是真的停止线程,也就是说,线程中断并不会立即终止线程,而是通知目标线程,有人希望你终止。至于目标线程收到通知后会如何处理,则完全由目标线程自行决定,所以如果希望线程 t 在中断后停止,就必须先调用isInterrupted()或者interrupted()方法或者判断是否被中断,并为它增加相应的中断处理代码,****interrupt() 方法终止遇到 sleep()wait(),**Thread.sleep() 方法会抛出一个 InterruptedException 异常,当线程被 sleep() 休眠时,如果被中断,就会抛出这个异常,Thread.sleep() 方法由于中断而抛出的异常,是会清除中断标记的
    • public boolean Thread.isInterrupted()// 判断是否被中断
    • public static boolean Thread.interrupted()// 判断是否被中断,并清除当前中断状态
  • 使用 volatile 修饰退出标志位,使线程正常退出,也就是当 run() 方法完成后线程中止(使用volatile目的是保证可见性,一处修改了标志,处处都要去主存读取新的值

- ArrayBlockingQueue和LinkedBlockingQueue的优劣点

  • ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁;
  • LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
  • ArrayBlockingQueue实现的队列中在生产和消费的时候,是直接将枚举对象插入或移除的;
  • LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
  • 如何设计一个高性能的有界阻塞队列?
    • ArrayBlockingQueue,基于数组,使用通知模式,• 使用 notEmpty、notFull 两个 Condition 条件对象 来分别实现消费者、生产者的等待唤醒,当获取数据的消费者线程被阻塞时会将该线程放置到 notEmpty 条件队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到 notFull 条件队列中,对于线程的获取锁、释放锁、等待、唤醒等基本操作的源码都交给 ReentrantLockConcition 来实现

https://blog.csdn.net/weixin_38192427/article/details/117638584

  • 如何设计一个高性能的无界阻塞队列?

    • 底层数据结构是一个单链表
    • 出队线程只操作队头,而入队线程只操作队尾,采用了两把锁,对插入数据采用 putLock作为消费线程获取的锁,同时有个对应的 notEmpty 条件对象用于消费线程的阻塞和唤醒,对移除数据采用 takeLock作为生产线程获取的锁,同时有个对应的 notFull 条件对象用于生产线程的阻塞和唤醒,即入队锁和出队锁,这样避免了出队线程和入队线程竞争同一把锁的现象
    • 添加和删除操作并 不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量
    • LinkedBlockingQueue 的工作模式都是 非公平 的,也不能手动指定为公平模式,这样的好处是可以提升并发量

    https://blog.csdn.net/weixin_38192427/article/details/117262033

-volatile关键字可以解决什么问题,如何实现的?

  • 防重排序,实现可见性
  • 当一个变量被声明为 volatile 时,线程在写入变量时,会把最新值刷新写回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
  • happens-before原则
    • 程序顺序
    • 监视器锁,锁的解锁在happens-before于之后的加锁
    • volatile变量规则,对一个 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 的结论。
  • 内存屏障
    • 是一个 CPU 指令,它使得CPU 或编译器在对内存进行操作的时候, 严格按照一定的顺序来执行,
    • 在指令序列中插入内存屏障来禁止重排序,在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
  • volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。

-ReentrantLock和synchronized 的区别?

  • synchronized效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
  • synchronized不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
  • synchronized无法知道是否成功获得锁,相对而言,Lock可以拿到状态
  • synchronized不会死锁,lock会

-什么是CAS?Unsafe类再操作系统层面是如何实现CAS的?CAS会一直自旋嘛?

  • 是一种乐观锁,乐观锁就是假定操作不会发生并发安全问题((不会有其他线程对数据进行修改)),因此不会加锁,但是在更新数据时会判断其他线程是否有没有对数据进行过修改。

  • 比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步

  • 函数包含3个操作数:

    • V:表示需要更新的值;

    • E:表示期望值;

    • N:表示要写入的新值;

      思想是如果V相等于E,则将V的值写入N,若V不能于E,说明有其他线程做了更新,可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

  • 自旋CAS预测未来会获取到锁而不断循环等待。在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起

  • sleep()

    • 让当前线程休眠指定时间
    • 休眠时间的准确性依赖于系统时钟和 CPU 调度机制
    • 不释放已获取的锁资源,如果 sleep() 方法在同步上下文中调用,那么其他线程是无法进入到当前同步块或者同步方法中的
    • 可通过调用 interrupt() 方法来唤醒休眠线程
  • wait()

    • 让当前线程进入等待状态,当别的其他线程调用 notify() 或者 notifyAll() 方法时,当前线程进入就绪状态
    • 想要调用 wait() 方法,前提是必须获取对象上的锁资源
    • 当 wait() 方法调用时,当前线程将会释放已获取的对象锁资源,并进入等待队列,其他线程就可以尝试获取对象上的锁资源
  • notify():被调用后,锁不会自动释放,必须执行完 notify() 方法所在的 synchronized 代码块后才释放;只会随机任意的唤醒等待队列中的一个线程(不会通知优先级比较高的线程)

11.数据结构与算法题

  1. 对二叉树理解?对红黑树理解?为什么使用红黑树,不使用平衡二叉树?
    1. 红黑树要求
    • 性质 1. 结点是红色或黑色。
    • 性质 2. 根结点是黑色。
    • 性质 3. 所有叶子都是黑色。(叶子是 NIL 结点)。
    • 性质 4.(从每个叶子到根的所有路径上不能有两个连续的红色结点)。
    • 性质 5. 任意一节点到每个叶子节点的路径都包含数量相同的黑节点。
    • 红黑树放弃了追求完全平衡,追求大致平衡,在与平衡二叉树的时间复杂度相差不大的情况下,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。
    • 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。
  • 如何一次遍历,判断有环链表的环长度

    slow和fast分别是指针,如果从链表头部开始往后移动,且fast指针移动速度为slow指针移动速度的两倍,fast 与low相遇则表示有环,环的长度等于第一次相遇时low走的长度

12.shiro

  • shiro的认证流程、为什么选择shiro
    • 可以非常容易的开发出足够好的应用。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。而且Shiro的API也是非常简单。
    • 认证流程
      1. 用户进行登录操作,参数是封装了用户信息的token(Token是服务端端生成的一串字符串,作为客户端进行请求时辨别客户身份的的一个令牌。 当用户第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码,绕过数据库查询,能够减少数据库的压力
      2. Security Manager进行登录操作
      3. Security Manager委托给Authenticator进行认证逻辑处理
      4. 调用AuthenticationStrategy进行多Realm身份验证
      5. 调用对应Realm进行登录校验,认证成功则返回用户属性,失败则抛出对应异常
  • pagerank算法
    • 是一个用于衡量网络中结点重要性的算法,首先,直觉来说,一个结点如果链接越多,那么这个结点就可能越重要,因为入链不容易伪造,所以使用入链的个数来衡量,而且,来自比较重要的结点的链接更重要,在比较重要性时,权重更大,这是一个递归的问题,想要算这个结点的重要性首先需要算前继结点的重要性
    • 在初始阶段:每个页面设置相同的PageRank值
    • 首先初始化一个随机邻接矩阵M,每一列是该点在邻接结点的重要性分布,如果有链接j指向i则M的第i行第j列等于结点j的出度的倒数,则rank向量r= M*r
    • 然后怎么算呢,用到了随机游走算法,想象一个网页冲浪者在t时间在i网页上,则t+1时刻就会从i结点的出链中均匀随机选一条游走,然后无限期重复,那么如果已经知道t时刻所在结点的概率分布,那么t+1时刻所在结点的概率分布为t时刻的概率分布乘以转移概率分布M,当M收敛我们就能算出rank向量r
    • 但是还会有两个问题
      • 有的结点没有出链即死结点,最后所有结点的pangrank会收敛为0,解决方法是,从死结点随机跳到任意结点
      • 所有出边都在一个节点组中,会吸收所有重要性,会一直在节点组打转,解决方法,以β概率使用算法随机选择,以1-β的概率跳到一个页面上(概率平均),这样就不会一直打转
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值