说一下HashMap的工作原理
思路:1.底层数据结构-->2.初始化核心参数-->3.hash算法-->4.寻址算法-->5.hash冲突-->
6.扩容机制-->7.put方法,get方法和remove方法执行流程-->8.hashMap是否是线程安全
#HashMap是一个用于存储key-value键值对的集合,每一个键值对也叫做Entry,这些个键值对分散存储在一个数组中,这个数组就是HashMap的主干。
#每一个Entry对象通过Next指针指向它的下一个Entry节点。并且数组中的每一个元素不止是一个Entry对象,也是一个链表的头节点。
#HashMap中主要是通过key的hashCode来计算hash值的,只要hashCode相同,计算出来的hash值就一样。如果存储的对象对多了,就有可能不同的对象所算出来的hash值是相同的,这就出现了所谓的hash冲突。解决办法就是使用链表),只需要插入到对应的链表即可。
#jdk1.7是采用表头插入法插入链表,jdk1.8采用的是尾部插入法。头插法在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。尾插法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
#当HashMap初始化的时候,有两个重要的参数,分别是初始容量(initialCapacity 默认16)和装载因子(loadFactor 默认0.75)。即当Map的容量小于initialCapacity*loadFactor的时候,就会进行扩容。
#jdk1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
#上面是put的流程,那么当我们想要查询数据(get)的时候,原理就比较简单,只需要根据key的hashcode算出元素在数组中的下标,之后遍历Entry对象链表,直到找到元素为止。
#之后说一下删除的流程,就是先通过hash方法找到数组上的位置,然后进行遍历,找到对应的key,把被删除的节点的上一个节点的指针指向被删除节点的下一个节点即可。
#jdk1.7中采用数组+链表,1.8采用的是数组+链表/红黑树,即链表长度大于阈值8后,就改成红黑树存储。这是因为如果hash值相等的元素较多时,通过key值依次查找的效率较低。使用红黑树可以大大减少了查找时间。
#那么为什么不直接采用红黑树呢,是因为红黑树需要进行左旋,右旋操作,而单链表不需要。在元素<8时,红黑树查询成本高,新增成本低。在元素>8时,红黑树查询成本低,新增成本高。
#HashMap不是线程安全的,jdk1.7中主要体现在扩容会造成死循环、数据丢失的情况。jdk1.8中,虽然使用了尾插法避免了环形链表的情况,但是在多线程下,仍然会产生数据丢失的情况。
#Hashmap多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。
说一下线程安全ConcurrentHashMap的工作原理
思路:1.底层数据结构-->2.hash算法-->3.寻址算法-->4.put、get、size方法
#因为HashMap是非线程安全的,所以在多线程下,想要安全的操作map,一般有三种方法
#1.使用Hashtable线程安全类。但是由于它几乎所有的crud方法都加了synchronized同步锁。相当于给整个哈希表加了一把锁,所以在多线程下,性能会非常差。不建议采用。
#2.使用Collections.synchronizedMap方法,如果传入的是HashMap对象,里面是使用对象锁来保证多线程的安全的,实际上也是对HashMap进行全表锁。性能也会很差,不建议采用。
#3.使用并发包中的ConcurrentHashMap类。
#jdk1.7中底层数据结构是 分段的数组+链表
#ConcurrentHashMap是由Segment数据结构和HashEntry数据结构组成。Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。
#一个ConcurrentHashMap包含一个Segment数组。Segment数组中的每个元素包含一个HashEntry数组,HashEntry数组中的每个元素是一个链表结构的元素。
#Segment数组的每个元素各守护着一个HashEntry数组中的素有元素。当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment数组元素的锁。
#jdk1.8中底层数据结构是 数组+链表/红黑树
#JDK1.8中ConcurrentHashMap类取消了Segment分段锁,采用CAS+synchronized来保证并发安全。
#ConcurrentHashMap中synchronized只锁定当前链表或红黑二叉树的首节点,只要节点hash不冲突,就不会产生并发,相比JDK1.7的ConcurrentHashMap效率又提升了N倍!
#get方法
#1.为输入的Key做Hash运算,得到hash值。
#2.通过hash值,定位到对应的Segment对象。
#3.再次通过hash值,定位到Segment当中数组的具体位置。
#整个get方法不需要加锁,只需要计算两次hash值,然后遍历一个单向链表。
#put方法
#1.为输入的Key做Hash运算,得到hash值。
#2.通过hash值,定位到对应的Segment对象
#3.获取可重入锁
#4.再次通过hash值,定位到Segment当中数组的具体位置。
#5.插入或覆盖HashEntry对象。
#6.释放锁。
#由于需要对共享变量进行写操作,所以为了线程安全,在操作共享变量时必须加锁。put方法首先定位到Segment,然后在Segment里进行插入操作.
#插入操作需要经历两个步骤
#1.判断是否需要对Segment里的HashEntry数组进行扩容。2.定位添加元素的位置,然后将其放在HashEntry数组里。
#在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。
#值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容.
#在扩容的时候,首先会创建一个容量是原来两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment扩容。
#size方法
#1.遍历所有的Segment。
#2.把Segment的元素数量累加起来。
#3.把Segment的修改次数累加起来。
#4.判断所有Segment的总修改次数是否大于上一次的总修改次数。如果大于,说明统计过程中有修改,重新统计,尝试次数+1;如果不是。说明没有修改,统计结束。
#5.如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计(阈值默认为2)。
#6.再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
#7.释放锁,统计结束。
#这种思想和乐观锁悲观锁的思想如出一辙。为了尽量不锁住所有Segment,首先乐观地假设Size过程中不会有修改。当尝试一定次数,才无奈转为悲观锁,锁住所有Segment保证强一致性。
说一下springboot的启动原理
思路:先从SpringBootApplication注解开始说,然后再说main方法里的SpringApplication.run
#我们开发一个springboot项目,都要写一个启动类,在启动类中最关键的就是SpringBootApplication注解和main方法里的SpringApplication.run方法。
#首先先说SpringBootApplication注解,它主要由三个注解组成@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan注解组成。他们的作用分别是:
#(1)@SpringBootConfiguration:这个注解实际功能就是与@Configuration注解是一样的,将它标注在类上,代表这是一个配置类,一般里面会使用@Bean注解来标注方法,方法的返回值会作为一个bean定义注册到Spring的IoC容器中,方法名将默认成该bean定义的id。用这种注解的方式,替代了之前spring的xml的配置文件方式。
#(2)@EnableAutoConfiguration:主要是由两个注解组成,分别是@AutoConfigurationPackage和@Import({AutoConfigurationImportSelector.class})。这个注解实现了springboot的自动装配,即通过自定义实现ImportSelector接口,从而导致项目启动时会自动将所有项目META-INF/spring.factories路径下的配置类注入到spring容器中,从而实现了自动装配。
#(3)@ComponentScan:作用就是自动扫描并加载符合类型的bean,将这个bean定义加载到IoC容器。默认是扫描启动类所在的包路径下文件。也可以使用basePackages属性来指定扫描的范围。
#总结一下就是被SpringBootApplication注解标注的类就是启动类,SpringBoot启动的时候,会去运行这个类的main方法来启动SpringBoot应用。
#之后再说一下main方法里的SpringApplication.run方法。
#(1)首先创建了一个SpringApplication对象。
# 创建的时候,读取了classpath下所有的MTEA-INF/spring.factories xml配置文件的ApplicationContextInitializer(容器初始化器)还有ApplicaiontListener(侦听器),将这两者封装到SpringApplication实例中。
#(2)执行SpringApplication实例的run方法。run方法里
# 1.创建计时器
# 2.声明IOC容器
# 3.从类路径MTEA-INF/spring.factories下获取SpringApplicationRunListeners对象并回调其starting方法。
# 4.封装命令行参数
# 5.准备环境,包括创建环境。
# 6.打印Banner
# 7.创建IOC容器(决定创建 web 的 IOC 容器还是普通的 IOC 容器)
# 8.准备上下文环境,将environment保存到IOC容器中,并且调用applyInitializers()方法。
# 9.刷新容器,对IOC容器进行初始化(如果是web应用还会创建嵌入式的Tomcat),并扫描、创建、加载所有组件。
# 10.从 IOC 容器中获取所有的 ApplicationRunner 和 CommandLineRunner 进行回调
# 11.最后调用所有的SpringApplicationRunListeners的started()方法
说一下springboot如何实现自动装配
思路:从启动类的@EnableAutoConfiguration的@Import注解延申说
#@EnableAutoConfiguration:主要是由两个注解组成,分别是
#(1)@AutoConfigurationPackage
#(2)@Import({AutoConfigurationImportSelector.class})。
#那么实现原理其实就是在@Import(AutoConfigurationImportSelector.class)这个注解中,@Import注解的参数可以是静态类(用作直接导入)也可以是实现了ImportSelector接口的类,当是实现了ImportSelector会根据实现的selectImports方法来对类进行导入。
#那么可以看到AutoConfigurationImportSelector的实现方法selectImports方法里有两个重要的方法
# 1.loadmetadata的方法是加载项目的基本配置数据信息
# 2.getAutoConfigurationEntry方法则是自动装配的逻辑,也就是加载类,再往下看就可以看到自动装配实际上就是在自动扫描和加载META-INF/spring.factories路径下的配置类注入到spring容器中,从而实现了自动装配。
#总结一下:
#springboot的自动装配就是通过自定义实现ImportSelector接口,从而导致项目启动时会自动将所有项目META-INF/spring.factories路径下的配置类注入到spring容器中,从而实现了自动装配。
说一下SpringBoot如何实现自定义starter
思路:首先说一下自动装配原理,之后再说一下实现自定义starter的步骤
#首先说一下自动装配原理:
#项目启动时,Spring通过@Import注解导入了AutoConfigurationImportSelector, 然后调用该类selectImports时,从classpath下的META-INF/spring.factories文件中读取key为EnableAutoConfiguration的配置类,然后Spring便会将这些类加载到Spring的容器中,变成一个个的Bean。
#那么通过这个自动装配的原理,我们可以知道,我们将要自定义的starter其实也要符合这个规范,这样就可以被springboot成功引用了。
#自定义Starter,首选需要实现自动化配置,而要实现自动化配置需要满足以下两个条件:
#(1)能够自动配置项目所需要的配置信息,也就是自动加载依赖环境;
#(2)能够根据项目提供的信息自动生成Bean,并且注册到Bean管理容器中;
#具体步骤如下:
#1.在项目的pom.xml文件中引入spring-boot-autoconfigure依赖,用来实现自动化配置
# <dependency>
# <groupId>org.springframework.boot</groupId>
# <artifactId>spring-boot-autoconfigure</artifactId>
# <version>2.1.4.RELEASE</version>
# </dependency>
#2.定义xxxService,业务类,完成一些相关的逻辑操作
#3.定义xxxProperties类,属性配置类,完成属性配置相关的操作
#4.定义xxxConfigurationProperties类,自动配置类,用于完成Bean创建等工作
#5.在resources下创建目录META-INF,在META-INF目录下创建spring.factories,在SpringBoot启动时会根据此文件来加载项目的自动化配置类,将这些类加载到Spring的容器中,变成一个个的Bean。
说一下Spring是如何解决循环依赖的
思路:首先说一下造成循环依赖的原因,之后再说一下spring针对这种情况具体处理的步骤
@Component
public class A {
private B b;
public void setB(B b) {
this.b = b;
}
}
@Component
public class B {
private A a;
public void setA(A a) {
this.a = a;
}
}
说一下spring的三个缓存
思路:首先介绍三个缓存的概念和作用,然后再介绍三个缓存一起使用可以解决什么问题。
#首先说一下Spring中的三个缓存:
#一级缓存:singletonObjects,存放初始化后的单例对象
#二级缓存:earlySingletonObjects,存放实例化,未完成初始化的单例对象(未完成属性注入的对象)
#三级缓存:singletonFactories,存放ObjectFactory对象
#都是Map集合
#单例对象先实例化存在于singletonFactories中,后存在于earlySingletonObjects中,最后初始化完成后放入singletonObjects中**。
#Spring是如何解决循环依赖问题的?
#上面说到Spring是使用三级缓存(Map)解决的循环依赖问题,那具体是怎么做的,看下面的步骤。
#假设A依赖B,B依赖A,Spring创建A实例的过程如下:
#1、A依次执行doGetBean方法、依次查询三个缓存是否存在该bean、没有就createBean,实例化完成(早期引用,未完成属性装配),放入三级缓存中,接着执行populateBean方法装配属性,但是发现装配的属性是B对象,走下面步骤。
#2、创建B实例,依次执行doGetBean、查询三个缓存、createBean创建实例,接着执行populateBean发现属性中需要A对象。
#3、再次调用doGetBean创建A实例,查询三个缓存,在三级缓存singletonFactories得到了A的早期引用(在第一步的时候创建出来了),将它放到二级缓存并移除3级缓存并返回,B完成属性装配,一个完整的对象放到一级缓存singletonObjects中。
#4、B完成之后就回到A了,A得到完整的B,肯定也完成全部初始化,也存入一级缓存中。
#解决了循环依赖问题。
#为什么不使用二级缓存?
#如果仅仅是解决循环依赖问题,二级缓存也可以,但是如果注入的对象实现了AOP,那么注入到其他bean的时候,不是最终的代理对象,而是原始的。通过三级缓存的ObjectFactory才能实现类最终的代理对象。
#一级缓存能不能解决循环依赖问题?
#可以解决,但是因为初始化完成和未初始化完成的都放在这个map中,拿到的可能是没有完成初始化的,属性都是空的,直接空指针异常。
说一下如何保证接口的幂等性
思路:定义 + 前端 + 数据库层面 + JVM锁 + 分布式锁
#对于一个接口而言,无论调用多少次,最终得到的结果都是一样的。
#比如用户点击完"提交"按钮之后,我们可以把按钮设置为不可用或者隐藏状态,避免用户重复点击。但是如果是模拟请求,就会有问题了。但是可以把这个当成一个容错的手段。
#数据库层面就是加乐观锁。乐观锁是指在执行数据操作时(更改或添加)进行加锁操作。可以通过版本号实现。如:update table_name set version = version + 1 where version = 0
#JVM锁是指通过JVM提供的内置锁,比如使用Lock对业务代码段进行加锁操作,但是由于Lock本身为单机锁,所以只适合单机,不适合分布式环境。
#分布式锁实现幂等性的逻辑是,在每次执行方法之前判断,是否可以获取到分布式锁,如果可以,则表示为第一次执行方法,否则直接舍弃请求即可。通常使用redis或者zookeeper来实现分布式锁
说一下Spring中都用到了那些设计模式
思路:工厂模式 + 单例模式 + 代理模式 + 模板方法模式 + 观察者模式 + 适配器模式 + 装饰者模式 + 策略模式
# 简单说:
# 工厂设计模式 : Spring使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
# 代理设计模式 : Spring AOP 功能的实现。
# 单例设计模式 : Spring 中的 Bean 默认都是单例的。
# 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
# 装饰者设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
# 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
# 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。
# 策略模式:Spring中在实例化对象的时候用到Strategy模式。
# 复杂说:
# 工厂模式:
# Spring使用工厂模式可以通过 BeanFactory 或 ApplicationContext 创建 bean 对象。
# BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于ApplicationContext来说会占用更少的内存,程序启动速度更快。ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。
# BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
# 单例模式:
# Spring 中 bean 的默认作用域就是 singleton(单例)的。 实现单例的方式是 xml : <bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>注解:@Scope(value = "singleton")
# 除此以外还有下面几种作用域
# prototype : 每次请求都会创建一个新的 bean 实例。
# request : 每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
# session : 每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效。
# global-session: 全局session作用域,仅仅在基于portlet的web应用中才有意义,Spring5已经没有了。Portlet是能够生成语义代码(例如:HTML)片段的小型Java Web插件。它们基于portlet容器,可以像servlet一样处理HTTP请求。但是,与 servlet 不同,每个 portlet 都有不同的会话
# 除此以外,Spring 通过 ConcurrentHashMap 实现单例注册表的特殊方式实现单例模式。
# 代理模式:
# Spring AOP 就是基于动态代理的。
# 如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。
# 当然也可以使用 AspectJ ,Spring AOP 已经集成了AspectJ。Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。
# 模板方法模式:
# 定义一个操作中的算法的框架,而将一些步骤延迟到了子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些步骤。
# Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
# 一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
# 观察者模式:
# 它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,这个对象所依赖的对象也会做出反应。
# Spring 事件驱动模型就是使用的观察者模式。
# 适配器模式:
# 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。
# Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式。
# 在Spring MVC中,DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
# 如何没有使用适配器模式,结果可能就是,
# if(type = userController){
# XXX
# }else if(type = deptController){
# XXX...
# }
# 装饰者模式:
# 装饰者模式可以动态地给对象添加一些额外的属性或行为。
# JDK 中就有很多地方用到了装饰者模式,比如 InputStream家族,InputStream 类下有 FileInputStream (读取文件)、BufferedInputStream (增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream 代码的情况下扩展了它的功能。
# Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。这个时候就要用到装饰者模式。
说一下synchronized是如何保证原子性、可见性、有序性
思路:分别介绍原子性、可见性、有序性->指令重排->as-if-serial
# 原子性
# 原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。
# Java提供了两个高级的字节码指令monitorenter和monitorexit来保证被synchronized修饰的代码在同一时间只能被一个线程访问。
# 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。
# 即使执行过程中,cpu时间片用完,线程1放弃了cpu,但是他没有解锁,而synchronized的锁是可重用的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。
# 可见性
# 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
# Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存的是主内存副本拷贝。线程对变量的操作必须在工作内存中进行,不能直接读写主内存。
# 不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。那么这就可能出现线程1改了某个变量的值,但是线程2不可见的情况。
# 处理办法就是对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。
# 有序性
# 有序性即程序执行的顺序按照代码的先后顺序执行。
# 除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。
# synchronized是无法禁止指令重排和处理器优化的。具体处理是as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。
# as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
# 指令重排
# 为了提高程序执行效率,编译器和处理器(cpu)会对指令做一些优化,即指令重排序。
# as-if-serial语义
# 不管编译器和CPU如何重排序,必须保证在单线程的情况下程序的结果是正确的。
# synchronized是无法禁止指令重排和处理器优化的。只不过,我们有同步代码块,可以保证只有一个线程执行同步代码快中的代码,从而保证有序性。
说一下Spring-Boot-Starter启动器都有哪些
思路:web-jdbc-test-log4j-logging-redis-es等
# spring-boot-starter-web 支持全栈式Web开发,包括Tomcat和spring-webmvc。
# spring-boot-starter-jdbc 支持JDBC数据库。
# spring-boot-starter-security 支持spring-security。
# spring-boot-starter-test 支持常规的测试依赖,包括JUnit、Hamcrest、Mockito以及spring-test模块。
# spring-boot-starter-websocket 支持WebSocket开发。
# spring-boot-starter-log4j 支持Log4J日志框架
# spring-boot-starter-logging 引入了Spring Boot默认的日志框架Logback。
# spring-boot-starter-tomcat 引入了Spring Boot默认的HTTP引擎Tomcat。
# spring-boot-starter-actuator 增加了面向产品上线相关的功能,比如测量和监控。
# spring-boot-starter-thymeleaf 支持Thymeleaf模板引擎,包括与Spring的集成。
# spring-boot-starter-redis 支持Redis键值存储数据库,包括spring-redis。
# spring-boot-starter-freemarker 支持FreeMarker模板引擎。
# spring-boot-starter-data-elasticsearch 支持ElasticSearch搜索和分析引擎,包括spring-data-elasticsearch。
# spring-boot-starter-data-jpa 支持JPA(Java Persistence API),包括spring-data-jpa、spring-orm、Hibernate。
# spring-boot-starter-data-mongodb 支持MongoDB数据,包括spring-data-mongodb。
说一下分布式一致性协议2PC、3PC、TCC
思路:解释一下分布式事务->XA协议->2PC->3PC->TCC
# 分布式事务
# 简单的说,就是一个接口需要同时往多个服务中写数据,要保证所有服务的数据都可以正常的提交和回滚,保证数据在多个系统的一致性,就需要分布式事务介入。
#
# XA协议
# XA协议是一个基于数据库的分布式事务协议,其分为两部分:事务管理器和本地资源管理器。事务管理器作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。2PC、3PC就是根据此协议衍生出来而来。如今Oracle、Mysql等数据库均已实现了XA接口。
#
# 2PC(两阶段提交)
# 分布式事务的2PC协议有两个阶段,一个是准备阶段(Prepare),一个是提交阶段(Commit)。两个角色,一个是协调者(单个),一个是参与者(多个)。
# MySQL的2PC是为了保证单个数据库事务的完整性,让每次执行写操作时 redo_log 和 binlog 两个文件的一致性。
# 准备阶段:
# 1.协调者向所有参与者发起事务请求,询问是否可执行事务操作,然后等待各个参与者的响应
# 2.各参与者接收到协调者事务请求后,执行事务操作,并将信息记录到事务日志中
# 3.如果参与者成功执行了事务并写入日志信息,则向协调者返回 Yes 响应,否则返回 NO 响应,当然,参与者也可能宕机,不返回响应。
# 提交阶段:
# 分为两个情况:正常提交和回退。
# 1.协调者向所有参与者发送 Commit 请求
# 2.参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源
# 3.参与者执行事务提交后向协调者发送 ACK 响应
# 4.接收到所有参与者的 ACK 响应后,完成事务提交
# 中断事务:
# 在执行 Prepare 阶段过程中,如果某些参与者执行事务失败,宕机或协调者之间的网络中断,那么协调者就无法收到所有参与者的 Yes 响应,或者某个参与者返回了 No 响应,此时,协调者就会进入回退流程,对事务进行回退。
# 注:只要协调者没有收到所有参与者的 Yes 响应,就会发起事务中断请求。
# 1.协调者向所有参与者发送 rollback 请求
# 2.参与者收到 rollback 后,使用 Prepare 阶段的 undo 日志执行事务回滚,完成后释放事务执行期间占用的所有资源
# 3.参与者执行事务回滚后向协调者发送 ACK 响应
# 4.接收到所有参与者的ACK响应后,完成事务中断
# 2PC 存在的问题
# 1.单点故障:一切请求都来自协调者,一旦协调者发生故障,那么参与者会一直阻塞下去,无法完成事务操作。
# 2.性能问题:无论是上面的第一个阶段还是第二个阶段,所有的参与者和协调者的资源都是被锁定的,要等所有参与者准备完毕,协调者才会通知进行全局提交。
# 3.数据不一致:在第二阶段的时候,有的参与者接受到commit请求后,发生了网络波动,导致部分参与者接收到commit请求但是部分没有接受到,导致当接受到命令的参与者执行命令后导致数据不一致的问题。
# 4.分布式问题:如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长等),所以也无法顺利处理上一个事务。
#
# 3PC(三阶段提交)
# 三阶段提交是二阶段提交的升级版,改动点如下:
# 1.引入超时机制
# 2.在第一阶段和第二阶段中间插入了一个准备阶段,保证了在最后提交阶段前各节点的状态一致。
# 一共三个阶段:canCommit、preCommit、doCommit
# 1.canCommit:协调者向所有的参与者询问是否可以提交事务,参与者接到请求后根据自己的情况返回yes或者是no。
# 2.preCommit:协调者根据第一阶段参与者反馈的情况判断是否可以执行基于事务的preCommit操作(如果阶段一全部返回yes则向所有参与者发出preCommit请求,进入准备阶段,参与者接受到请求后,执行事务操作,将undo和redo信息记录到事务日志中,但是不提交事务,然后参与者向协调者反馈ack响应或者no响应),这里做了除提交事务外的所有事情。
# 3.doCommit:真正提交事务的阶段,如果阶段二中所有的参与者返回的都是ack,那么协调者会向参与者发出do commit请求,参与者收到请求后会执行事务提交,完成后向发起者反馈ack消息;如果阶段二中只要有一个参与者反馈no或者是超时后协调者无法收到所有参与者的反馈,即中断事务,中断是由协调者向参与者发出abort请求,参与者使用阶段一的undo信息执行回滚操作,操作完成后向协调者返回ack。
# 3PC的改进
# 1.降低了阻塞
# (1)参与者返回 canCommit 请求的响应后,等待 PreCommit 阶段的指令,若等待超时,则自动 Abort,降低了阻塞
# (2)参与者返回 PreCommit 阶段的请求响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞
# 2.解决单点故障问题
# (1)参与者返回 canCommit 请求响应后,等待 PreCommit 阶段指令,若协调者宕机,等待超时后参与者自动 Abort
# (2)参与者返回 PreCommit 请求响应后,等待 doCommit 阶段指令,若协调者宕机,等待超时后自动 commit 事务
# 3PC的不足
# 数据的不一致问题仍然可能存在,比如 doCommit 阶段协调者发出了 Abort 请求,然后有些参与者没有收到 Abort,那么就会自动 commit,造成数据不一致。
#
# TCC(Try-Confirm-Cancel)补偿事务
# TCC与2PC的思想很相似,事务处理流程也很相似,但2PC是应用于在DB层面,TCC则可以理解为在应用层面的2PC,是需要我们编写业务逻辑来实现。它的核心思想是:"针对每个操作都要注册一个与其对应的确认(Try)和补偿(Cancel)"。
# 1.Try:调用方会分别调用多个被调用方,只有当多个被调用方都返回成功的时候,才会执行confirm操作,否则就执行cancel,这个阶段其实就是一个检查资源和锁定资源的操作,在真正执行前的一个冻结操作。
# 2.Confirm:使用预留的资源,完成真正的业务操作,要求Try成功Confirm一定也是要成功的。要是个别或者全部confirm失败了或者调用超时了,和try阶段不同的是在confirm阶段不会做回滚操作,而是将所有的confirm进行重试,如果超过重试次数,则必须告警通知人工进行处理。
# 3.Cancel:这个阶段是释放预留资源的阶段,只有在try阶段失败或者异常的情况下才会执行cancel操作。而且这里只对try步骤中已经成功的被调用者进行cannel处理,同时这个步骤失败也会重试。
# TCC是柔性事务,解决了2PC中全局资源锁定导致效率低下的问题。
# 缺点:
# 1.应用侵入性强:TCC由于基于在业务层面,至使每个操作都需要有try、confirm、cancel三个接口。
# 2.开发难度大:代码开发量很大,要保证数据一致性 confirm 和 cancel 接口还必须实现幂等性。
说一下如何保证数据的最终一致性
思路:分两部分来回答,第一部分先回答事务消息的实现流程以及如何保证数据的最终一致性。第二部分用代码展示。
TransactionMQProducer transactionMQProducer = new TransactionMQProducer("producerGroup");
TransactionSendResult result = transactionMQProducer.sendMessageInTransaction(msg, null);
if(result.getSendStatus() == SendStatus.SEND_OK){
}else{
}
public interface TransactionListener {
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}
public class MyTransactionListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try{
return LocalTransactionState.COMMIT_MESSAGE;
}catch (Exception e){
}
return LocalTransactionState.ROLLBACK_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
return null;
}
}
TransactionMQProducer transactionMQProducer = new TransactionMQProducer("producerGroup");
transactionMQProducer.setTransactionListener(new MyTransactionListener());
说一下最大努力通知方案
思路:三个模块->具体流程
# 所谓最大努力交付,就是俺反正用最大努力做,能不能成功,不做完全保证
# 会涉及到三个模块
# 1.上游应用,发消息到 MQ 队列。
# 2.下游应用(例如短信服务、邮件服务),接受请求,并返回通知结果。
# 3.最大努力通知服务,监听消息队列,将消息存储到数据库中,并按照通知规则调用下游应用的发送通知接口。
# 具体流程
# 1.上游应用发送 MQ 消息到 MQ 组件内,消息内包含通知规则和通知地址
# 2.最大努力通知服务监听到 MQ 内的消息,解析通知规则并放入延时队列等待触发通知
# 3.最大努力通知服务调用下游的通知地址,如果调用成功,则该消息标记为通知成功,如果失败则在满足通知规则( MQ会按照间隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知间隔)的情况下重新放入延时队列等待下次触发。
说一下SpringMVC的工作原理
思路:DispatcherServlet->HandlerMapping->HandlerAdapter->Controller->ViewReslover
# 1.用户发送请求至前端控制器DispatcherServlet。
# 2.DispatcherServlet收到请求调用HandlerMapping处理器映射器。
# 3.处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
# 4.DispatcherServlet调用HandlerAdapter处理器适配器。
# 5.HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
# 6.Controller执行完成返回ModelAndView。
# 7.HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
# 8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
# 9.ViewReslover解析后返回具体View,这个view不是完整的,仅仅是一个页面(视图)名字,且没有后缀名。
# 10.DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
# 11.DispatcherServlet响应用户。
说一下spring bean的生命周期
思路: 对比普通对象和Spring管理bean的实例化区别->Bean的生命周期(实例化->属性赋值->初始化->使用->销毁)
# 区别
# 首先要知道的是普通java对象和spring所管理的bean实例化的过程是有些区别的。
# 普通Java环境下创建对象步骤如下:
# 1.java源码被编译为class文件。
# 2.等到类要被初始化的时候(new、反射等)。
# 3.class文件被虚拟机通过类加载器加载到jvm。
# 4.初始化对象供我们使用。
# 5.当它没有任何引用的时候被垃圾回收机制回收
# 简单来说,可以理解为它是用class对象作为‘模板’进而创建出具体的实例。
# Spring所管理的bean
# 1.除了class对象之外,还会使用BeanDefinition的实例来描述对象的信息。
# 2.由Spring IoC容器托管,对象的生命周期完全由容器控制。
# 可以理解为:Class只描述了类的信息,而BeanDefination描述了对象的信息。(即@Scope、@Lazy、@DependsOn元数据存储在BeanDifinetion中)
# 生命周期
# 前提: Spring启动的时候会调用AbstractApplicationContext.refresh()方法初始化Spring IoC容器。由容器来管理bean。
# 1.对象的实例化
# (1)Spring在启动的时候需要扫描在xml、注解、JavaConfig中需要被Spring管理的Bean信息。
# (2)随后会把这些信息放到一个beanDefinitionMap中。这个map的key是beanName,value是BeanDefinition对象。
# 到这里实际上就是把定义的元数据加载起来,目前真实对象还没实例化。
# (3)接着会遍历这个beanDefinitionMap,执行BeanFactoryPostProcessor这个Bean工厂后置处理器的逻辑。
# (4)BeanFactoryPostProcessor后置处理器执行完了以后,就到了实例化对象了。
# 在Spring里是通过反射来实现的,一般情况下会通过反射选择合适的构造器来把对象实例化。
# 但这里把对象实例化,只是把对象给创建出来,而对象具体的属性还是没有注入的。
# 比如对象是UserService,而UserService对象依赖着SendService对象,这时SendService还是null的。
# 2.对象的属性赋值
# 利用依赖注入完成Bean中所有属性值的配置注入
# 注入主要有三种方式
# (1)使用set方法注入
# (2)使用有参数构造注入
# (3)使用接口注入
# 3.对象的初始化
# (1)首先判断该Bean是否实现了Aware相关的接口,如果存在则填充相关的资源。
# 1> 如果Bean实现了BeanNameAware接口,则Spring调用Bean的setBeanName()方法传入当前Bean的id值。
# 2> 如果Bean实现了BeanFactoryAware接口,则Spring调用setBeanFactory()方法传入当前工厂实例的引用。
# 3> 如果Bean实现了ApplicationContextAware接口,则Spring调用setApplicationContext()方法传入当前ApplicationContext实例的引用。
# (2)前置处理。如果BeanPostProcessor和Bean关联,则Spring将调用该接口的预初始化方法postProcessBeforeInitialization()对bean进行加工操作。AOP就是利用此处实现的。
# (3)如果Bean实现了initializingBean接口,则Spring将调用该接口的afterPropertiesSet()方法。
# (4)如果在配置文件中通过init-method属性指定了初始化方法,则调用该初始化方法。
# (5)后置处理。如果BeanPostProcessor和Bean关联,则Spring将调用该接口的初始化方法postProcessAfterInitaillization()。此时,Bean已经可以被应用系统使用了。
# 4.对象的使用
# 1.如果在<bean>中指定了Bean的作用范围时scope="singleton",则将该Bean放入Spring IoC的缓存池中,将触发Spring对该Bean的生命周期管理。
# 2.如果在<bean>中指定了Bean的作用范围时scope="prototype",则将该Bean交给调用者。
# 5.对象的销毁
# 1.如果Bean实现了DisposableBean接口,则Spring会调用destory()方法将Spring中的Bean销毁。
# 2.如果在配置文件中通过destory-method属性指定了Bean的销毁方法,则Spring将调用该方法对Bean进行销毁。
说一下TCP三次握手
思路:定义->三次握手流程->为什么是三次不是两次、四次->半连接队列->全连接队列->三次握手过程中可以携带数据吗?
# 定义
# 三次握手其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
# 三次握手流程
# 第一次握手
# 客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态(请求连接)。
# 第二次握手
# 服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。
# 第三次握手
# 客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
# 为什么是三次不是两次、四次
# 三次握手是保证双方都得知自已和对方收发能力正常的最低值。如果只进行两次握手,服务端不知道客户端能否接收消息,同时也不知道自已的消息发出去了没有,三次握手已经保证了TCP连接的需求,所以就不用第四次握手了。
# 半连接队列
# 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
# 全连接队列
# 当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
# 三次握手过程中可以携带数据吗?
# 其实第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据
# 假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
说一下TCP四次挥手
思路:定义->为什么是四次不是三次->四次挥手流程->挥手为什么需要四次
# 定义
# TCP 的连接的拆除需要发送四个包,因此称为四次挥手。客户端或服务器均可主动发起挥手动作。
# 为什么是四次不是三次
# 建立一个连接需要三次握手,而终止一个连接要经过四次挥手。这由TCP的半关闭造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力。
# 四次挥手流程
# 第一次挥手: 客户端发送一个FIN报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态(终止等待1状态)。
# 即发出连接释放报文段,并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
# 第二次挥手: 服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值+1作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于CLOSE_WAIT(关闭等待)状态。
# 即服务端收到连接释放报文段后即发出确认报文段,服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
# 第三次挥手: 如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK(最后确认)的状态。
# 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段,服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
# 第四次挥手: 客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
# 即客户端收到服务端的连接释放报文段后,对此发出确认报文段,客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
# 在socket编程中,任何一方执行close()操作即可产生挥手操作。
# 挥手为什么需要四次
# 因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,"你发的FIN报文我收到了"。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
说一下Spring的IOC
思路:定义->传统和IOC方式创建对象方式对比->IOC解决了哪些问题->容器的两个接口->IoC初始化的过程
# 定义
# IoC,也叫控制反转。它是一种思想不是一个技术实现。控制,指的是创建、管理对象的权力。反转,将这个权力交给外部环境(比如Spring的IOC容器)。实际上IOC容器就是个Map,里面存储各种对象。
# 传统和IOC方式创建对象方式对比
# 传统的开发方式是通过new关键字去创建对象,但是使用了IOC思想之后,不再使用new去创建对象,而是使用IOC容器来帮助我们创建对象。我们需要哪个容器,直接从IOC容器里获取即可。
# IOC解决了哪些问题
# 1.降低了对象之间的耦合度。
# 2.资源变的容易管理;比如你用 Spring 容器提供的话很容易就可以实现一个单例。
# 容器的两个接口
# Spring中设计了两个接口来表示容器,分别是BeanFactory、ApplicationContext。
# 1.BeanFactory: BeanFactory可以理解为就是个 HashMap,Key 是 BeanName,Value 是 Bean 实例。通常只提供注册(put),获取(get)这两个功能。
# 2.ApplicationContext: ApplicationContext比 BeanFactory 多了更多的功能,比如:资源的获取,支持多种消息等,该接口定义了一个refresh方法,此方法用于刷新整个容器,即重新加载/刷新所有的 Bean。
# IoC初始化的过程
# IoC初始化的过程本质上是实例化Bean的过程。具体流程如果想说,参考 '说一下spring bean的生命周期' 的回答。
说一下线程相关的知识
思路:线程的创建方式->终止方式->常见的方法->基本状态->线程安全的集合
线程的创建方式
1.继承Thread类
2.实现runnable接口
3.实现callable接口
4.定时器Timer
5.线程池创建线程
6.利用java8新特性 stream 实现并发
线程的终止方式
1.正常运行结束
2.使用退出标志退出线程
3.使用Interrupt方法来中断线程
4.stop方法终止线程(线程不安全)
线程常见的方法
1.sleep(休眠)
2.yield(让步/放弃)
3.wait(等待)
4.interrupt(中断)
5.join(加入)
6.notify/notifyAll(唤醒)
线程有哪些基本状态
1.新建状态(new)
2.就绪状态(runnable)
3.运行状态(running)
4.阻塞状态(blocked)
5.线程死亡(dead)
线程安全的集合
1.CopyOnWriteArrayList
(1)线程安全的ArrayList,加强版的读写分离。
(2)写有锁,读无锁,读写之间不阻塞,优于读写锁
(3)写入时,先copy一个容器副本、再添加新元素,最后替换引用
(4)使用方式与ArrayList无异
2.CopyOnWriteArraySet
(1)线程安全的Set,底层使用CopyOnWriteArrayList实现
(2)唯一不同在于,底层使用addIfAbsent()添加元素,会遍历数组
(3)如存在元素,则不添加(扔掉副本)
3.ConcurrentLinkedQueue
(1)线程安全、可高效读写的队列,高并发下性能最好的队列
(2)无锁、CAS比较交换算法,修改的方法包含三个核心参数(V、E、N)
(3)V:要更新的变量。E:预期值。N:新值
(4)只有当V==E时,V=N;否则表示已被更新过,则取消当前操作
4.BlockingQueue接口(阻塞队列)
(1)ArrayBlockingQueue:由数据结构组成的有界阻塞队列(公平、非公平)
(2)LinkedBlockingQueue:由链表结构组成的有界阻塞队列(两个独立锁提高并发)
(3)PriorityBlockingQueue:支持优先级排序的无界阻塞队列(compareTo排序实现优先)
(4)DelayQueue:使用优先级队列实现的无界阻塞队列(缓存失效、定时任务)
(5)SynchronousQueue:不存储元素的阻塞队列(不存储数据、可用于传递数据)
(6)LinkedTransferQueue:由链表结构组成的无界阻塞队列
(7)LinkedBlockingQueue:由链表结构组成的双向阻塞队列
5.ConcurrentHashMap
说一下线程池相关的知识
思路:线程池的组成->线程池的拒绝策略->线程池的种类
线程池的组成
1.线程管理器:用于创建并管理线程池。
2.工作线程:线程池中的线程。
3.任务接口:每个任务必须实现的接口,用于工作线程调度其运行。
4.任务队列:用于存放待处理的任务,提供一种缓冲机制。
线程池的拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
JDK内置的拒绝策略如下:
1.AbortPolicy:直接抛出异常,阻止系统正常运行。
2.CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被废弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能急剧下降。
3.DiscardOldestPolicy:丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
4.DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任务处理。如果允许任务丢失,这是最好的一种策略。
5.以上内置拒绝策略均实现了RejectedExecutionHandler接口,若以上策略仍无法满足实际需要,完全可以自己扩展RejectedExecutionHandler接口。
线程池的种类
java里面线程池接口的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正线程池接口是ExecutorService。
1.newCachedThreadPool
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用他们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用execute将重用以前构造的线程(如果线程可用)。如果现在线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60s未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资源。
2.newFixedThreadPool
创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数nThreads线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
3.newScheduledThreadPool
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
4.newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
说一下volatile
思路:作用->JMM三大特性->volatile底层的实现机制->重排序->volatile两个使用例子
# 作用
# 1.类的成员变量、类的静态成员变量被volatile修饰之后,无论哪个线程修改了这个变量的值,那么这个新值对于其他线程来说都是立即可见的。(可见性)本质就是被volatile作用的属性不会被线程缓存,始终从主存中读取。
# 2.禁止进行指令重排序。
# 3.volatile可以保证可见性和有序性,不能保证原子性,但是可以和CAS结合,来保证原子性。
# JMM(java内存模型)的三大特性
# 1.原子性: Java中,对基本数据类型的读取和赋值操作是原子性操作。这些操作要么就做完,要么就都没有执行。
# 2.可见性: 一个共享变量的值如果被修改了,那么其他线程可以立即看到这个新值。Java就是利用volatile来提供可见性的。
# 3.有序性: JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。
# volatile底层的实现机制?
# 如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:
# 1.重排序时不能把后面的指令重排序到内存屏障之前的位置
# 2.使得本CPU的Cache写入内存
# 3.写入动作也会引起别的CPU或者别的内核无效化其Cache,相当于让新写入的值对别的线程可见。
# 什么是重排序?
# 为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。一般重排序可以分为如下三种:
# 1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
# 2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
# 3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
# 不管怎么重排序,单线程下的执行结果不能被改变。
# volatile使用内存屏障保证不会被执行重排序。volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
# 你在哪里会使用到volatile,举两个例子呢?
# 1.状态量标记,就如上面对flag的标记。
# 2.单例模式的实现,典型的双重检查锁定(DCL)
说一下CAS
思路:CAS算法过程->CAS三大问题
# 算法过程
# CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:
# 它包含 3 个参数CAS(V,E,N)。
# V 表示要更新的变量(内存值)
# E 表示预期值(旧的)
# N 表示新值。
# 当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
# CAS仍然存在三大问题
# 1.ABA问题
# 因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
# 2.循环时间长开销大
# 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
# 如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:
# 第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
# 第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
# 3.只能保证一个共享变量的原子操作
# 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,
说一下TCP/IP和UDP
思路:TCP/IP概念->UDP概念->TCP/IP和UDP区别->应用场景->运行在TCP或者UDP的应用层协议
# TCP/IP
# TCP/IP即传输控制/网络协议,是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达。
# UDP
# 是无连接的协议,发送数据前不需要建立连接,是没有可靠性的协议。所以可以在网络上以任何可能的路径传输,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。
# TCP/IP和UDP区别
# 1.TCP是面向连接的协议,发送数据前要先建立连接。UDP是无连接的协议,发送数据前不需要建立连接。
# 2.TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达。UDP没有可靠性。
# 3.TCP通信类似打电话,接通了,确认身份后,才开始进行通信。UDP通信类似于学校广播,靠着广播播报直接进行通信。
# 4.TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多。
# 5.TCP是面向字节流的,UDP是面向报文的。
# 面向字节流是指发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
# 6.TCP首部开销(20字节)比UDP首部开销(8字节)要大
# 7.UDP 的主机不需要维持复杂的连接状态表
# 应用场景
# 1.对某些实时性要求比较高的情况使用UDP,比如游戏,媒体通信,实时直播,即使出现传输错误也可以容忍。
# 2.其它大部分情况下,HTTP都是用TCP,因为要求传输的内容可靠,不出现丢失的情况
# 运行在TCP或者UDP的应用层协议
# 1.运行在TCP协议上的协议
# (1)HTTP(Hypertext Transfer Protocol,超文本传输协议),主要用于普通浏览。
# (2)HTTPS(HTTP over SSL,安全超文本传输协议),HTTP协议的安全版本。
# (3)FTP(File Transfer Protocol,文件传输协议),用于文件传输。
# (4)POP3(Post Office Protocol, version 3,邮局协议),收邮件用。
# (5)SMTP(Simple Mail Transfer Protocol,简单邮件传输协议),用来发送电子邮件。
# (6)TELNET(Teletype over the Network,网络电传),通过一个终端(terminal)登陆到网络。
# (7)SSH(Secure Shell,用于替代安全性差的TELNET),用于加密安全登陆用。
# 2.运行在UDP协议上的协议
# (1)BOOTP(Boot Protocol,启动协议),应用于无盘设备。
# (2)NTP(Network Time Protocol,网络时间协议),用于网络同步。
# (3)DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。
# 3.运行在TCP和UDP协议上
# (1)DNS(Domain Name Service,域名服务),用于完成地址查找,邮件转发等工作。
# (2)ECHO(Echo Protocol,回绕协议),用于查错及测量应答时间(运行在TCP和UDP协议上)。
# (3)SNMP(Simple Network Management Protocol,简单网络管理协议),用于网络信息的收集和网络管理。
# (4)DHCP(Dynamic Host Configuration Protocol,动态主机配置协议),动态配置IP地址。
# (5)ARP(Address Resolution Protocol,地址解析协议),用于动态解析以太网硬件的地址。
# TCP的拥塞避免机制
# 拥塞:对资源的需求超过了可用的资源。若网络中许多资源同时供应不足,网络的性能就要明显变坏,整个网络的吞吐量随之负荷的增大而下降。
# 拥塞控制:防止过多的数据注入到网络中,使得网络中的路由器或链路不致过载。
# 拥塞控制的方法:.
# 1.慢启动 + 拥塞避免:
# 慢启动:不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小;
# 拥塞避免:拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,这样拥塞窗口按线性规律缓慢增长。
# 2、快重传 + 快恢复:
# 快重传:快重传要求接收方在收到一个 失序的报文段 后就立即发出 重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时捎带确认。
# 快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
# 快恢复:快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半,
# 但是接下去并不执行慢开始算法:因为如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。
# 所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法。
说一下HTTP和HTTPS
思路:HTTP协议->HTTP的请求体->HTTP的响应报文->HTTPS协议->Https的工作原理->HTTP协议和HTTPS协议的区别->一次完整的HTTP请求所经历几个步骤->浏览器中输入:“ www.xxx.com ” 之后都发生了什么?请详细阐述->什么是 HTTP 协议无状态协议?怎么解决Http协议无状态协议->常用的HTTP方法->常见HTTP状态码
# 什么是HTTP协议
# 1.Http协议是对客户端和服务器端之间数据之间实现可靠性的传输文字、图片、音频、视频等超文本数据的规范,格式简称为“超文本传输协议”
# 2.Http协议属于应用层,及用户访问的第一层就是http。
# 3.Http协议运行在TCP之上,明文传输,客户端与服务器端都无法验证对方的身份;
# 什么是HTTP的请求体
# HTTP请求体是我们请求数据时先发送给服务器的数据。
# HTTP请求体由:请求行、请求头、请求数据组成的
# GET请求是没有请求体的
# HTTP的响应报文有哪些
# 1.http的响应报文是服务器返回给我们的数据,必须先有请求体再有响应报文
# 2.响应报文包含三部分 状态行、响应首部字段、响应内容实体实现
# 什么是HTTPS协议
# 1.Https是身披SSL(Secure Socket Layer)外壳的Http,运行于SSL上,SSL运行于TCP之上,是添加了加密和认证机制的HTTP。
# 2.Https的加密机制是一种共享密钥加密和公开密钥加密并用的混合加密机制。
# Https的工作原理
# 1.首先HTTP请求服务端生成证书,客户端对证书的有效期、合法性、域名是否与请求的域名一致、证书的公钥(RSA加密)等进行校验;
# 2.客户端如果校验通过后,就根据证书的公钥的有效,生成随机数,随机数使用公钥进行加密(RSA加密);
# 3.消息体产生的后,对它的摘要进行MD5(或者SHA1)算法加密,此时就得到了RSA签名;
# 4.发送给服务端,此时只有服务端(RSA私钥)能解密。
# 5.解密得到的随机数,再用AES加密,作为密钥(此时的密钥只有客户端和服务端知道)。
# HTTP协议和HTTPS协议的区别
# 1.端口不同:Http与Http使用不同的连接方式,用的端口也不一样,前者是80,后者是443;
# 2.资源消耗:和HTTP通信相比,Https通信会由于加解密处理消耗更多的CPU和内存资源;
# 3.开销:Https通信需要证书,而证书一般需要向认证机构购买;
# 一次完整的HTTP请求所经历几个步骤
# HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤
# 1.建立TCP连接 (三次握手)
# 2.Web浏览器向Web服务器发送请求行
# 3.Web浏览器发送请求头
# 4.Web服务器应答 (HTTP/1.1 200 OK 应答的第一部分是协议的版本号和应答状态码)
# 5.Web服务器发送应答头
# 6.Web服务器向浏览器发送数据
# 7.Web服务器关闭TCP连接
# 浏览器中输入:“ www.xxx.com ” 之后都发生了什么?请详细阐述。
# 1.由域名→IP地址 寻找IP地址的过程依次经过了浏览器缓存、系统缓存、hosts文件、路由器缓存、递归搜索根域名服务器。
# 2.建立TCP/IP连接(三次握手具体过程)
# 3.由浏览器发送一个HTTP请求
# 4.经过路由器的转发,通过服务器的防火墙,该HTTP请求到达了服务器
# 5.服务器处理该HTTP请求,返回一个HTML文件
# 6.浏览器解析该HTML文件,并且显示在浏览器端
# 什么是 HTTP 协议无状态协议?怎么解决Http协议无状态协议?
# HTTP 是一个无状态的协议,也就是没有记忆力,这意味着每一次的请求都是独立的,缺少状态意味着如果后续处理需要前面的信息,则它必须要重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就很快。
# HTTP 的这种特性有优点也有缺点:
# 优点:解放了服务器,每一次的请求“点到为止”,不会造成不必要的连接占用
# 缺点:每次请求会传输大量重复的内容信息,并且,在请求之间无法实现数据的共享
# 解决方案:
# 1.使用参数传递机制:
# 将参数拼接在请求的 URL 后面,实现数据的传递(GET方式),例如: /param/list?username=wmyskxz
# 问题:可以解决数据共享的问题,但是这种方式一不安全,二数据允许传输量只有1kb
# 2.使用 Cookie 技术
# 3.使用 Session 技术
# 常用的HTTP方法有哪些?
# (1)GET:用于请求访问已经被URI(统一资源标识符)识别的资源,可以通过URL传参给服务器
# (2)POST:用于传输信息给服务器,主要功能与GET方法类似,但一般推荐使用POST方式。
# (3)PUT:传输文件,报文主体中包含文件内容,保存到对应URI位置。
# (4)HEAD:获得报文首部,与GET方法类似,只是不返回报文主体,一般用于验证URI是否有效。
# (5)DELETE:删除文件,与PUT方法相反,删除对应URI位置的文件。
# (6)OPTIONS:查询相应URI支持的HTTP方法。
# 常见HTTP状态码
# 1.1xx(临时响应)
# 2.2xx(成功)
# 3.3xx(重定向):表示要完成请求需要进一步操作
# 4.4xx(错误):表示请求可能出错,妨碍了服务器的处理
# 5.5xx(服务器错误):表示服务器在尝试处理请求时发生内部错误
# 200(成功)
# 304(未修改):自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容
# 401(未授权):请求要求身份验证
# 403(禁止):服务器拒绝请求
# 404(未找到):服务器找不到请求的网页
说一下Socket
思路:什么是Socket->Socket通讯的过程->Socket和http的区别和应用场景
# 什么是Socket
# 1.网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个Socket。
# 2.Socket通常用来实现客户方和服务方的连接。Socket是TCP/IP协议的一个十分流行的编程界面,一个Socket由一个IP地址和一个端口号唯一确定。
# 3.但是,Socket所支持的协议种类也不光TCP/IP、UDP,因此两者之间是没有必然联系的。在Java环境下,Socket编程主要是指基于TCP/IP协议的网络编程。
# 4.socket连接就是所谓的长连接,客户端和服务器需要互相连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉的,但是有时候网络波动还是有可能的。
# 5.Socket偏向于底层。一般很少直接使用Socket来编程,框架底层使用Socket比较多
# 6.Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
# 在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
# Socket通讯的过程
# 1.基于TCP:
# 服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。
# 在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。
# 客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。
# 2.基于UDP:
# UDP 协议是用户数据报协议的简称,也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议,但有时在需要较快地接收数据并且可以忍受较小错误的情况下,UDP 就会表现出更大的优势。
# 我客户端只需要发送,服务端能不能接收的到我不管。
# Socket和http的区别和应用场景
# 1.Socket连接就是所谓的长连接,理论上客户端和服务器端一旦建立起连接将不会主动断掉。
# http连接就是所谓的短连接,即客户端向服务器端发送一次请求,服务器端响应后连接即会断开等待下次连接
# 2.Socket适用场景:网络游戏,银行持续交互,直播,在线视屏等。
# http适用场景:公司OA服务,互联网服务,电商,办公,网站等等。
说一下websocket
思路:概念->特点->为什么需要WebSocket->WebSocket与HTTP的区别->WebSocket连接的过程
->WebSocket应用场景->websocket断线重连原因以及处理办法
# 概念
# 1.WebSocket 是一种在单个TCP连接上进行全双工通信的协议。
# 2.WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。弥补了http协议在持久通信能力上的不足。本质上属于计算机网络应用层的协议。
# 3.在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
# WebSocket的特点
# (1)建立在 TCP 协议之上,服务器端的实现比较容易。
# (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
# (3)数据格式比较轻量,性能开销小,通信高效。
# (4)可以发送文本,也可以发送二进制数据。
# (5)没有同源限制,客户端可以与任意服务器通信。
# (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
# 为什么需要WebSocket?
# 因为 HTTP 协议有一个缺陷:通信只能由客户端发起,不具备服务器推送能力。
# WebSocket与HTTP的区别
# 1.相同点
# (1)都是一样基于TCP的
# (2)都是可靠性传输协议
# (3)都是应用层协议
# WebSocket在建立握手时,数据是通过HTTP传输的。但是建立之后,在真正传输时候是不需要HTTP协议的。
# 2.不同点
# (1)WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的
# (2)WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接
# WebSocket连接的过程
# 1.首先,客户端发起http请求,经过3次握手后,建立起TCP连接。http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
# 2.然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
# 3.最后,客户端收到连接成功的消息后,开始借助于TCP传输通道进行全双工通信。
# WebSocket应用场景
# 1.即时聊天通信
# 2.多玩家游戏
# 3.在线协同编辑/编辑
# 4.实时数据流的拉取与推送
# 5.体育/游戏实况
# 6.实时地图位置
# websocket断线重连原因以及处理办法
# 1.心跳
# 心跳就是客户端定时的给服务端发送消息,证明客户端是在线的,如果超过一定的时间没有发送则就是离线了。
# 2.如何判断在线离线
# (1)当客户端第一次发送请求至服务端时会携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果不存在就存入db或者缓存中,
# (2)第二次客户端定时再次发送请求依旧携带唯一标识、以及时间戳,服务端到db或者缓存去查询改请求的唯一标识,如果存在就把上次的时间戳拿取出来,
# 使用当前时间戳减去上次的时间,得出的毫秒秒数判断是否大于指定的时间,若小于的话就是在线,否则就是离线。
# 3.断线原因及处理办法
# (1)主动断开连接 ws.close();
# 这个没什么好说的了,手动关闭的。
# (2)websocket超时没有消息自动断开连接
# 先看服务端设置的超时时长是多少。之后在小于超时时间内发送心跳包,有2种方案:一种是客户端主动发送上行心跳包,另一种方案是服务端主动发送下行心跳包。
# 如果是nginx代理的websocket转发,可以通过修改nginx配置信息来处理。
# (3)websocket异常包括服务端出现中断,交互切屏等等客户端异常中断
# 1>当服务端宕机了,客户端做法服务端再次上线时怎么做?
# 客户端则需要断开连接,通过onclose关闭连接。
# 2>当服务端再次上线时做法
# 服务端再次上线时则需要清除缓存的数据,若不清除 则会造成只要请求到服务端的都会被视为离线。
# 针对这种异常的中断解决方案就是处理重连
# 1.重连方案是使用js库处理:引入reconnecting-websocket.min.js,ws建立链接方法使用js库api方法
# 2.断网监测支持使用js库:offline.min.js