导图社区 Java面试题
这是一篇关于Java面试题的思维导图,主要内容包括:锁,SpringCloud Alibaba系列,Spring Cloud 系列,Redisson数据网格与分布式锁,Elasticsearch,数据机构和算法,设计模式,Maven,Linux,JVM,JUC,Nginx,Rabbitmq,Docker,Redis,MySQL,Mybatis,Spr。
编辑于2024-03-28 15:33:40Java面试题
Java基础
面向对象
什么是面向对象?
什么是面向对象? 对比面向过程,是两种不同的处理问题的角度。 面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么。 比如:洗衣机洗衣服 面向过程会将任务拆解成一系列的步骤(函数): 1.打开洗衣 → 2.放衣服 → 3.放洗衣粉 → 4.清洗 → 5.烘干 面向对象会拆出人和洗衣机两个对象: 人:打开洗衣机 放衣服 放洗衣粉 洗衣机:清洗 烘干 从以上例子能看出,面向过程比较直接高效,而面向对象更易于复用、扩展和维护。
面向对象的三大特征
1. 封装 封装的意义,保护属性,不让类以外的程序直接访问和修改。 影藏方法细节,对外提供get和set方法。 2. 继承 继承父类的方法,并做出自己的改变或扩展。 3. 多态 继承,方法重写,父类引用指向子类对象。
==和equals的区别
1. == 如果比较的是基本数据类型,比较的是变量的值。 如果比较的是引用数据类型,比较的是地址值(两个对象是否指向同一块内存) 2. equals 如果没有重写equals方法,比较的是两个对象的地址值。 如果重写了equals方法,我们往往比较的是对象中的属性的内容。 equals方法是从Object类中继承的,Object默认的实现方式就是==。 3. String类中被复写的equals()方法其实是比较两个字符串的内容
String能被继承吗?
不能,String类飞final修饰的,final修饰的类是不能被继承的。 为了安全,String类中有很多调用底层的本地方法,调用了操作系统的API,是禁止被继承和重写的。
hashCode与equals
hashCode介绍
hashCode介绍 hashCode()的作用是获取哈希码,也称为散列码,它实际上是返回一个int型的整数; 这个哈希码的作用就是确定该对象在哈希表中的索引位置,hashCode()定义在Object中,Java中任何类都包含hashCode()函数。
为什么要有hashCode
以“HashSet如何检查重复”为例子来说明为什么要有hashCode: 对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值。如果没有,HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。 如果两个对象相等,则hashCode一定也是相同的。 两个对象相等,对两个对象分别调用equals方法都返回true。 两个对象的hashcode值相同,它们也不一定是相等的。 equals方法被覆盖过,则hashCode()方法也必须被覆盖。 hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
Final、Finally、Finalize
final:修饰符(关键字)有三种用法:修饰类、变量、方法。 修饰类:该类不能被继承。 修饰变量:在定义时必须赋初始值,并且不能在修改,即常量。 修饰方法:子类不能重写,但可以重载。 finally:通常放在try...catch的后面构造最终执行代码块,可以释放外部资源的代码写在里面。 finalize:Object类中定义的方法,当对象被回收时,系统自动调用该对象的finalize方法。子类可以重写该方法,做一些释放资源的操作。 对象什么时候被回收? 当某个对象没有任何引用时,则jvm就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize方法。
String、StringBuffer、StringBuilder
区别
String、StringBuffer、StringBuilder String是final修饰的,不可变,每次操作都会产生新的String对象。 StringBuffer和StringBuilder都是在原对象上操作。 StringBuffer是线程安全的,StringBuilder线程不安全的。 StringBuffer方法都是synchronized修饰的。 性能:StringBuilder > StringBuffer > String
应用场景
经常需要改变字符串内容时使用StringBuffer、StringBuilder,优先使用StringBuilder,多线程使用共享变量时使用StringBuffer。
重载和重写的区别
重载
重载 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写
重写 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
接口和抽象类的区别
区别
接口和抽象类的区别 抽象类可以存在普通成员方法,而接口中只能存在public abstract 方法。 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的。 抽象类只能继承一个,接口可以实现多个。
接口设计的目的
是对类的行为进行约束,也就是提供一种机制,可以强制要求不同的类具有相同的行为。它只约束了行为的有无, 但不对如何实现行为进行限制。
抽象类的设计目的
是代码复用。当不同的类具有某些相同的行为(记为行为集合A),且其中一部分行为的实现方式一致时(A的非真子集,记为B),可以让这些类都派生于一个抽象类。在这个抽象类中实现了B,避免让所有的子类来实现B,这就达到了代码复用的目的。而A减B的部分,留给各个子类自己实现。正是因为A-B在这里没有实现,所以抽象类不允许实例化出来(否则当调用到A-B时,无法执行)。
理解
抽象类是对类本质的抽象,表达的是 is a 的关系,比如: BMW is a Car 。抽象类包含并实现子类的通 用特性,将子类存在差异化的特性进行抽象,交由子类去实现。 而接口是对行为的抽象,表达的是 like a 的关系。比如: Bird like a Aircraft (像飞行器一样可以飞),但其本质上 is a Bird 。接口的核心是定义行为,即实现类可以做什么,至于实现类主体是谁、 是如何实现的,接口并不关心。
应用场景
当你关注一个事物的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
集合
集合框架体系图

List和Set的区别
List
有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出 所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set
无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素。
ArrayList和LinkedList区别
ArrayList和LinkedList区别 (1)ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。 (2)对于随机访问get和set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。 (3)对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。 这一点要看实际情况的。若只对单条数据插入或删除,ArrayList的速度反而优于LinkedList。但若是批量随机的插入删除数据,LinkedList的速度大大优于ArrayList.,因为ArrayList每插入一条数据,要移动插入点及之后的所有数据。 
ArrayList
ArrayList中维护了一个Object类型的数组elementData。 当创建对象时,如果使用的是无参构造器,则初始elementData容量为0。 当添加元素时:先判断是否需要扩容,如果需要扩容,则调用grow方法,否则直接添加元素到合适位置。 如果使用的是无参构造器,如果第一次添加,需要扩容的话,则扩容elementData为10,如果需要再次扩容的话,则扩容elementData为1.5倍。 如果使用的是指定容量capacity的构造器,则初始elementData容量为capacity。
LinkedList
LinkedList实现了双向链表和双端队列特点。 可以添加任意元素(元素可以重复),包括null。 线程不安全,没有实现同步。
LinkedList的底层操作机制
LinkedList底层维护了一个双向链表。 LinkedList中维护了两个属性first和last分别指向首节点和尾节点。 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表。 所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率较高。 
如果我们改查的操作多,选择ArrayList 如果我们增删的操作多,选择LinkedList
Vector和ArrayList的比较

HashMap和HashTable区别
HashMap和HashTable区别 (1)线程安全性不同 HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronize的,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己增加同步处理。 (2)是否提供contains方法 HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。 (3)key和value是否允许null值 HashTable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。 (4)数组初始化和扩容机制 HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,HashTable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。 (5)HashTable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
HashMap
jdk1.8底层使用 数组 + 链表 + 红黑树实现 扩容:默认16,满后,2倍。 树化:高度到8,并且数组的长度到64,会进行树化操作(转换为红黑树)。
HashTable
自动装箱和拆箱( 含jdk 5.0)
装箱:基本数据类型转换为包装类对象。 拆箱:包装类对象转换成基本类型的值。 1. 为什么要引入自动装箱和拆箱的功能? 可以提供一些便利的属性和方法。 2. 自动装箱底层调用的是valueOf方法。 3. 区别 (1) Integer是int的包装类,int则是Java的一种基本数据类型。 (2) Integer变量必须实例化后才能使用,int不需要。 (3) Integer实际是对象的引用,int直接存储的数据值。
泛型
概念:泛型又称参数化类型,是Jdk5.0 出现的新特性,解决数据类型的安全性问题。
好处
编译时,检查添加元素的类型,提高了安全性。
减少了类型转换的次数, 提高效率。
不在提示编译警告
多线程
线程的生命周期?线程有几种状态
线程的状态 新建状态(New) :线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。 运行状态(Running):线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种: 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 (5) 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
sleep()、wait()、join()、yield()的区别
sleep()、wait()、join()、yield()的区别 sleep 是 Thread 类的静态本地方法,wait 则是 Object 类的本地方法。 sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中。 sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字。 sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要(不指定时间需要被别人中断)。 yield()执行后线程直接进入就绪状态,马上释放了cpu的执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行 join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程
Thread、Runable的区别
Thread、Runable的区别 Thread和Runnable的实质是继承关系,没有可比性。无论使用Runnable还是Thread,都会new Thread,然后执行run方法。用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简 单的执行一个任务,那就实现runnable。
Spring
spring是什么?
spring是什么? Spring 是一个开源框架,它是一个IOC 和 AOP容器框架,用来装javabean(java对象)。
IOC
控制反转(IOC) 传统的 java 开发模式中,当需要一个对象时,我们会自己使用 new 或者 getInstance 等直接或者间接调用构造方法创建一个对象。而在 spring 开发模式中,spring 容器使用了工厂模式为我们创建了所需要的对象,不需要我们自己创建了,直接调用spring 提供的对象就可以了,这是控制反转的思想。
DI
依赖注入(DI) spring 使用 javaBean 对象的 set 方法或者带参数的构造方法为我们在创建所需对象时将其属性自动设置所需要的值的过程,就是依赖注入的思想。
AOP
面向切面编程(AOP) 在面向对象编程(oop)思想中,我们将事物纵向抽成一个个的对象。而在面向切面编程中,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制、事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想。AOP 底层是动态代理,如果是接口采用 JDK 动态代理,如果是类采用CGLIB 方式实现动态代理。
AOP中的几个术语
通知(Advice)
通知就是增强的代码,比如前置增强的代码、后置增强的代码、异常增强代码。
切面(Aspect)
切面就是包含有通知代码的类叫切面。
横切关注点
横切关注点,就是我们可以添加增强代码的位置。
目标(Target)
目标对象就是被关注的对象,或者被代理的对象。
代理(Proxy)
为了拦截目标对象方法,而被创建出来的那个对象,就叫做代理对象。
连接点(Joinpoint)
连接点指的是横切关注点和程序代码的连接,叫连接点。
切入点(pointcut)
切入点指的是用户真正处理的连接点,叫切入点。
@Autowired自动装配
先按类型查找,找到一个就注入。
如果按类型查找到多个,接着按属性名做为id继续查找并注入。
@Autowired 注解如果标识在方法上,则Bean对象创建好之后就会马上调用,一般用于给JavaBean属性初始化赋值。
BeanFactory和ApplicationContext有什么区别?
BeanFactory和ApplicationContext有什么区别? ApplicationContext是BeanFactory的子接口。 BeanFactory需要手动注册,而ApplicationContext则是自动注册。 BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。 ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。
Spring Bean的生命周期?
Spring Bean的生命周期? 默认情况下,IOC容器中bean的生命周期分为五个阶段: 调用构造器 或者是通过工厂的方式创建Bean对象。 给bean对象的属性注入值。 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的。 使用。 IOC容器关闭时, 销毁Bean对象。 当加入了Bean的后置处理器后,IOC容器中bean的生命周期分为七个阶段: 调用构造器 或者是通过工厂的方式创建Bean对象。 给bean对象的属性注入值。 执行Bean后置处理器中的 postProcessBeforeInitialization。 调用初始化方法,进行初始化, 初始化方法是通过init-method来指定的。 执行Bean的后置处理器中 postProcessAfterInitialization。 使用。 IOC容器关闭时, 销毁Bean对象。
Spring支持的几种bean的作用域
Spring支持的几种bean的作用域 Singleton 单例的 Prototype 原型的 Request Session
Spring框架中的单例Bean是线程安全的么?
Spring框架中的单例Bean是线程安全的么? 最简单的办法就是改变bean的作用域 把 "singleton"改为’‘protopyte’ 这样每次请求Bean就相当于是 new Bean() 这样就可以保证线程的安全了。
Spring 框架中都用到了哪些设计模式?
Spring 框架中都用到了哪些设计模式? 代理模式——spring 中两种代理方式,若目标对象实现了若干接口,spring 使用jdk 的java.lang.reflect.Proxy类代理。若目标兑现没有实现任何接口,spring 使用 CGLIB 库生成目标类的子类。 单例模式——在 spring 的配置文件中设置 bean 默认为单例模式。 工厂模式——在工厂模式中,我们在创建对象时不会对客户端暴露创建逻辑,并且是通过使用同一个接口来指向新创建的对象。Spring 中使用 beanFactory 来创建对象的实例。
Spring事务的实现方式和原理以及隔离级别?
Spring事务的实现方式和原理以及隔离级别? 在使用Spring框架时,可以有两种使用事务的方式,一种是编程式的,一种是申明式的,@Transactional注解就是申明式的。 比如我们可以通过在某个方法上增加@Transactional注解,就可以开启事务,这个方法中所有的sql都会在一个事务中执行,统一成功或失败。 在一个方法上加了@Transactional注解后,Spring会基于这个类生成一个代理对象,会将这个代理对象作bean,当在使用这个代理对象的方法时,如果这个方法上存在@Transactional注解,那么代理逻辑会先把事务的自动提交设置为false,然后再去执行原本的业务逻辑方法,如果执行业务逻辑方法没有出现异常,那么代理逻辑中就会将事务进行提交,如果执行业务逻辑方法出现了异常,那么则会将事务进行回滚。 针对哪些异常回滚事务是可以配置的,可以利用@Transactional注解中的rollbackFor属性进行 配置,默认情况下会对RuntimeException和Error进行回滚。
spring事务传播机制
spring事务传播机制 多个事务方法相互调用时,事务如何在这些方法间传播: 方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。 REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务。
spring事务什么时候会失效?
spring事务什么时候会失效? spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有如下几种: 方法不是public的,@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。 数据库不支持事务。 没有被spring管理。
说出Spring 或者 Springmvc中常用的5个注解,并解释含义
说出Spring 或者 Springmvc中常用的5个注解,并解释含义: @Component 基本注解,标识一个受Spring管理的组件 @Controller 标识为一个表示层的组件 @Service 标识为一个业务层的组件 @Repository 标识为一个持久层的组件 @Autowired 自动装配 @Qualifier(“”) 具体指定要装配的组件的id值 @RequestMapping() 完成请求映射 @PathVariable 映射请求URL中占位符到请求处理方法的形参
SpringMVC
Spring MVC的工作流程
SpringMVC的工作流程  用户发送请求至前端控制器DispatcherServlet DispatcherServlet收到请求调用HandlerMapping处理器映射器。 处理器映射器找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。 DispatcherServlet调用HandlerAdapter处理器适配器 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。 Controller执行完成返回ModelAndView HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet DispatcherServlet将ModelAndView传给ViewReslover视图解析器 ViewReslover解析后返回具体View DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。 DispatcherServlet响应用户
Spring MVC的主要组件?
Handler:也就是处理器。它直接应对着MVC中的C也就是Controller层,它的具体表现形式有很多,可以是类,也可以是方法。在Controller层中@RequestMapping标注的所有方法都可以看成是一个Handler,只要可以实际处理请求就可以是Handler。 HandlerMapping:处理器映射器,根据用户请求的资源uri来查找Handler的。在SpringMVC中会有很多请求,每个请求都需要一个Handler处理,具体接收到一个请求之后使用哪个Handler进行,这就是HandlerMapping需要做的事。 HandlerAdapter:适配器。因为SpringMVC中的Handler可以是任意的形式,只要能处理请求就ok,但是Servlet需要的处理方法的结构却是固定的,都是以request和response为参数的方法。如何让固定的Servlet处理方法调用灵活的Handler来进行处理呢?这就是HandlerAdapter要做的 事情。 Handler是用来干活的工具;HandlerMapping用于根据需要干的活找到相应的工具;HandlerAdapter是使用工具干活的人。
SpringBoot
怎么理解SpringBoot框架(和Spring的关系)?
怎么理解SpringBoot框架? Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
Spring Boot的优点
Spring Boot的优点 独立运行 简化配置 自动配置 无代码生成和XML配置
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?
Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? 启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项;如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 @ComponentScan:Spring组件扫描。
Spring Boot 自动配置原理是什么?
Spring Boot 自动配置原理是什么? 注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。 @EnableAutoConfiguration是实现自动配置的注解。 @Configuration表示这是一个配置文件。
SpringBoot配置文件有哪些?怎么实现多环境配置?
SpringBoot配置文件有哪些?怎么实现多环境配置? application.yml 多环境配置 applcation.yml application-dev.yml application-test.yml application-prod.yml 
Spring Boot和Spring Cloud是什么关系
Spring Boot和Spring Cloud是什么关系 Spring Boot 是 Spring 的一套快速配置脚手架,可以基于Spring Boot 快速开发单个微服务。 Spring Cloud是一个基于Spring Boot实现的开发工具。 Spring Boot专注于快速、方便集成的单个微服务个体,Spring Cloud关注全局的服务治理框架。 Spring Cloud很大的一部分是基于Spring Boot来实现,必须基于Spring Boot开发。 可以单独使用Spring Boot开发项目,但是Spring Cloud离不开 Spring Boot。
SpringCloud常用组件
SpringCloud常用组件 服务发现——Netflix Eureka实现服务治理(服务注册与发现)。 远程调用——Feign 实现服务间的远程调用。 服务网关——Gateway网关,路由,负载均衡等多种作用。 分布式配置——Spring Cloud Config配置管理。 断路器——Netflix Hystrix断路器,保护系统,控制故障范围。 客服端负载均衡——Netflix Ribbon主要提供客户侧的软件负载均衡算法。
Eureka和zookeeper的区别
Eureka和zookeeper的区别 著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。在此Zookeeper保证的是CP, 而Eureka则是AP。
Spring Cloud 系列
Eureka注册中心/配置中心
Hystrix服务熔断
OpenFeign声明式远程调用
Gateway网关
客服端负载均衡——Netflix Ribbon
SpringCloud Alibaba系列
Nacos注册中心/配置中心
Sentinel服务熔断
Seata分布式事务
Mybatis
mybatis的优缺点
mybatis的优缺点 优点: SQL 写在XML 里,解除 sql 与程序代码的耦合,便于统一管理; 与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接; 能够与 Spring 很好的集成; 缺点: SQL 语句的编写工作量较大, 尤其当字段多、关联表多时, 对开发人员编写SQL 语句的功底有一定要求。 SQL 语句依赖于数据库, 导致数据库移植性差, 不能随意更换数据库。
#{}和${}的区别是什么
#{}和${}的区别是什么 #{}是预编译处理、是占位符, ${}是字符串替换、是拼接符。 Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值。 Mybatis 在处理${}时, 就是把${}替换成变量的值,调用 Statement 来赋值。 #{} 的变量替换是在DBMS 中、变量替换后,#{} 对应的变量自动加上单引号。 ${} 的变量替换是在 DBMS 外、变量替换后,${} 对应的变量不会加上单引号。 使用#{}可以有效的防止 SQL 注入, 提高系统安全性。
MySQL
事务的四大特性
事务特性ACID:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。 原子性:是指事务包含的所有操作要么全部成功,要么全部失败回滚。 一致性:是指一个事务执行之前和执行之后都必须处于一致性状态。比如a与b账户共有1000块,两人之间转账之后无论成功还是失败,它们的账户总和还是1000。 隔离性:同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。 持久性:事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
事务的并发问题?
事务的并发问题? 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读到的数据是脏数据。 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。 幻读:当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,就像产生幻觉一样,这就是发生了幻读。 不可重复读和脏读的区别是:脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。 幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修改,幻读的重点在于新增或者删除。 事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。
事务的隔离级别

索引
什么是索引?
什么是索引? 索引是存储引擎用于提高数据库表的访问速度的一种数据结构。
索引的优缺点?
优点: 加快数据查找的速度。 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度。 加快表与表之间连接的速度。 缺点: 建立索引需要占用物理空间。 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长。
索引的作用?
索引的作用? 数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。
什么情况下需要建立索引?
什么情况下需要建索引? 经常用于查询的字段。 经常用于连接的字段建立索引,可以加快连接的速度。 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度。
什么情况下不建索引?
什么情况下不建索引? 1. where条件中用不到的字段不适合建立索引。 2. 表记录较少。 3. 需要经常增删改。 4. 参与列计算的列不适合建索引。 5. 区分度不高的字段不适合建立索引,如性别等
索引的底层实现是什么?
索引的底层实现是什么? InnoDB 存储引擎是用 B+Tree 实现其索引结构。
索引的数据结构
索引的数据结构 索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类型有B+树索引和哈希索引,默认的索引类型为B+树索引。 B+树索引 B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。 在 B+树中,节点中的 key 从左到右递增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。
索引分类
索引分类 1. 主键索引:名为primary的唯一非空索引,不允许有空值。 2. 唯一索引:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一约束的列可以为null且可以存在多个null值。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。 3. 组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。 4. 全文索引:只有在MyISAM引擎上才能使用,只能在CHAR、VARCHAR和TEXT类型字段上使用全文索引。
什么是最左匹配原则?
什么是最左匹配原则? 如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配,后面的字段不会用到索引。 对(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。如果查询条件为a = 1 and b > 2 and c = 3,那么a、b个字两段能用到索引,而c无法使用索引,因为b字段是范围查询,导致后面的字段无法使用索引。
索引设计原则
索引的设计原则? 索引列的区分度越高,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。 尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。 索引不是越多越好,每个索引都需要额外的物理空间,维护也需要花费时间。 利用最左前缀原则。
索引什么时候会失效?
索引什么时候会失效? 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引。 以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引。 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效。 判断索引列是否不等于某个值时。 对索引列进行运算。 查询条件使用or连接,也会导致索引失效。
常见的存储引擎有哪些?
InnoDB存储引擎
InnoDB存储引擎 InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。 优点:支持事务和崩溃修复能力;引入了行级锁和外键约束。 缺点:占用的数据空间相对较大。 适用场景:需要事务支持,并且有较高的并发读写频率。
MyISAM存储引擎
MyISAM存储引擎 数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI。 优点:访问速度快。 缺点:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。 适用场景:对事务完整性没有要求;表的数据都会只读的。。
MyISAM和InnoDB的区别?
MyISAM和InnoDB的区别? 1. 是否支持行级锁 : MyISAM 只有表级锁,而InnoDB 支持行级锁和表级锁,默认为行级锁。 2. 是否支持事务和崩溃后的安全恢复:MyISAM 不提供事务支持。而InnoDB提供事务支持,具有事务、回滚和崩溃修复能力。 3. 是否支持外键: MyISAM不支持,而InnoDB支持。 4. MyISAM不支持聚集索引,InnoDB支持聚集索引。
MySQL新能优化
MySQL性能优化 1. 将where中用的比较频繁的字段建立索引。 2. select语句中避免使用 “*” 。 3. 避免在索引列上进行计算。 4. 尽量缩小查询的结果。
大表怎么优化?
大表怎么优化? 某个表有近千万数据,查询比较慢,如何优化? 当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: 1. 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内; 2. 读写分离:经典的数据库拆分方案,主库负责写,从库负责读; 3. 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。
MySQL中SQL执行流程

乐观锁和悲观锁是什么?
乐观锁和悲观锁是什么? 悲观锁:假定会发生并发冲突,在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制。 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加version字段,在修改提交之前检查version与原来取到的version值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS算法实现。
Redis
Redis为何这么快?
基于内存; 单线程减少上下文切换,同时保证原子性; 多路IO复用;
为何使用单线程?
因为Redis是基于内存操作,CPU不会成为Redis的瓶颈,而最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。
缓存三大问题以及解决方案
使用 incr、decr、setnx 等原子操作; 客户端加锁; 使用 Lua 脚本实现 CAS 操作。
缓存穿透
查询数据不存在(Redis和数据库都没有)。 解决: 缓存空值。 key值校验,如布隆过滤器。
缓存击穿
缓存过期,伴随大量对该key的请求。 解决: 互斥锁。 热点数据永不过期。 熔断降级。
缓存雪崩
同一时间大批量key过期。 解决: 热点数据永不过期。 随机分散过期时间。
如何保证Redis的高并发
Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。
Redis如何保证原子性
因为 Redis 是单线程的,所以 Redis 提供的 API 也是原子操作。但我们业务中常常有先 get 后 set 的业务常见,在并发下会导致数据不一致的情况。
解决
使用 incr、decr、setnx 等原子操作; 客户端加锁; 使用Lua脚本实现CAS操作。
应用场景
string:缓存、限流、分布式锁、计数器、分布式 Session 等。 hash:用户信息、用户主页访问量、组合查询等。 list:简单队列、关注列表时间轴。 set:赞、踩、标签等。 zset:排行榜、好友关系链表。
Redis 有哪些持久化机制?
RDB
RDB 持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式。也就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为 dump.rdb。 RDB 支持 同步(save 命令)、后台异步(bgsave)以及自动配置三种方式触发。 
优点
RDB 文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。 生成 RDB 文件时支持异步处理,主进程不需要进行任何磁盘IO操作。 RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
缺点
RDB 快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。且在快照持久化期间修改的数据不会被保存,可能丢失数据。
AOF
全量备份总是耗时的,有时候我们提供一种更加高效的方式 AOF,其工作机制更加简单:会将每一个收到的写命令追加到文件中。 随着时间推移,AOF 持久化文件也会变的越来越大。为了解决此问题,Redis 提供了 bgrewriteaof 命令,作用是 fork 出一条新进程将内存中的数据以命令的方式保存到临时文件中,完成对AOF 文件的重写。 AOF 也有三种触发方式:1)每修改同步 always 2)每秒同步 everysec 3)不同no:从不同步。
优点
AOF 可以更好的保护数据不丢失,一般 AOF 隔 1 秒通过一个后台线程执行一次 fsync 操作。 AOF 日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。 AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。 AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。
缺点
对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大
先删后写还是先写后删?
先删缓存后写 DB
比如两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
产生脏数据的概率较大
先写 DB 再删缓存
比如一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后之前的那个读操作再把老的数据放进去,所以会造成脏数据。
产生脏数据的概率较小
解决方案
缓存设置过期时间,实现最终一致性
Redis 有哪些数据结构?
五大数据类型,三种特殊类型
Redis 的数据过期策略是什么?
如何设置 Redis 中数据的过期时间?
expire key time (以秒为单位)–这是最常用的方式
常见的过期策略
定时删除
在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时,对 key 进行删除。
惰性删除
定期删除
每隔一段时间执行一次删除(在 redis.conf 配置文件设置,1s 刷新的频率)过期 key 操作。
Redis采用的过期策略
Redis 采用了惰性删除+定期删除的方式处理过期数据。
流程
在进行get或setnx等操作时,先检查key是否过期;
若过期,删除key,然后执行相应操作;
若没过期,直接执行相应操作。
持久化文件对过期策略的处理?
过期 key 是不会写入 RDB 和 AOF 文件,同时数据恢复时也会做过期验证。
Redis 集群搭建有几种模式?
主从模式
哨兵模式
Cluster 集群模式
Redis 主从复制的实现?
全量同步
增量同步
Redis 的主从同步策略?
主从同步刚连接的时候进行全量同步,全量同步结束后开始增量同步。
哨兵模式的原理?
每个哨兵会向其它哨兵、master、slave 定时发送消息,以确认对方是否还存活。如果发现对方在配置的指定时间内未回应,则暂时认为对方已挂。若“哨兵群”中的多数 sentinel 都报告某一 master 没响应,系统才认为该 master “彻底死亡”,通过一定的 vote 算法从剩下的 slave 节点中选一台提升为 master,然后自动修改相关配置。
Nginx
什么是Nginx?
什么是Nginx? Nginx是一个 轻量级/高性能的反向代理Web服务器。他实现非常高效的反向代理、负载均衡。
为什么Nginx性能这么高?
为什么Nginx性能这么高? 因为他的事件处理机制采用的是异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决。
Nginx怎么处理请求的?
Nginx怎么处理请求的? nginx接收一个请求后,首先由listen和server_name指令匹配server模块,再匹配server模块里的location,location就是实际地址。
什么是正向代理和反向代理?
什么是正向代理和反向代理? 正向代理 正向代理就是一个人发送一个请求直接就到达了目标的服务器。 反向代理 反方代理就是请求统一被Nginx接收,nginx反向代理服务器接收到之后,按照一定的规则分发给了后端的业务处理服务器进行处理了。
反向代理的优点是什么?
反向代理的优点是什么? 很好的隐藏了后端服务器的IP。
Nginx负载均衡的算法怎么实现的?策略有哪些?
Nginx负载均衡的算法怎么实现的?策略有哪些? 轮询(默认)。 权重(weight)。 ip_Hash(ip绑定):每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,。
Nginx配置高可用性怎么配置?
Nginx配置高可用性怎么配置? 当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用。
限流是怎么做的?
限流是怎么做的? 漏桶算法 突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。  令牌桶算法 令牌桶算法的机制如下:存在一个大小固定的令牌桶,会以恒定的速率源源不断产生令牌。如果令牌消耗速率小于生产令牌的速度,令牌就会一直产生直至装满整个令牌桶。 
Rabbitmq
什么是MQ?
什么是MQ? MQ(message Queue)消息队列,是先进先出的一种数据结构。生产者把消息放到队列,然后由消费者去消费。
什么是队列?
什么是队列? 队列是一种先进先出的数据结构。
为什么使用MQ?
为什么使用MQ? 异步处理:提高了系统的吞吐量。 流量消锋:缓解短时间内的并发请求。 应用解耦:系统间通过消息通信,不关心其它系统的处理。
什么是RabbitMQ?
什么是RabbitMQ? RabbitMQ是一款开源的,Erlang语言编写的,消息中间件。可以用它来:解耦、异步、削峰。
RabbitMQ使用场景?
RabbitMQ使用场景? 服务间异步通信。 顺序消费。 定时任务。 请求削峰。
RabbitMQ的工作模式
RabbitMQ的工作模式 1. simple模式(简单模式)  生产者将消息放入队列。 消费者监听消息队列,如果队列中有消息,就消费,消息被拿走后,自动从队列中删除。 2. work工作模式  生产者将消息放入队列,消费者可以有多个,2个消费者同时监听一个队列来消费消息。 3. publish/subscribe发布订阅  生产者将消息发送给broker,由交换机将消息投递到与之绑定的每一个队列中。 每个消费者监听自己的队列。 4. Routing路由模式  路由模式下,对应使用的交换机是Direct交换机,生产者发送消息时需要指定routing key,交换机会根据routing key将消息投递到指定的队列。 5. Topic主题模式  * 代表通配一个单词。 # 代表通配多个单词。 采用的交换机是Topic类型的交换机。 生产者发送消息时指定Routing key,Topic交换机根据Routing key将消息投递到一个或多个队列中(发送消息时指定Routing key)。 6. RPC模式  RPC模式是一种远程过程调用模式,目前了解得还不多
消息重复消费
什么是重复消费?
什么是重复消费? 正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给MQ(消息队列),消息队列就知道该消息被消费了,就会将该消息从消息队列中删除。 消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
解决方案
重复消费解决方案 MQ 消费者的幂等性的解决一般使用全局 ID或者写个唯一标识比如时间戳或者 UUID 或者订单,消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。 消费端的幂等性保障:在海量订单生成的业务高峰期,生产端有可能就会重复发消息,这时候消费端就要实现幂等性。这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。 业界主流的幂等性有两种操作:a.唯一 ID+指纹码机制,利用数据库主键去重;b.利用 redis 执行 setnx 命令,天然具有幂等性,从而实现不重复消费。 唯一 ID+指纹码机制 指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码。
如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?
如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息? 发送方确认模式 将信道设置成 confirm 模式(发送确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认(ack)给生产者(包含消息唯一 ID)。 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。 接收方确认机制 消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。
如何保证RabbitMQ消息的可靠传输(消息不丢失)?
如何保证RabbitMQ消息的可靠传输? 生产者丢失消息 提供了发布确认模式,确保生产者不丢消息; 一旦channel进入confirm模式,所有在该信道上发布的消息都将会被指派一个唯一的ID(从1开始)。 一旦消息被投递到所有匹配的队列之后;MQ就会发送一个ACK给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;如果MQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。 MQ丢失数据 一般是队列持久化和消息持久化。 消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。 消费者丢失消息 消费者丢数据一般是因为采用了自动确认消息模式,改为手动确认消息即可。
如何保证高可用的?RabbitMQ 的集群
如何保证高可用的?RabbitMQ 的集群 RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 单机模式 就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式。 普通集群模式 意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。 你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。 镜像集群模式 这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。 这样的好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。
死信队列
死信队列 死信,顾名思义就是无法被消费的消息,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。 死信的来源 消息 TTL 过期 队列达到最大长度(队列满了,无法再添加数据到 mq 中) 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false 应用场景: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
RabbitMQ如何保证消息的顺序性?
RabbitMQ如何保证消息的顺序性? 顺序会错乱的俩场景: RabbitMQ:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。  解决方案: 拆分多个 queue,每个 queue 一个 consumer。
Linux
Docker
什么是Docker?
docker是一个用Go语言实现的开源应用容器引擎。
Elasticsearch
Redisson数据网格与分布式锁
JUC
JUC是什么?
java.util.concurrent在并发编程中使用的工具类

什么是并发?什么是并行?
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点 例子:小米9今天上午10点,限量抢购 春运抢票 电商秒杀... 并行:多项工作一起执行,之后再汇总 例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
Lock接口
复习synchronized
多线程编程模板上
线程 操作 资源类
高内聚低耦合
卖票
package org.example; /** * @author ljh * @date 2022.04.21 09:53 * <p> * description: 三个售票员卖出100张票,每个售票员能卖60张 */ public class TicketDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "AA").start(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "BB").start(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "CC").start(); } } class Ticket { /** * 100 张票 */ int number = 100; public synchronized void sale() { while (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出" + (number--) + "号票,还剩 " + number + "张票"); } } }
Lock
什么是可重入锁?
当线程获取某个锁后,还可以继续获取它,可以递归调用,而不会发生死锁;
不可重入锁
与可重入相反,获取锁后不能重复获取,否则会死锁(自己锁自己)。
Lock接口的实现ReentrantLock可重入锁
如何使用
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }
卖票
package org.example.ticket_lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author ljh * @date 2022.04.21 09:53 * <p> * description: 三个售票员卖出100张票,每个售票员能卖60张 */ public class TicketDemo { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "AA").start(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "BB").start(); new Thread(() -> { for (int i = 0; i < 60; i++) { ticket.sale(); } }, "CC").start(); } } class Ticket { /** * 100 张票 */ int number = 100; private final Lock lock = new ReentrantLock(); public void sale() { // 上锁 lock.lock(); try { while (number > 0) { System.out.println(Thread.currentThread().getName() + "卖出" + (number--) + "号票,还剩 " + number + "张票"); } } finally { // 解锁 lock.unlock(); } } }
sync和Lock的区别?
sync没有获取到锁的话会一直等待
ReentrantLock没有获取到锁会返回失败,不会一直等待锁的释放
线程间通信
面试题
两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z, 要求用线程间通信
线程间通信:1、生产者+消费者;2、通知等待唤醒机制
多线程编程模板中
1、判断
2、干活
3、通知
synchronized实现
代码
package org.example.notify_wait; /** * @author ljh * @date 2022.04.21 11:17 * <p> * description: */ public class NotifyWaitDemo { public static void main(String[] args) { ShareDataOne shareDataOne = new ShareDataOne(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.incre(); } catch (Exception e) { e.printStackTrace(); } } }, "AA").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.decre(); } catch (Exception e) { e.printStackTrace(); } } }, "BB").start(); } } /** * 资源类 * 现在两个线程: 一个线程对变量加1,一个线程对变量减1,例如 0 1 0 1 0 1, 来10轮 */ class ShareDataOne { private int number = 0; public synchronized void incre() throws Exception { // 判断 while (number != 0) { this.wait(); } // 干活 ++number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 this.notifyAll(); } public synchronized void decre() throws Exception{ // 判断 while (number != 1) { this.wait(); } // 干活 --number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 this.notifyAll(); } }
多线程编程模板下
注意多线程之间的虚假唤醒
当有四个线程时候,用if判断会出问题,所以我们应该使用while循环判断
java8新版实现
对标实现

Condition

代码
package org.example.notify_wait; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author ljh * @date 2022.04.21 11:17 * <p> * description: */ public class NotifyWaitDemo { public static void main(String[] args) { ShareDataOne shareDataOne = new ShareDataOne(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.incre(); } catch (Exception e) { e.printStackTrace(); } } }, "AA").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.decre(); } catch (Exception e) { e.printStackTrace(); } } }, "BB").start(); } } /** * 资源类 * 现在两个线程: 一个线程对变量加1,一个线程对变量减1,例如 0 1 0 1 0 1, 来10轮 */ class ShareDataOne { private int number = 0; private Lock lock = new ReentrantLock(); Condition cd = lock.newCondition(); public void incre() throws Exception { lock.lock(); try { // 判断 while (number != 0) { cd.await(); } // 干活 ++number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 cd.signalAll(); } finally { lock.unlock(); } } public void decre() throws Exception { lock.lock(); try { // 判断 while (number != 1) { cd.await(); } // 干活 --number; System.out.println(Thread.currentThread().getName() + "\t" + number); // 通知 cd.signalAll(); } finally { lock.unlock(); } } }
线程间定制化调用通信
判断、干活、通知
1、有顺序通知,需要有标识位 2、有一个锁Lock,3把钥匙Condition 3、判断标志位 4、输出线程名+第几次+第几轮 5、修改标志位,通知下一个
例子
多线程之间按顺序调用,实现A->B->C AA打印5次,BB打印10次,CC打印15次 接着 AA打印5次,BB打印10次,CC打印15次 来10轮
代码
package org.example.notify_wait; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author ljh * @date 2022.04.21 12:47 * <p> * description: */ public class ThreadOrderAccess { public static void main(String[] args) { ShareResource shareDataOne = new ShareResource(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.print5(i); } catch (Exception e) { e.printStackTrace(); } } }, "AA线程").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.print10(i); } catch (Exception e) { e.printStackTrace(); } } }, "BB线程").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { try { shareDataOne.print15(i); } catch (Exception e) { e.printStackTrace(); } } }, "CC线程").start(); } } /** * 多线程之间按顺序调用,实现A->B->C * AA打印5次,BB打印10次,CC打印15次 * 接着 * AA打印5次,BB打印10次,CC打印15次 * 来10轮 */ class ShareResource { /** * 1-A线程 * 2-B线程 * 3-C线程 */ private int number = 1; private Lock lock = new ReentrantLock(); Condition c1 = lock.newCondition(); Condition c2 = lock.newCondition(); Condition c3 = lock.newCondition(); public void print5(int total) throws Exception { lock.lock(); try { // 判断 while (number != 1) { c1.await(); } // 干活 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " 第" + total + "轮第" + i + "次"); } // 通知 number = 2; c2.signal(); } finally { lock.unlock(); } } public void print10(int total) throws Exception { lock.lock(); try { // 判断 while (number != 2) { c2.await(); } // 干活 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " 第" + total + "轮第" + i + "次"); } // 通知 number = 3; c3.signal(); } finally { lock.unlock(); } } public void print15(int total) throws Exception { lock.lock(); try { // 判断 while (number != 3) { c3.await(); } // 干活 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " 第" + total + "轮第" + i + "次"); } // 通知 number = 1; c1.signal(); } finally { lock.unlock(); } } }
多线程锁
synchronized实现同步的基础:Java中的每一个对象都可以作为锁。 具体表现为以下3种形式。 对于普通同步方法,锁是当前实例对象。 对于静态同步方法,锁是当前类的Class对象。 对于同步方法块,锁是Synchonized括号里配置的对象。
Callable接口
面试题:获取多线程的方法有几种?
传统的是继承thread类和实现runnable接口,java5以后又有实现callable接口和java的线程池获得
runnable和callable有什么区别?
创建新类MyThread实现runnable接口 class MyThread implements Runnable{ @Override public void run() { } } 新类MyThread2实现callable接口 class MyThread2 implements Callable{ @Override public Integer call() throws Exception { return 200; } } 面试题:callable接口与runnable接口的区别? 是否有返回值 是否抛异常 落地方法不一样,一个是run,一个是call
JUC强大辅助类
CountDownLatch 减少计数
原理
CountDownLatch主要有两个方法: await:当线程调用await方法时,线程会阻塞。 countDown:其他线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器变为0时,因await方法阻塞的线程会被唤醒,继续执行。
代码
package org.example.count_down; import java.util.concurrent.CountDownLatch; /** * @author ljh * @date 2022.04.22 10:18 * <p> * description: */ public class CountDownDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch cd = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "号同学离开教室"); cd.countDown(); }, String.valueOf(i)).start(); } cd.await(); System.out.println(Thread.currentThread().getName() + "班长最后锁门离开"); } }
CyclicBarrier 循环栅栏
原理
CyclicBarrier 的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是: 让一组线程到达一个屏障(也可以叫同步点)时被阻塞, 直到最后一个线程到达屏障时,屏障才会开门,所有 被屏障拦截的线程才会继续干活。 线程进入屏障通过CyclicBarrier的await()方法。
代码
package org.example.cyclicbarrier; import java.util.concurrent.CyclicBarrier; /** * @author ljh * @date 2022.04.22 10:30 * <p> * description: */ public class CyclicBarrierDemo { public static void main(String[] args) throws Exception { CyclicBarrier cb = new CyclicBarrier(7, () -> { System.out.println("集齐七颗龙珠,召唤神龙"); }); for (int i = 1; i <= 7; i++) { new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "颗龙珠被收集"); cb.await(); } catch (Exception e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } }
Semaphore 信号灯
原理
在信号量上我们定义两种操作: acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1), 要么一直等下去,直到有线程释放信号量,或超时。 release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。 信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。
代码
package org.example.semaphore; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * @author ljh * @date 2022.04.22 10:49 * <p> * description: */ public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3); for (int i = 1; i <= 6; i++) { new Thread(() -> { try { // 获得 semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "停入停车位"); TimeUnit.SECONDS.sleep(3); System.out.println(Thread.currentThread().getName() + "离开停车位~~~"); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放 semaphore.release(); } }, String.valueOf(i)).start(); } } }
BlockQueue 阻塞队列
种类分析
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
DelayQueue:使用优先级队列实现的延迟无界阻塞队列。
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
LinkedTransferQueue:由链表组成的无界阻塞队列。
LinkedBlockingDeque:由链表组成的双向阻塞队列。
阻塞队列的用处
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起 为什么需要BlockingQueue 好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了。  线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素 当队列是空的,从队列中获取元素的操作将会被阻塞 当队列是满的,从队列中添加元素的操作将会被阻塞
线程池
线程池的创建方式
可缓存线程池 Executors.newCachedThreadPool() 可定长度,限制最大线程数 Executors.newFixedThreadPool() 可定时 Executors.newScheduledThreadPool() 单例 Executors.newSingleThreadExecutor() 底层都是基于ThreadPoolExecutor构造函数封装; 答:线程池获得方式可以通过jdk原生的Executors来进行获得,提供了四种api(可缓存、可定长度、可定时、单例),但阿里巴巴规范手册不建议使用,因为他们都是基于ThreadPoolExecutor来实现的,它传递的是一个无界的缓存队列,可能会导致线程池溢出。
项目中哪些地方使用到了线程池?
异步发短信、发邮件(小项目); 大项目可以使用MQ
线程池拒绝策略
为什么用线程池?解释下线程池参数?
为什么用线程池? 降低资源消耗; 提高线程利用率,降低创建和销毁线程的消耗。 提高响应速度; 任务来了,直接有线程可用可执行,而不是先创建线程,再执行。 提高线程的可管理性; 线程是稀缺资源,使用线程池可以统一分配调优监控。 线程池的七大参数 corePoolSize:核心线程数;也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是一种常驻线程。 maxinumPoolSize:最大线程数;线程池能容纳的最大线程数,当线程池中的线程达到最大时,此时添加任务将会采用拒绝策略,默认的拒绝策略是抛出一个运行时错误(RejectedExecutionException)。值得一提的是,当初始化时用的工作队列为LinkedBlockingDeque时,这个值将无效。 keepAliveTime:存活时间;表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepAliveTime 来设置空闲时间。 unit:keepAliveTime的单位。 workQueue:任务队列;常用有三种队列,即SynchronousQueue,LinkedBlockingDeque(无界队列),ArrayBlockingQueue(有界队列)。 ThreadFactory:线程工厂;ThreadFactory是一个接口,用来创建worker。通过线程工厂可以对线程的一些属性进行定制。默认直接新建线程。 拒绝策略: AbortPolicy:直接抛出异常,默认策略。 CallerRunsPolicy:用调用者所在的线程来执行任务。 DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务; DiscardPolicy:直接丢弃任务。
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不 会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中 尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。 如果允许任务丢失,这是最好的一种策略。
线程池底层工作原理
在创建了线程池后,线程池中的线程数为零。 当调用execute()方法添加一个请求任务时,线程池会做出如下判断: 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务; 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列; 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务; 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。 当一个线程完成任务时,它会从队列中取下一个任务来执行。 当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断: 如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。 所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。
JVM
Maven
设计模式
单例设计模式
饿汉式(静态常量)
package com.example.single_1; /** * @author ljh * @date 2022.03.21 13:15 * <p> * description: * 饿汉式(静态常量) */ public class Single { // 构造器私有化,外部不能直接new private Single() { } // 类的内部创建的对象 public static final Single SINGLE = new Single(); // 向外暴露一个静态的公共方法 public static Single getInstance() { return SINGLE; } }
饿汉式(静态代码块)
package com.example.single_2; /** * @author ljh * @date 2022.03.21 13:21 * <p> * description: * 饿汉式(静态代码块) */ public class Single { private Single() { } private static Single single; static { single = new Single(); } public static Single getInstance() { return single; } }
懒汉式(同步方法):不推荐使用:效率太低
package com.example.single_3; /** * @author ljh * @date 2022.03.21 13:27 * <p> * description: * 懒汉式(同步方法) */ public class Single { private Single() { } private static Single single; public static synchronized Single getInstance() { if (null == single) { single = new Single(); return single; } return single; } }
懒汉式(双重检查)
package com.example.single_4; /** * @author ljh * @date 2022.03.21 13:33 * <p> * description: * 懒汉式(双重检查) */ public class Single { private Single() { } private static Single single; public static Single getInstance() { if (null == single) { synchronized (Single.class) { if (null == single) { single = new Single(); } } } return single; } }
静态内部类
package com.example.single_5; /** * @author ljh * @date 2022.03.21 13:38 * <p> * description: * 静态内部类 */ public class Single { private Single() { } private static class SingleInstance { public static final Single SINGLE = new Single(); } public static Single getInstance() { return SingleInstance.SINGLE; } }
枚举
package com.example.single_6; /** * @author ljh * @date 2022.03.21 13:43 * <p> * description: * 枚举 */ public enum Single { /** * 属性 */ ISNTANCE; }
工厂模式
数据机构和算法
排序算法
冒泡排序
基本思想
比较相邻两个数的大小,如果前一个数大于后一个数,就交换,以此类推。
代码
/** * <p> * 功能描述: 冒泡排序 * </p> * * @param arr 数据 int[] arr = {23, 1, 56, 3, -1}; */ public static void bubbleSort(int[] arr) { int arrLength = arr.length; boolean flag = false; for (int i = 0; i < arrLength - 1; i++) { for (int j = 0; j < arrLength - 1 - i; j++) { if (arr[j] > arr[j + 1]) { flag = true; int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } System.out.println("第" + (i + 1) + "趟排序" + " " + Arrays.toString(arr)); } }
选择排序
基本思想
如果有N个元素需要排序,那么首先从N个元素中找到最小的那个元素与第0位置上的元素交换(说明一点,如果没有比原本在第0位置上的元素小的就不用交换了,后面的同样是),然后再从剩下的N-1个元素中找到最小的元素与第1位置上的元素交换
代码
/** * <p> * 功能描述:选择排序 * </p> * * @param arr 要排序的数组 int[] arr = {23, 1, 56, 3, -1}; */ public static void selectSort(int[] arr) { for (int i = 0; i < arr.length - 1; i++) { // 假定第一个是最小数,对应的下标 int minIndex = i; // 假定第一个是最小数 int min = arr[i]; for (int j = i + 1; j < arr.length; j++) { if (min > arr[j]) { min = arr[j]; minIndex = j; } } if (minIndex != i) { // 交换 arr[minIndex] = arr[i]; arr[i] = min; } System.out.println("第" + (i + 1) + "趟排序" + " " + Arrays.toString(arr)); } }
插入排序
基本思想
把n个待排序的元素看成一个有序表和无序表,开始时,有序表只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它插入到有序表中的适当位置,使之成为新的有序表。
代码
/** * 插入排序 * * @param arr 排序的数组 int[] arr = {23, 1, 56, 3, -1}; */ public static void insertSort(int[] arr) { int insertVal, insertIndex; for (int i = 1; i < arr.length; i++) { // 定义待插入的数 insertVal = arr[i]; // 待插入的数的索引 insertIndex = i - 1; // 第一个条件保证在给insertVal找插入位置不越界,第二个条件如果满足,还没有找到插入位置 while (insertIndex >= 0 && insertVal < arr[insertIndex]) { // 需要将arr[insertIndex] 后移 arr[insertIndex + 1] = arr[insertIndex]; insertIndex--; } // 当退出循环时,说明插入的位置找到, insertIndex + 1 arr[insertIndex + 1] = insertVal; System.out.println("第" + (i) + "趟排序" + " " + Arrays.toString(arr)); }
希尔排序
基本思想
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
代码
package com.example.sort_04; import java.util.Arrays; /** * @author ljh * @date 2022.03.22 13:22 * <p> * description: * 希尔排序 */ public class ShellSort { public static void main(String[] args) { int[] arr = {8, 9, 1, 7, 2, 3, 5, 4, 6, 0}; // 对这个数据进行分组 for (int group = arr.length / 2; group > 0; group /= 2) { for (int i = group; i < arr.length; i++) { int j = i; int temp = arr[j]; if (arr[j] < arr[j - group]) { while (j - group >= 0 && temp < arr[j - group]) { // 移动 arr[j] = arr[j - group]; j -= group; } // 退出while循环后,就给temp找到插入位置 arr[j] = temp; } } } System.out.println("希尔排序" + " " + Arrays.toString(arr)); } }
快速排序
基本思想
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列
代码
package com.example.sort_05; import java.util.Arrays; /** * @author ljh * @date 2022.03.22 15:01 * <p> * description: * 快速排序 */ public class QuickSort { public static void main(String[] args) { int[] arr = {3, -1, 2, 1, 0, 10, 222, -110, -3}; quickSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } public static void quickSort(int[] arr, int left, int right) { int l = left; // 左下标 int r = right; // 右下标 // 获取中间值 int pivot = arr[(left + right) / 2]; // while循环的目的是让比pivot 值小放到左边, 比pivot 值大的放到右边 while (l < r) { // 在pivot左边一直找,找到大于等于pivot的值,才退出 while (arr[l] < pivot) { l += 1; } // 在pivot右边一直找,找到小于等于pivot的值,才退出 while (arr[r] > pivot) { r -= 1; } // 如果l >= r说明左边全是小于pivot的,右边是大于pivot的 if (l >= r) { break; } int temp = arr[l]; arr[l] = arr[r]; arr[r] = temp; if (arr[l] == pivot) { r -= 1; } if (arr[r] == pivot) { l += 1; } } // 如果 l == r, 必须l++, r--, 否则为出现栈溢出 if (l == r) { l += 1; r -= 1; } // 向左递归 if (left < r) { quickSort(arr, left, r); } // 向右递归 if (right > l) { quickSort(arr, l, right); } } }
数据结构
数组(Array)
优点: 按照索引查询元素的速度很快; 按照索引遍历数组也很方便。 缺点: 数组的大小在创建后就确定了,无法扩容; 数组只能存储一种类型的数据; 添加、删除元素的操作很耗时间,因为要移动其他元素。
链表(Linked List)
介绍 链表是一种递归的数据结构,它或者为空(null),或者是指向一个结点(node)的引用,该节点还有一个元素和一个指向另一条链表的引用。 单向链表的缺点只能从头到尾依次遍历,双向链表可进可退。 链表中的数据是“链式”的结构存储,因此可以达到内存上非连续的效果,数组必须是连续的一块内存。 链表在插入、删除的时候可以达到 O(1) 的时间复杂度(只需要重新指向引用即可,不需要像数组那样移动其他元素)。 优点 不需要初始化容量; 可以添加任意元素; 插入和删除的时候只需要更新引用。 缺点 含有大量的引用,占用的内存空间大; 查找元素需要遍历整个链表,耗时。
树(Tree)
二叉树
满二叉树 如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。 完全二叉树 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。 
平衡二叉树(AVL树)
B树
B+树
栈(Stack)
介绍 像抢的弹夹一样,先进后出。 栈按照“后进先出”、“先进后出”的原则来存储数据,先插入的数据被压入栈底,后插入的数据在栈顶,读出数据的时候,从栈顶开始依次读出。 
队列(Queue)
介绍 队列就好像一段水管一样,两端都是开口的,水从一端进去,然后从另外一端出来。先进去的水先出来,后进去的水后出来。 和水管有些不同的是,队列会对两端进行定义,一端叫队头,另外一端就叫队尾。队头只允许删除操作(出队),队尾只允许插入操作(入队)。 
哈希表(Hash)
介绍 哈希表(Hash Table),也叫散列表,是一种可以通过关键码值(key-value)直接访问的数据结构,它最大的特点就是可以快速实现查找、插入和删除。 哈希函数在哈希表中起着⾮常关键的作⽤,它可以把任意长度的输入变换成固定长度的输出,该输出就是哈希值。哈希函数使得一个数据序列的访问过程变得更加迅速有效,通过哈希函数,数据元素能够被很快的进行定位。 若关键字为 k,则其值存放在 hash(k) 的存储位置上。由此,不需要遍历就可以直接取得 k 对应的值。 对于任意两个不同的数据块,其哈希值相同的可能性极小,也就是说,对于一个给定的数据块,找到和它哈希值相同的数据块极为困难。再者,对于一个数据块,哪怕只改动它的一个比特位,其哈希值的改动也会非常的大——这正是 Hash 存在的价值! 尽管可能性极小,但仍然会发生,如果哈希冲突了,Java 的 HashMap 会在数组的同一个位置上增加链表,如果链表的长度大于 8,将会转化成红黑树进行处理——这就是所谓的拉链法(数组+链表)。
底层:数组+链表 或 数组 + 树
堆(Heap)
堆可以被看做是一棵树的数组对象,具有以下特点: 堆中某个节点的值总是不大于或不小于其父节点的值; 堆总是一棵完全二叉树。 将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
图(Graph)
锁
java中的锁有哪些?
乐观锁、悲观锁
浮动主题