💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
## 转发(Forward)和重定向(Redirect)的区别 **转发是服务器行为,重定向是客户端行为。** **转发(Forword)** 通过RequestDispatcher对象的forward(HttpServletRequest request,HttpServletResponse response) 方法实现的。RequestDispatcher 可以通过HttpServletRequest 的 getRequestDispatcher() 方法获得。例如下面的代码就是跳转到 login_success.jsp 页面。 **重定向(Redirect)** 是利用服务器返回的状态吗来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状 态码。服务器通过HttpServletRequestResponse的setStatus(int status)方法设置状态码。如果服务器返回301或者 302,则浏览器会到新的网址重新请求该资源。 1. **从地址栏显示来说**: forward是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来, 然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的**地址栏还是原来的地址**. redirect是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以**地址栏显示的是新的URL**. 2. **从数据共享来说**: forward:转发页面和转发到的页面可以共享request里面的数据. redirect:不能共享数据. 3. **从运用地方来说**: forward:一般用于用户**登陆**的时候,根据角色转发到相应的模块. redirect:一般用于用户**注销**登 陆时返回主页面和跳转到其它的网站等 4. **从效率来说**: forward:高. redirect:低. ## TCP/IP五层协议 物理层、数据链路层、网络层、传输层、应用层 ## TCP、UDP 协议的区别 ![](https://box.kancloud.cn/fc4d9463833cb2c88eb0394696238436_1128x293.png) **UDP 在传送数据之前不需要先建立连接**,远地主机在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可 靠交付,但在某些情况下 UDP 确是一种最有效的工作方式(一般用于即时通信),比如: **QQ 语音、 QQ 视频 、直 播**等等 **TCP 提供面向连接的服务**。在传送数据之前必须先建立连接,数据传送结束后要释放连接。 TCP 不提供广播或多播 服务。由于 TCP 要提供可靠的,面向连接的运输服务(TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立 连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资 源),这一难以避免增加了许多开销,如确认,流量控制,计时器以及连接管理等。这不仅使协议数据单元的首部增 大很多,还要占用许多处理机资源。**TCP 一般用于文件传输、发送和接收邮件、远程登录等场景**。 ## HTTP长连接、短连接 **在HTTP/1.0中默认使用短连接**。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中 断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像 文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。 而**从HTTP/1.1起,默认使用长连接,用以保持连接特性**。使用长连接的HTTP协议,会在响应头加入这行代码: ``` Connection:keep-alive ``` 在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客 户端再次访问这个服务器时,会继续使用这一条已经建立的连接。**Keep-Alive不会永久保持连接,它有一个保持时 间**,可以在不同的服务器软件(如Apache)中设定这个时间。**实现长连接需要客户端和服务端都支持长连接**。 HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。 ## 在浏览器中输入url地址到显示主页的过程 **总体来说分为以下几个过程**: 1. DNS解析 (浏览器查找域名的IP地址) 2. TCP连接 3. 发送HTTP请求 (浏览器向WEB服务器发送请求) 4. 服务器处理请求并返回HTTP报文 5. 浏览器解析渲染页面 6. 连接结束 ## TCP 三次握手和四次挥手 **三次握手** 三次握手的目的是建立可靠的通信信道,说到通讯,简单来说就是数据的发送与接收,而三次握手最主要的目的就是 **双方确认自己与对方的发送与接收是正常**的。 客户端–发送带有 SYN 标志的数据包–一次握手–服务端 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端 客户端–发送带有带有 ACK 标志的数据包–三次握手–服务端 **四次挥手** 断开一个 TCP 连接则需要“四次挥手”: 任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送 的时候,则发出连接释放通知,对方确认后就完全关闭了TCP连接。 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号 服务器-关闭与客户端的连接,发送一个FIN给客户端 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1 ## 为什么要使用索引 1. 通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 2. 可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。 3. 帮助服务器避免排序和临时表 4. 将随机IO变为顺序IO 5. 可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。 ## 索引这么多优点,为什么不对表中的每一个列创建一个索引呢? 1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。 2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索 引,那么需要的空间就会更大。 3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。 ## 索引是如何提高查询速度的? 将无序的数据变成相对有序的数据(就像查目录一样) ## Mysql索引主要使用的哪两种数据结构? 1. 哈希索引:对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可 以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。 2. BTree索引:Mysql的BTree索引使用的是B树中的B+Tree。但对于主要的两种存储引擎(MyISAM和InnoDB) 的实现方式是不同的。 ## 什么是覆盖索引? 如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称 之为“覆盖索引”。我们知道在InnoDB存储引 擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就 会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! ## MyISAM与InnoDB MyISAM更适合读密集的表,而InnoDB更适合写密集的表。 在数据库做主从分离的情况下,经常选择MyISAM作 为主库的存储引擎。 一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高 时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘 的空间占用),而且不需要支持事务时,MyISAM是最好的选择。 **主要区别:** 1. MyISAM是非事务安全型的,而InnoDB是事务安全型的。 2. MyISAM锁的粒度是表级,而InnoDB支持行级锁定。 3. MyISAM支持全文类型索引,而InnoDB不支持全文索引。 4. MyISAM相对简单,所以在效率上要优于InnoDB,小型应用可以考虑使用MyISAM。 5. MyISAM表是保存成文件的形式,在跨平台的数据转移中使用MyISAM存储会省去不少的麻烦。 6. InnoDB表比MyISAM表更安全,可以在保证数据不会丢失的情况下,切换非事务表到事务表(alter table tablename type=innodb)。 7. count运算上的区别: 因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好 的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存 8. 是否支持事务和崩溃后的安全恢复: MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型 更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务 (commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 9. 是否支持外键: MyISAM不支持,而InnoDB支持。 **应用场景:** 1. MyISAM管理非事务表。它提供高速存储和检索,以及全文搜索能力。如果应用中需要执行大量的SELECT查询,那么MyISAM是更好的选择。 2. InnoDB用于事务处理应用程序,具有众多特性,包括ACID事务支持。如果应用中需要执行大量的INSERT或 UPDATE操作,则应该使用InnoDB,这样可以提高多用户并发操作的性能。 ## 当MySQL单表记录数过大时,一些常见的优化措施 当MySQL单表记录数过大时,数据库的CRUD性能会明显下 降 1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时 候,我们可以控制在一个月的范围内。; 2. 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读; 3. 垂直分区: 根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信 息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。简单来说垂直拆分是指数据表列的拆分, 把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。 垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简 化表的结构,易于维护。垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过 在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂; 4. 水平分区: 保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达 到了分布式的目的。 水平拆分可以支撑非常大的数据量。 水平拆分是指数据表行的拆分,表的行数超过200万 行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多 个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。 水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的 数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。水平拆分能 够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。 ## 进程与线程的区别是什么? 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同 的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工 作时,负担要比进程小得多,也正因为如此,**线程也被称为轻量级进程**。另外,也正是因为共享资源,所以线程中执 行时一般都要进行同步和互斥。总的来说,进程和线程的主要差别在于它们是**不同的操作系统资源管理方式**。 ## 进程间的几种通信方式 1. 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。 进程的血缘关系通常指父子进程关系。管道分为pipe(无名管道)和fifo(命名管道)两种,有名管道也是半双 工的通信方式,但是它允许无亲缘关系进程间通信。 2. 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁 机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同 线程之间的同步手段。 3. 消息队列(message queue):消息队列是由消息组成的链表,存放在内核中 并由消息队列标识符标识。消 息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。消息队列与管道通信 相比,其优势是对每个消息指定特定的消息类型,接收的时候不需要按照队列次序,而是可以根据自定义条件 接收特定类型的消息。 4. 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。 5. 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进 程创建,但多个进程都可以访问,**共享内存是最快的IPC方式**,它是针对其他进程间的通信方式运行效率低而专 门设计的。**它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信**。 6. 套接字(socket):socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程) 系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网 络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。 ## 线程间的几种通信方式 线程间通信的主要目的是用于**线程同步**,所以线程没有像进程通信中用于**数据交换的**通信机制。 1. 锁机制 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。 读写锁:允许多个线程同时读共享数据,而对写操作互斥。 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行 的。条件变量始终与互斥锁一起使用。 2. 信号量机制:包括无名线程信号量与有名线程信号量 3. 信号机制:类似于进程间的信号处理。 ## 单例模式 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统 开销; 由于 new 操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。 **单线程:** * 饿汉式 ``` public class HungryMode { private static HungryMode sHungryMode = new HungryMode(); private HungryMode() { } public static HungryMode getInstance(){ return sHungryMode; } } ``` * 懒汉式 ``` public class LazyMode { private static LazyMode sLazyMode; private LazyMode() { } public static LazyMode getInstance(){ if (sLazyMode == null) { sLazyMode = new LazyMode(); } return sLazyMode; } } ``` **多线程:** * 双重校验机制 ``` /** * 多线程的单例模式,使用双重校验机制 */ public class DoubleCheckMode { private volatile static DoubleCheckMode sDoubleCheckMode ; public DoubleCheckMode() { } public static DoubleCheckMode getInstance() { if (sDoubleCheckMode == null) synchronized (DoubleCheckMode.class) { if (sDoubleCheckMode == null) { sDoubleCheckMode = new DoubleCheckMode(); } } return sDoubleCheckMode; } } ``` * 静态内部类 ``` /** * 静态内部类的方式实现单例,可以保证多线程的对象唯一性,还有提升性能,不用同步锁机制 */ public class InnerStaticMode { private static class SingleTonHolder { public static InnerStaticMode sInnerStaticMode = new InnerStaticMode(); } public static InnerStaticMode getInstance(){ return SingleTonHolder.sInnerStaticMode; } } ``` * 枚举 枚举实现单例,不推荐在Android平台使用,因为内存消耗会其他方式多一些,Android官方也不推荐枚举,Android平台推荐双重校验或者静态内部类单例,现在的Android开发环境jdk一般都大于1.5了。所以volatile的问题不必担心。Java平台开发的Effective Java一书中推荐使用枚举实现单例,可以保证效率,而且还能解决反序列化创建新对象的问题。 ``` /** * 利用枚举的方式实现单例,Android不推荐 */ public enum EnumMode { INSTANCE; private int id; public int getId() { return id; } public void setId(int id) { this.id = id; } } ``` ## Spring、SpringMVC和Springboot的区别 Spring 最初利用“工厂模式”(DI,IoC)和“代理模式”(AOP)解耦应用组件。大家觉得挺好用,于是按照这种模式搞了一个 MVC框架(一些用Spring 解耦的组件),用开发 web 应用( SpringMVC )。然后发现每次开发都写很多样板代码,为了简化工作流程,于是开发出了一些“懒人整合包”(starter),这套就是 Spring Boot。 所以,用最简练的语言概括就是: * Spring 是一个“引擎”; * Spring MVC 是基于Spring的一个 MVC 框架; * Spring Boot 是基于Spring4的条件注册的一套快速开发整合包。 **SpringBoot和SpringCloud区别** * Spring boot 是 Spring 的一套快速配置脚手架,可以基于spring boot 快速开发单个微服务; Spring Cloud是一个基于Spring Boot实现的云应用开发工具 * Spring boot可以离开Spring Cloud独立使用开发项目,但是Spring Cloud离不开Spring boot,属于依赖的关系 ## Spring的bean 在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,**bean 就 是由 IOC 容器初始化、装配及管理的对象**,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。 **Spring中的bean默认都是单例的**,这些单例Bean在多线程程序下如何保证线程安全呢? 例如对于Web应用来说, Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例 的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? Spring的单例是基于BeanFactory也就是Spring 容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。 ## Spring AOP IOC 实现原理 **IOC: 控制反转也叫依赖注入**。**IOC利用java反射机制,AOP利用代理模式**。IOC 概念看似很抽象,但是很容易理解。 说简单点就是将对象交给容器管理,你只需要在spring配置文件中配置对应的bean以及设置相关的属性,让spring 容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始 化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。 **AOP: 面向切面编程**。(Aspect-Oriented Programming) 。AOP可以说是对OOP的补充和完善。OOP引入封装、 继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。实现AOP的技术,主要分为两大类: 一是采用**动态代理技术**,**利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行**;二是采用**静态织入 的方式**,**引入特定的语法创建“方面”**,从而使得编译器可以在编译期间织入有关“方面”的代码,属于静态代理。 ## Spring MVC原理 客户端发送请求-> 前端控制器 DispatcherServlet 接受客户端请求 -> 找到处理器映射 HandlerMapping 解析请求对 应的 Handler-> HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑 -> 处理 器返回一个模型视图 ModelAndView -> 视图解析器进行解析 -> 返回一个视图对象->前端控制器 DispatcherServlet 渲染数据(Model)->将得到视图对象返回给用户 ## Spring事务隔离级别 spring(数据库)事务隔离级别分为四种(级别递减): Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别 1. Serializable (串行化):最严格的级别,事务串行执行,资源消耗最大; 2. REPEATABLE READ(重复读) :保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但不能避免“幻读”,但是带来了更多的性能损失。 3. READ COMMITTED (提交读):大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”,但不能避免“幻读”和“不可重复读取”。该级别适用于大多数系统。 4. Read Uncommitted(未提交读) :事务中的修改,即使没有提交,其他事务也可以看得到,会导致“脏读”、“幻读”和“不可重复读取”。 **脏读、不可重复读、幻读:** **脏读**:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。 也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。 **不可重复读**:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。 也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。 **幻读**:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。 也就是说,当前事务读第一次取到的数据比后来读取到数据条目少。 ## Nginx Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 Nginx 主要提供反向代 理、负载均衡、动静分离(静态资源服务)等服务。 **反向代理** 正向代理:某些情况下,代理我们用户去访问服务器,需要用户手动的设置代理服务器的ip和端口号。正向代 理比较常见的一个例子就是 VPN了。 反向代理: 是用来代理服务器的,代理我们要访问的目标服务器。代理服务器接受请求,然后将请求转发给内 部网络的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。 所以,简单的理解,就是正向代理是为客户端做代理,代替客户端去访问服务器,而反向代理是为服务器做代理,代 替服务器接受客户端请求。 **负载均衡** 在**高并发**情况下需要使用,其原理就是将并发请求分摊到多个服务器执行,减轻每台服务器的压力,多台服务器(集 群)共同完成工作任务,从而提高了数据的吞吐量。 Nginx支持的weight轮询(默认)、ip_hash、fair、url_hash这四种负载均衡调度算法,感兴趣的可以自行查阅。 负载均衡相比于反向代理更侧重的时将请求分担到多台服务器上去,所以谈论负载均衡只有在提供某服务的服务器大 于两台时才有意义。 **动静分离** 动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以 后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。 **优点** 1. 高并发、高性能(这是其他web服务器不具有的) 2. 可扩展性好(模块化设计,第三方插件生态圈丰富) 3. 高可靠性(可以在服务器行持续不间断的运行数年) 4. 热部署(这个功能对于 Nginx 来说特别重要,热部署指可以在不停止 Nginx服务的情况下升级 Nginx) 5. BSD许可证(意味着我们可以将源代码下载下来进行修改然后使用自己的版本) **主要组成部分** * Nginx 二进制可执行文件:由各模块源码编译出一个文件 * Nginx.conf 配置文件:控制Nginx 行为 * acess.log 访问日志: 记录每一条HTTP请求信息 * error.log 错误日志:定位问题 ## 列举几种消息队列 * ActiveMQ * RabbitMQ 微秒级,延迟是最低的 * RocketMQ 分布式架构 * Kafaka 分布式架构 ## Arraylist 与 LinkedList 有什么不同 1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全; 2. 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(注意双向 链表和双向循环链表的区别:); 3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素 位置的影响。 比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种 情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时 间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向 前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是 近似 O(1)而数组为近似 O(n)。 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通 过元素的序号快速获取元素对象(对应于get(int index) 方法)。 5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空 间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数 据)。 ## 红黑树 红黑树属于(自)平衡二叉树,红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 1. 每个节点非红即黑; 2. 根节点总是黑色的; 3. 每个叶子节点都是黑色的空节点(NIL节点); 4. 如果节点是红色的,则它的子节点必须是黑色的(反之不一定); 5. 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度) ## Object类的常见方法总结 ``` public final native Class<?> getClass()//native方法,用于返回当前运行时对象的Class对象,使用了 final关键字修饰,故不允许子类重写。 public native int hashCode() //native方法,用于返回对象的哈希码,主要使用在哈希表中,比如JDK中的 HashMap。 public boolean equals(Object obj)//用于比较2个对象的内存地址是否相等,String类对该方法进行了重写用 户比较字符串的值是否相等。 protected native Object clone() throws CloneNotSupportedException//naitive方法,用于创建并返 回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为true, x.clone().getClass() == x.getClass() 为true。Object本身没有实现Cloneable接口,所以不重写clone 方法并且进行调用的话会发生CloneNotSupportedException异常。 public String toString()//返回类的名字@实例的哈希码的16进制的字符串。建议Object所有的子类都重写这个 方法。 public final native void notify()//native方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监 视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 public final native void notifyAll()//native方法,并且不能重写。跟notify一样,唯一的区别就是会唤 醒在此对象监视器上等待的所有线程,而不是一个线程。 public final native void wait(long timeout) throws InterruptedException//native方法,并且不 能重写。暂停线程的执行。注意:sleep方法没有释放锁,而wait方法释放了锁 。timeout是等待时间。 public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos参 数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上nanos毫秒。 public final void wait() throws InterruptedException//跟之前的2个wait方法一样,只不过该方法一直 等待,没有超时时间这个概念 protected void finalize() throws Throwable { }//实例被垃圾回收器回收的时候触发的操作 ``` ## hashCode()与equals() 1. 如果两个对象相等,则hashcode一定也是相同的 2. 两个对象相等,对两个对象分别调用equals方法都返回true 3. 两个对象有相同的hashcode值,它们也不一定是相等的 4. 因此,equals方法被覆盖过,则hashCode方法也必须被覆盖 5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何 都不会相等(即使这两个对象指向相同的数据) ## ==与equals **== **: 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。(基本数据类型==比较的是 值,引用数据类型==比较的是内存地址) **equals()** : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况: 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。 情况2:类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等, 则返回true(即,认为这两个对象相等)。 String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方 法比较的是对象的值。 当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就 把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。 ## synchronized 和 ReenTrantLock 的区别 1. 两者都是可重入锁 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时 这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死 锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。 2. synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多 优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就 是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看 它是如何实现的。 3. ReenTrantLock 比 synchronized 增加了一些高级功能 ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也 就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等 待的线程先获得锁。 synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类需要借助于Condition接口与newCondition() 方法。 4. 两者的性能已经相差无几 在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。JDK1.6之后,性能已经不是选择synchronized和 ReenTrantLock的影响因素了! ## 线程池 * 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 * 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。 * 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一的分配,调优和监控。 **Java 主要提供了下面4种线程池** **FixedThreadPool**: 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的 任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线 程空闲时,便处理在任务队列中的任务。 **SingleThreadExecutor**: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会 被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 **CachedThreadPool**: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但 若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新 的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 **ScheduledThreadPoolExecutor**: 主要用来在给定的延迟后运行任务,或者定期执行任务。 ScheduledThreadPoolExecutor又分为:ScheduledThreadPoolExecutor(包含多个线程)和 SingleThreadScheduledExecutor (只包含一个线程)两种。 **各种线程池的适用场景介绍** **FixedThreadPool**: 适用于为了满足资源管理需求,而需要限制当前线程数量的应用场景。它适用于负载比较 重的服务器; **SingleThreadExecutor**: 适用于需要保证顺序地执行各个任务并且在任意时间点,不会有多个线程是活动的 应用场景。 **CachedThreadPool**: 适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器; **ScheduledThreadPoolExecutor**: 适用于需要多个后台执行周期任务,同时为了满足资源管理需求而需要限 制后台线程的数量的应用场景, **SingleThreadScheduledExecutor**: 适用于需要单个后台线程执行周期任务,同时保证顺序地执行各个任务 的应用场景。 **创建的线程池的方式** 使用 Executors 创建:在《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建。 ThreadPoolExecutor的构造函数创建:创建的同时,给 BlockQueue 指定容 量 使用开源类库:pache和guava ## Dubbo **什么是 Dubbo** 一款高性能、轻量级的开源Java RPC 框架,它提供了三大核心能力:面向 接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。简单来说 Dubbo 是一个分布式服务框架, 致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。 RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解 底层网络技术的协议。 **Dubbo的优点** 1. 负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务 2. 服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用 要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如 何调用的。 3. 服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。 4. 服务降级——某个服务挂掉之后调用备用服务 **Dubbo的架构** ![](https://box.kancloud.cn/c24d0eb6281bec8ad12b13bbde6f878b_772x522.png) * Provider: 暴露服务的服务提供方 * Consumer: 调用远程服务的服务消费方 * Registry: 服务注册与发现的注册中心 * Monitor: 统计服务的调用次数和调用时间的监控中心 * Container: 服务运行容器 调用关系说明: 1. 服务容器负责启动,加载,运行服务提供者。 2. 服务提供者在启动时,向注册中心注册自己提供的服务。 3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一 台调用。 6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 重要知识点总结: 注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注 册中心不转发请求,压力较小 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并 以报表展示 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者 服务提供者无状态,任意一台宕掉后,不影响使用 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复 **zookeeper宕机与dubbo直连** 在实际生产中,假如zookeeper注册中心宕掉,一段时间内服务消费方还是能够调用提供方的服务的,实际上它使用 的本地缓存进行通讯,这只是dubbo健壮性的一种提现。注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中 心交互,注册中心不转发请求,压力较小。所以,我们可以完全可以绕过注册中心——采用 dubbo 直连 ,即在服务 消费方配置服务提供方的位置信息。 ## 消息队列 **概念** 我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队 列是**分布式系统中重要的组件**,使用消息队列主要是为了通过异步处理**提高系统性能和削峰、降低系统耦合性**。目前 使用较多的消息队列有ActiveMQ,RabbitMQ,Kafka,RocketMQ。 **优点** 削峰——即通过异步处理,将短时间高并发产生的事 务消息存储在消息队列中,从而削平高峰期的并发事务。 举例:在电子商务一些秒杀、促销活动中,合理使用消息 队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。因为用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中 可能失败。因此使用消息队列进行异步处理之后,需要适当修改业务流程进行配合。 降低耦合——不要认为消息队列只能利用发布-订阅模式工作,只不过**在解耦这个特定业务环境下是使用发布-订阅模式的**。 除了发布-订阅模式,还有点对点订阅模式(一个消息只有一个消费者),我们比较常用的是发布-订阅模式。 另外, 这两种消息模型是 JMS 提供的,AMQP 协议还提供了 5 种消息模型。 **使用消息队列带来的一些问题** * 系统可用性降低: 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或 者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了! * 系统复杂性提高: 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺 序性等等问题! * 一致性问题: 上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万 一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了! **JMS VS AMQP** **JMS**(JAVA Message Service,java消息服务)是java的消息服务,JMS的客户端之间可以通过JMS服务进行异步的 消息传输。JMS(JAVA Message Service,Java消息服务)API是一个消息服务的标准或者说是规范,允许应用程序 组件基于JavaEE平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 ActiveMQ 就是基于 JMS 规范实现的。 JMS两种消息模型:点到点(P2P)模型、发布/订阅(Pub/Sub)模型 **AMQP**,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准 高级消息队列协议(二 进制应用层协议),是应用层协议的一个开放标准,为面向消息的中间件设计,兼容 JMS。基于此协议的客户端与消 息中间件可传递消息,并不受客户端/中间件同产品,不同的开发语言等条件的限制。 RabbitMQ 就是基于 AMQP 协议实现的。 AMQP提供了五种消息模型:①direct exchange;②fanout exchange;③topic change;④headers exchange;⑤system exchange。本质来讲,后四种和JMS的 pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分。 **对比:** * AMQP 为消息定义了线路层(wire-level protocol)的协议,而JMS所定义的是API规范。在 Java 体系中,多个 client均可以通过JMS进行交互,不需要应用修改代码,但是其对跨平台的支持较差。而AMQP天然具有跨平 台、跨语言特性。 * JMS 支持TextMessage、MapMessage 等复杂的消息类型;而 AMQP 仅支持 byte[] 消息类型(复杂的类型可 序列化后发送)。 * 由于Exchange 提供的路由算法,AMQP可以提供多样化的路由方式来传递消息到消息队列,而 JMS 仅支持 队 列 和 主题/订阅 方式两种。 # Spring Boot ## 什么是 Spring Boot Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。 ## 为什么要用 Spring Boot Spring Boot 优点非常多,如: * 独立运行 * 简化配置 * 自动配置 * 无代码生成和XML配置 * 应用监控 * 上手容易 ## Spring Boot 的核心配置文件有哪几个?它们的区别是什么 Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。 application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。 bootstrap 配置文件有以下几个应用场景。 * 使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息; * 一些固定的不能被覆盖的属性; * 一些加密/解密的场景; ## Spring Boot 的配置文件有哪几种格式?它们有什么区别 .properties 和 .yml,它们的区别主要是书写格式不同。 1).properties ``` app.user.name = javastack ``` 2).yml ``` app:   user:     name: javastack ``` 另外,.yml 格式不支持 `@PropertySource` 注解导入配置。 ## Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的? 启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能:             @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 @ComponentScan:Spring组件扫描。 ## 开启 Spring Boot 特性有哪几种方式 1)继承spring-boot-starter-parent项目 2)导入spring-boot-dependencies项目依赖 ## Spring Boot 需要独立的容器运行吗 可以不需要,内置了 Tomcat/ Jetty 等容器 ## 运行 Spring Boot 有哪几种方式 1)打包用命令或者放到容器中运行 2)用 Maven/ Gradle 插件运行 3)直接执行 main 方法运行 ## Spring Boot 自动配置原理是什么 注解 @EnableAutoConfiguration, @Configuration, @ConditionalOnClass 就是自动配置的核心,首先它得是一个配置文件,其次根据类路径下是否有这个类去自动配置。 ## 如何理解 Spring Boot 中的 Starters Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成 Spring 及其他技术,而不需要到处找示例代码和依赖包。如你想使用 Spring JPA 访问数据库,只要加入 spring-boot-starter-data-jpa 启动器依赖就能使用了。 Starters包含了许多项目中需要用到的依赖,它们能快速持续的运行,都是一系列得到支持的管理传递性依赖。 ## 如何在 Spring Boot 启动的时候运行一些特定的代码 可以实现接口 ApplicationRunner 或者 CommandLineRunner,这两个接口实现方式一样,它们都只提供了一个 run 方法。 ## Spring Boot 有哪几种读取配置的方式 Spring Boot 可以通过 @PropertySource,@Value,@Environment, @ConfigurationProperties 来绑定变量 ## Spring Boot 支持哪些日志框架?推荐和默认的日志框架是哪个 Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用 Starters 启动器,Spring Boot 将使用 **Logback **作为默认日志框架。 ## SpringBoot 实现热部署有哪几种方式 主要有两种方式: * Spring Loaded * Spring-boot-devtools ## 如何理解 Spring Boot 配置加载顺序 在 Spring Boot 里面,可以使用以下几种方式来加载配置。 1)properties文件; 2)YAML文件; 3)系统环境变量; 4)命令行参数; 等等…… ## Spring Boot 如何定义多套不同环境配置 提供多套配置文件,如: applcation.properties application-dev.properties application-test.properties 利用**profile**参数指定环境。 ## Spring Boot 可以兼容老 Spring 项目吗 可以兼容,使用 `@ImportResource` 注解导入老 Spring 项目配置文件。 ## 保护 Spring Boot 应用有哪些方法 * 在生产中使用HTTPS * 使用Snyk检查你的依赖关系 * 升级到最新版本 * 启用CSRF保护 * 使用内容安全策略防止XSS攻击 ## 什么是Swagger Swagger广泛用于可视化API,使用Swagger UI为前端开发人员提供在线沙箱。Swagger是用于生成RESTful Web服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过Swagger正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。 ## 什么是JavaConfig Spring JavaConfig是Spring社区的产品,它提供了配置Spring IoC容器的纯Java方法。因此它有助于避免使用XML配置。使用JavaConfig的优点在于: * 面向对象的配置。由于配置被定义为JavaConfig中的类,因此用户可以充分利用Java中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean方法等。 * 减少或消除XML配置。基于依赖注入原则的外化配置的好处已被证明。但是,许多开发人员不希望在XML和Java之间来回切换。JavaConfig为开发人员提供了一种纯Java方法来配置与XML配置概念相似的Spring容器。从技术角度来讲,只使用JavaConfig配置类来配置容器是可行的,但实际上很多人认为将JavaConfig与XML混合匹配是理想的。 * 类型安全和重构友好。JavaConfig提供了一种类型安全的方法来配置Spring容器。由于Java 5.0对泛型的支持,现在可以按类型而不是按名称检索bean,不需要任何强制转换或基于字符串的查找。 # Spring Cloud ## spring Cloud和 Dubbo有哪些区別 本质区别:  * dubbo 是 基于 RPC 远程 过程调用  * cloud 是基于 http  rest api 调用 | | dubbo | Spring Cloud | | --- | --- | --- | | 服务注册中心 | Zookeeper | Spring Cloud Netflix Eureka | | 服务调用方式 | RPC | Restful API | | 监控服务 | Dubbo-monitor | Spring Boot Admin | | 断路器 | 不完善 | Spring Cloud Netflix Hystrix | | 服务网关 | 无 | Spring Cloud Netflix Zuul | | 分布式配置 | 无 | Spring Cloud Config | | 服务跟踪 | 无 | Spring Cloud Sleuth | | 消息总线 | 无 | Spring Cloud Bus | | 数据流 | 无 | Spring Cloud Stream | | 批量任务 | 无 | Spring Cloud Task | 很明显, Spring Cloud的功能比 DUBBO更加强大,涵盖面更广,而且作为 Spring的挙头项目,它也能够与 Spring FrameworkSpring Boot.、 Spring Data、 Spring Batch等其他 Springi项目完美融合,这些对于微服务而言是至关重要的。使用 Dubbo构建的微服务架构就像组装电脑,各环节我们的选择自由度很高,但是最终结果很有可能因为一条内存质量不行就点不亮了,总是让人不怎么放心,但是如果你是一名高手,那这些都不是问题;而 Spring Cloud就像品牌机,在 Spring Source的整合下,做了大量的兼容性测试,保证了机器拥有更高的稳定性,但是如果要在使用非原装组件外的东西,就需要对其基础有足够的了解。  ## REST与RPC概念 **REST** REST是一种架构风格,指的是一组架构约束条件和原则。**满足这些约束条件和原则的应用程序或设计就是 RESTful**。REST规范把所有内容都视为资源,网络上一切皆资源。 REST并没有创造新的技术,组件或服务,只是使用Web的现有特征和能力。 可以完全通过HTTP协议实现,**使用 HTTP 协议处理数据通信**。REST架构对资源的操作包括获取、创建、修改和删除资源的操作正好对应HTTP协议提供的GET、POST、PUT和DELETE方法。 **RPC** 远程方法调用,就是像调用本地方法一样调用远程方法。RPC框架要做到的最基本的三件事: 1、服务端如何确定客户端要调用的函数; 在远程调用中,客户端和服务端分别维护一个【ID->函数】的对应表, ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,附上这个ID,服务端通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。 2、如何进行序列化和反序列化; 客户端和服务端交互时将参数或结果转化为字节流在网络中传输,那么数据转化为字节流的或者将字节流转换成能读取的固定格式时就需要进行序列化和反序列化,**序列化和反序列化**的速度也会影响远程调用的效率。 3、如何进行网络传输(选择何种网络协议); **多数RPC框架选择TCP作为传输协议**,也有部分选择HTTP。如gRPC使用HTTP2。不同的协议各有利弊。**TCP更加高效,而HTTP在实际应用中更加的灵活。** **REST与RPC应用场景** REST和RPC都常用于微服务架构中。 1、**HTTP相对更规范,更标准,更通用,无论哪种语言都支持http协议**。如果你是对外开放API,例如开放平台,外部的编程语言多种多样,你无法拒绝对每种语言的支持,现在开源中间件,基本最先支持的几个协议都包含RESTful。 2、 RPC 框架作为架构微服务化的基础组件,它能**大大降低架构微服务化的成本**,提高调用方与服务提供方的研发效率,屏蔽跨进程调用函数(服务)的各类复杂细节。让调用方感觉就像调用本地函数一样调用远端函数、让服务提供方感觉就像实现一个本地函数一样来实现服务。 ## 你在项目开发中碰到的坑,你所知道的微服务技术栈有哪些 我们在讨论分布式的微服务架构的时候  它需要有哪些维度?  1 服务治理   2 服务注册 3 服务调用 4 负载均衡 5 服务监控 具体为: 1. 服务开发 :spring boot spring mvc  spring  2. 服务的配置与管理 : netfix 公司 archaius 阿里的diamond等 3. 服务的注册与发现 :spriing cloud 所采用的 eureka,consul,zookeeper 等 4. 服务的调用:rest GRPC RPC  5. .服务的熔断器 :hystrix 、envoy等 6. 负载均衡 :ribbon 、nginx 7. 服务接口调用(客户端调用服务的简化工具) Feign等消息队列Kafka、 Rabbitmq、 Activemq等 8. 服务配置中心管理Spring Cloud Config、Chef等 9. 服务路由(API网关)Zuul等 10. 服务监控Zabbix、 Nagios、 Metrics、 Spectator等 11. 全链路追踪Slueth、Zipkin, Brave、 Dapper等 12. 服务部罟Docker、 Open Stack、 Kubernetes等 13. 数据流操作开发包Spring Cloud Stream(封装与 Redis, Rabbit、 Kafka等发送接收消息) 14. 事件消息总线Spring Cloud Bus ## CAP 是什么 所谓的CAP  **C强一致性  A可用性 P 分区容错性** 著名的CAP理论指出:  一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于**分区容错性P在是分布式系统中必须要保证的**,因此我们只能在A和C之间进行权衡。  **zookeeper 遵守 CP,eureka 遵守 AP** ## 什么是Spring Cloud? Spring Cloud是一个含概多个子项目的开发工具集,集合了众多的开源框架,他利用了Spring Boot开发的便利性实现了很多功能,如服务注册,服务注册发现,负载均衡等.Spring Cloud在整合过程中主要是针对Netflix(耐非)开源组件的封装。**SpringCloud是关注全局的微服务协调整理治理框架**,整合并管理各个微服务,为各个微服务之间提供,配置管理,服务发现,断路器,路由,事件总线等集成服务。 ## 什么是服务熔断?什么是服务降级 在复杂的分布式系统中,微服务之间的相互调用,有可能出现各种各样的原因导致服务的阻塞,在高并发场景下,服务的阻塞意味着线程的阻塞,导致当前线程不可用,服务器的线程全部阻塞,导致服务器崩溃,由于服务之间的调用关系是同步的,会对整个微服务系统造成服务雪崩 为了解决某个微服务的调用响应时间过长或者不可用进而占用越来越多的系统资源引起雪崩效应就需要进行服务熔断和服务降级处理。 所谓的服务熔断指的是某个服务故障或异常一起类似显示世界中的“保险丝"当某个异常条件被触发就直接熔断整个服务,而不是一直等到此服务超时。 服务熔断就是相当于我们电闸的保险丝,一旦发生服务雪崩的,就会熔断整个服务,通过维护一个自己的线程池,当线程达到阈值的时候就启动服务降级,如果其他请求继续访问就直接返回fallback的默认值 。 ## 为什么需要负载均衡 为了提供并发量,有时同一个服务提供者可以部署多个。这个客户端在调用时要根据一定的负载均衡策略完成负载调用。Feign是以接口方式进行调用,而不是通过RestTemplate来调用,**feign底层还是ribbon**,它进行了封装,让我们调用起来更加容易。 当对同一个服务部署多个时,就要涉及负载均衡调用了,这是可以选择**Ribbon和Feign**。 ## 什么是ELK ELK堆栈由三个开源产品组成——Elasticsearch、Logstash和Kibana from Elastic。 Elasticsearch是一个基于Lucene搜索引擎的NoSQL数据库。     Logstash是一个日志管道工具,它接受来自不同来源的输入,执行不同的转换,并将数据导出到不同的目标。它是一个动态的数据收集管道,具有可扩展的插件生态系统和强大的弹性搜索协同作用 Kibana是一个可视化UI层,工作在Elasticsearch之上。 这三个项目一起用于各种环境中的日志分析。因此Logstash收集和解析日志、弹性搜索索引并存储这些信息,而Kibana提供了一个UI层,提供可操作的可见性。 ## 什么是WebSocket WebSocket是一种计算机通信协议,通过单个TCP连接提供全双工通信通道。 WebSocket是双向的——使用WebSocket客户端或服务器都可以发起发送消息。 WebSocket是全双工的——客户端和服务器之间的通信是相互独立的。 单个TCP连接——初始连接使用HTTP,然后将此连接升级为基于套接字的连接。然后,这个单一连接将用于未来的所有通信 轻- WebSocket消息数据交换比http轻得多。 # Redis ## 概念 简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以存写速度非常快, 因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业 务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 ## 为什么要用 redis /为什么要用缓存 **高性能**: 假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数 缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当 快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可! **高并发**: 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中 去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。 ## 为什么要用 redis 而不用 map/guava 做缓存? 缓存分为**本地缓存和分布式缓存**。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是 轻量以及快速,**生命周期随着 jvm 的销毁而结束**,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓 存**不具有一致性**。 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,**各实例共用一份缓存数据**,缓存具有一致 性。缺点是需要保持 redis 或 memcached服务的高可用,整个程序架构上较为复杂。 ## redis 和 memcached 的区别 1. redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供 list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。 2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache把数据全部存在内存之中。 3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前 是原生支持 cluster 模式的. 4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。 ## redis 常见数据结构 String、Hash、List、Set、Sort Set(ZSet) ## redis 设置过期时间 **定期删除**:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删 除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所 有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载! **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这 就是所谓的惰性删除,也是够懒的哈! ## 总结: **redis是单线程,线程安全** redis可以能够快速执行的原因: 1. 绝大部分请求是纯粹的内存操作(非常快速) 2. 采用单线程,避免了不必要的上下文切换和竞争条件 3. 非阻塞IO - IO多路复用 IO多路复用中有三种方式:select,poll,epoll。需要注意的是,select,poll是线程不安全的,epoll是线程安全的 **redis内部实现采用epoll**,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间 这3个条件不是相互独立的,特别是第一条,如果请求都是耗时的,采用单线程吞吐量及性能可想而知了。应该说redis为特殊的场景选择了合适的技术方案。 # Spring ## 什么是 Spring 框架 Spring 是一种轻量级开发框架,旨在提高开发人员的开发效率以及系统的可维护性。 我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是:核心容器、数据访问/集成,、Web、AOP(面向切面编程)、工具、消息和测试模块。比如:Core Container 中的 Core 组件是Spring 所有组件的核心,Beans 组件和 Context 组件是实现IOC和依赖注入的基础,AOP组件用来实现面向切面编程。 Spring 官网列出的 Spring 的 6 个特征: * **核心技术** :依赖注入(DI),AOP,事件(events),资源,i18n,验证,数据绑定,类型转换,SpEL。 * **测试** :模拟对象,TestContext框架,Spring MVC 测试,WebTestClient。 * **数据访问** :事务,DAO支持,JDBC,ORM,编组XML。 * **Web支持** : Spring MVC和Spring WebFlux Web框架。 * **集成** :远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 * **语言** :Kotlin,Groovy,动态语言。 ## IoC IoC(Inverse of Control:控制反转)是一种**设计思想**,就是 **将原本在程序中手动创建对象的控制权,交由Spring框架来管理。** IoC 在其他语言中也有应用,并非 Spirng 特有。 **IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个Map(key,value),Map 中存放的是各种对象。** 将对象之间的相互依赖关系交给 IOC 容器来管理,并由 IOC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 **IOC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。** 在实际项目中一个 Service 类可能有几百甚至上千个类作为它的底层,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IOC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。 Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。 **Spring IOC的初始化过程:** ![](https://box.kancloud.cn/4a43459dc172af88e817875e205ea4b0_709x56.png) ## AOP AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,**却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来**,便于**减少系统的重复代码**,**降低模块间的耦合度**,并**有利于未来的可拓展性和可维护性**。 **Spring AOP就是基于动态代理的**,如果要代理的对象,实现了某个接口,那么Spring AOP会使用**JDK Proxy**,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用**Cglib** ,这时候Spring AOP会使用 **Cglib** 生成一个被代理对象的子类来作为代理,如下图所示: ![](https://box.kancloud.cn/b55df1d8a80199f0167157ef84f8f15c_720x354.png) 使用 AOP 之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样大大简化了代码量。我们需要增加新功能时也方便,这样也提高了系统扩展性。日志功能、事务管理等等场景都用到了 AOP 。 ## Spring AOP 和 AspectJ AOP 有什么区别? **Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。** Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。 Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单, 如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比Spring AOP 快很多。 ## Spring 中的 bean 的作用域有哪些? * singleton : 唯一 bean 实例,Spring 中的 bean 默认都是单例的。 * 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 中的单例 bean 的线程安全问题了解吗? 大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 常见的有两种解决办法: 1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 ## Spring 中的 bean 生命周期 * Bean 容器找到配置文件中 Spring Bean 的定义。 * Bean 容器利用 Java Reflection API 创建一个Bean的实例。 * 如果涉及到一些属性值 利用 `set()`方法设置一些属性值。 * 如果 Bean 实现了 `BeanNameAware` 接口,调用 `setBeanName()`方法,传入Bean的名字。 * 如果 Bean 实现了 `BeanClassLoaderAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoader`对象的实例。 * 如果Bean实现了 `BeanFactoryAware` 接口,调用 `setBeanClassLoader()`方法,传入 `ClassLoade` r对象的实例。 * 与上面的类似,如果实现了其他 `*.Aware`接口,就调用相应的方法。 * 如果有和加载这个 Bean 的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessBeforeInitialization()` 方法 * 如果Bean实现了`InitializingBean`接口,执行`afterPropertiesSet()`方法。 * 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。 * 如果有和加载这个 Bean的 Spring 容器相关的 `BeanPostProcessor` 对象,执行`postProcessAfterInitialization()` 方法 * 当要销毁 Bean 的时候,如果 Bean 实现了 `DisposableBean` 接口,执行 `destroy()` 方法。 * 当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。 ![](https://box.kancloud.cn/304a2750ed19255e9edc54b281c33631_720x303.png) ## 说说自己对于 Spring MVC 了解? * **Model1 时代** : 很多学 Java 后端比较晚的朋友可能并没有接触过 Model1 模式下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。这个模式下 JSP 即是控制层又是表现层。显而易见,这种模式存在很多问题。比如①将控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;②前端和后端相互依赖,难以进行测试并且开发效率极低; * **Model2 时代** :学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View,)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。Model:系统涉及的数据,也就是 dao 和 bean。View:展示模型中的数据,只是用来展示。Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。 Model2 模式下还存在很多问题,Model2的抽象和封装程度还远远不够,使用Model2进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。于是很多JavaWeb开发相关的 MVC 框架营运而生比如Struts2,但是 Struts2 比较笨重。随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。 MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的Web层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service层(处理业务)、Dao层(数据库操作)、Entity层(实体类)、Controller层(控制层,返回数据给前台页面)。 **Spring MVC 的简单原理图如下:** ![](https://box.kancloud.cn/f1632c8003765a277da3039471d35a42_804x369.png) ## SpringMVC 工作原理了解吗? **原理如下图所示:** ![](https://box.kancloud.cn/07c38e076a21ad76abe0bb105b467957_1015x466.png) 上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 `DispatcherServlet` 的作用是接收请求,响应结果。 **流程说明(重要):** 1. 客户端(浏览器)发送请求,直接请求到 `DispatcherServlet`。 2. `DispatcherServlet` 根据请求信息调用 `HandlerMapping`,解析请求对应的 `Handler`。 3. 解析到对应的 `Handler`(也就是我们平常说的 `Controller` 控制器)后,开始由 `HandlerAdapter` 适配器处理。 4. `HandlerAdapter` 会根据 `Handler`来调用真正的处理器开处理请求,并处理相应的业务逻辑。 5. 处理器处理完业务后,会返回一个 `ModelAndView` 对象,`Model` 是返回的数据对象,`View` 是个逻辑上的 `View`。 6. `ViewResolver` 会根据逻辑 `View` 查找实际的 `View`。 7. `DispaterServlet` 把返回的 `Model` 传给 `View`(视图渲染)。 8. 把 `View` 返回给请求者(浏览器) ## Spring 框架中用到了哪些设计模式 * **工厂设计模式** : Spring使用工厂模式通过 `BeanFactory`、`ApplicationContext` 创建 bean 对象。 * **代理设计模式** : Spring AOP 功能的实现。 * **单例设计模式** : Spring 中的 Bean 默认都是单例的。 * **模板方法模式** : Spring 中 `jdbcTemplate`、`hibernateTemplate` 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 * **包装器设计模式** : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 * **观察者模式:** Spring 事件驱动模型就是观察者模式很经典的一个应用。 * **适配器模式** :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配`Controller`。 ## @Component 和 @Bean 的区别是什么 1. 作用对象不同: `@Component` 注解作用于类,而`@Bean`注解作用于方法。 2. `@Component`通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用 `@ComponentScan` 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。`@Bean` 注解通常是我们在标有该注解的方法中定义产生这个 bean,`@Bean`告诉了Spring这是某个类的实例,当我需要用它的时候还给我。 3. `@Bean` 注解比 `Component` 注解的自定义性更强,而且很多地方我们只能通过 `@Bean` 注解来注册bean。比如当我们引用第三方库中的类需要装配到 `Spring`容器时,则只能通过 `@Bean`来实现。 下面这个例子是通过 `@Component` 无法实现的。 ``` @Bean public OneService getService(status) { case (status)  {         when 1: returnnew serviceImpl1();         when 2: returnnew serviceImpl2();         when 3: returnnew serviceImpl3();     } } ``` ## 将一个类声明为Spring的 bean 的注解有哪些 我们一般使用 `@Autowired` 注解自动装配 bean,要想把类标识成可用于 `@Autowired`注解自动装配的 bean 的类,采用以下注解可实现: * `@Component` :通用的注解,可标注任意类为 `Spring` 组件。如果一个Bean不知道属于哪个层,可以使用`@Component` 注解标注。 * `@Repository` : 对应持久层即 Dao 层,主要用于数据库相关操作。 * `@Service` : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao层。 * `@Controller` : 对应 Spring MVC 控制层,主要用户接受用户请求并调用 Service 层返回数据给前端页面。 ## Spring 管理事务的方式有几种 1. 编程式事务,在代码中硬编码。(不推荐使用) 2. 声明式事务,在配置文件中配置(推荐使用) **声明式事务又分为两种:** 1. 基于XML的声明式事务 2. 基于注解的声明式事务 # 数据结构之数组、链表、栈、队列 ## 一 数组 **数组(Array)** 是一种很常见的数据结构。它是由相同类型的元素(element)的集合所组成,并且被分配一块连续的内存来存储(与链表对比)。利用元素的索引(index)可以计算出该元素对应的存储地址。它的特点是提供随机访问并且容量有限。 ``` 假如数组的长度为 n。 访问:O(1)//访问特定位置的元素    插入:O(n )//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时 删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时 ``` ![](https://box.kancloud.cn/9a33606bb8fb13a645c34427b220b03e_380x177.png) ## 二 链表 **链表(LinkedList)** 虽然是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序存储,链表在插入和删除的时候可以达到 O(1) 的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 O(n) 的时间,而顺序表相应的时间复杂度分别是O(logn) 和O(1)。 **使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。链表不具有数组随机读取的优点,但是插入删除元素的时间复杂度为O(1)** **链表分类** **常见链表分类:** 1. 单链表 2. 双向链表 3. 循环链表 4. 双向循环链表 ``` 假如链表中有n个元素。 访问:O(n)//访问特定位置的元素 插入删除:O(1)//必须要要知道插入元素的位置 ``` #### 2.2.1 单链表 **单链表** 单向链表只有一个方向,结点只有一个后继指针 next 指向后面的节点。因此,链表这种数据结构通常在物理内存上是不连续的。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向null。 ![](https://box.kancloud.cn/a7175e4341e48442fd054720fc74923d_591x181.png) #### 2.2.2 循环链表 **循环链表** 其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向null,而是指向链表的头结点。 ![](https://box.kancloud.cn/8ac54ac29e3690b8abbd73b8f17b6380_552x171.png) #### 2.2.3 双向链表 **双向链表** 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。 ![](https://box.kancloud.cn/724f97d98bb7a037f1ee703f501437cb_488x215.png) #### 2.2.4 双向循环链表 **双向循环链表** 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。 ![](https://box.kancloud.cn/5ec2f7f6192085f1f350d10b97f72ec9_530x247.png) ### 2.3 数组vs链表 1. 数组使用的是连续内存空间对CPU的缓存机制友好,链表则相反。 2. 数组的大小固定,声明之后就要占用所需的连续内存空间。如果声明的数组过小的话,需要再申请一个更大的内存空间,然后将原数组拷贝进去。数组多的情况下,这将是非常耗时的。链表则天然支持动态扩容。 ## 三 栈 ### 3.1 栈简介 **栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。** 栈常用一维数组或链表来实现,用数组实现的队列叫作 **顺序栈** ,用链表实现的队列叫作 **链式栈** 。 ``` 假设堆栈中有n个元素。 访问:O(n)//最坏情况  插入删除:O(1)//顶端插入和删除元素 ``` ![](https://box.kancloud.cn/76e54457b01cdf2983abd5524efcc125_497x257.png) ### 3.2 栈的常见应用常见应用场景 #### 3.2.1 实现浏览器的回退和前进功能 我们只需要使用两个栈(Stack1和Stack2)和就能实现这个功能。比如你按顺序查看了 1,2,3,4 这四个页面,我们依次把 1,2,3,4 这四个页面压入 Stack1 中。当你想回头看2这个页面的时候,你点击回退按钮,我们依次把4,3这两个页面从Stack1 弹出,然后压入 Stack2 中。假如你又想回到页面3,你点击前进按钮,我们将3页面从Stack2 弹出,然后压入到 Stack1 中。示例图如下: ![](https://box.kancloud.cn/3d37e435b7eb8c9034c9cf9c06addda1_682x483.png) #### 3.2.2 检查符号是否成对出现 ``` 给定一个只包括 `'('`,`')'`,`'{'`,`'}'`,`'['`,`']'` 的字符串,判断该字符串是否有效。 有效字符串需满足: 1. 左括号必须用相同类型的右括号闭合。 2. 左括号必须以正确的顺序闭合。 比如 "()"、"()\[\]{}"、"{\[\]}" 都是有效字符串,而 "(\]" 、"(\[)\]" 则不是。 ``` 这个问题实际是Leetcode的一道题目,我们可以利用栈 `Stack` 来解决这个问题。 1. 首先我们将括号间的对应规则存放在 `Map` 中,这一点应该毋容置疑; 2. 创建一个栈。遍历字符串,如果字符是左括号就直接加入`stack`中,否则将`stack`的栈顶元素与这个括号做比较,如果不相等就直接返回false。遍历结束,如果`stack`为空,返回 `true`。 ``` public boolean isValid(String s){ // 括号之间的对应规则     HashMap mappings = new HashMap();     mappings.put(')', '(');     mappings.put('}', '{');     mappings.put('\]', '\[');     Stack stack = new Stack(); char\[\] chars = s.toCharArray(); for (int i = 0; i < chars.length; i++) { if (mappings.containsKey(chars\[i\])) { char topElement = stack.empty() ? '#' : stack.pop(); if (topElement != mappings.get(chars\[i\])) { returnfalse;             }         } else {             stack.push(chars\[i\]);         }     } return stack.isEmpty(); } ``` #### 3.2.3 反转字符串 将字符串中的每个字符先入栈再出栈就可以了。 #### 3.2.4 维护函数调用 最后一个被调用的函数必须先完成执行,符合栈的 **后进先出(LIFO, Last In First Out)**特性。 ## 四 队列 ### 4.1 队列简介 **队列** 是 **先进先出( FIFO,First In, First Out)** 的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作 **顺序队列** ,用链表实现的队列叫作 **链式队列** 。**队列只允许在后端(rear)进行插入操作也就是 入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue** 队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。 ``` 假设队列中有n个元素。 访问:O(n)//最坏情况 插入删除:O(1)//后端插入前端删除元素 ``` ![](https://box.kancloud.cn/547a0e041851a7129c9711c103a43b20_571x118.png) ### 4.2 队列分类 #### 4.2.1 单队列 单队列就是常见的队列, 每次添加元素时,都是添加到队尾。单队列又分为 **顺序队列(数组实现)** 和 **链式队列(链表实现)**。 **顺序队列存在“假溢出”的问题也就是明明有位置却不能添加的情况。** 假设下图是一个顺序队列,我们将前两个元素1,2 出队,并入队两个元素7,8。当进行入队、出队操作的时候,front和 rear 都会持续往后移动,当 rear 移动到最后的时候,我们无法再往队列中添加数据,即使数组中还有空余空间,这种现象就是 **”假溢出“** 。除了假溢出问题之外,如下图所示,当添加元素8的时候,rear 指针移动到数组之外(越界)。 > 为了避免当只有一个元素的时候,队头和队尾重合使处理变得麻烦,所以引入两个指针,front 指针指向对头元素,rear 指针指向队列最后一个元素的下一个位置,这样当 front 等于 rear 时,此队列不是还剩一个元素,而是空队列。——From 《大话数据结构》 ![](https://box.kancloud.cn/d262d16f85759091c9aad00e8a07aea0_540x232.png) #### 4.2.2 循环队列 循环队列可以解决顺序队列的假溢出和越界问题。解决办法就是:从头开始,这样也就会形成头尾相接的循环,这也就是循环队列名字的由来。 还是用上面的图,我们将 rear 指针指向数组下标为 0 的位置就不会有越界问题了。当我们再向队列中添加元素的时候, rear 向后移动。 ![](https://box.kancloud.cn/211e3f3808ed980e32920f91561ea91e_478x451.png) 顺序队列中,我们说 `front==rear` 的时候队列为空,循环队列中则不一样,也可能为满,如上图所示。解决办法有两种: 1. 可以设置一个标志变量 `flag`,当 `front==rear` 并且 `flag=0` 的时候队列为空,当`front==rear` 并且 `flag=1` 的时候队列为满。 2. 队列为空的时候就是 `front==rear` ,队列满的时候,我们保证数组还有一个空闲的位置,rear 就指向这个空闲位置,如下图所示,那么现在判断队列是否为满的条件就是: `(rear+1) % QueueSize= front` 。 ![](https://box.kancloud.cn/e6fded6b8248540399848761139fae4a_484x112.png) ### 常见应用场景 * **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 * **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 * linux内核进程队列(按优先级排队) * 实现生活中的派对,播放器上的播放列表; * 消息队列 * 等等…… ## Java反射中Method类invoke方法的用法 Method getMethod(String name, Class... parameterTypes)   \--返回一个 Method 对象,它反映此 Class 对象所表示的类或接口的指定公共成员方法。   方法后面接收的就是Class类的对象,而如:String.class、int.class这些字节码才是Class类的对象 也可以此种方式: ``` //getMethod第一个参数是方法名,第二个参数是该方法的参数类型, //因为存在同方法名不同参数这种情况,所以只有同时指定方法名和参数类型才能唯一确定一个方法 Method method = XXX.getClass().getMethod(methodName,new Class\[0\]); //第一个参数是具体调用该方法的对象  //第二个参数是执行该方法的具体参数 如一个函数 int Test(int a, String str); 对应的getMethod方法: 1.  getMethod("Test",int.class,String.class); 2. getMethod("Test",new Class\[\]{int.class,String.class}); ``` 然后通过invoke来调用此方法: 函数原型:Object.Java.lang.reflect.Method.invoke(Object receiver, Object... args) ``` //Method类的invoke(Object obj,Object args\[\])方法接收的参数必须为对象,  //如果参数为基本类型数据,必须转换为相应的包装类型的对象。invoke()方法的返回值总是对象,   //如果实际被调用的方法的返回类型是基本类型数据,那么invoke()方法会把它转换为相应的包装类型的对象,再将其返回 receiver:该方法所在类的一个对象 args: 传入的参数 如 100,“hello” ``` ## ThreadLocal ### ThreadLocal是什么 ThreadLocal是一个**本地线程副本变量工具类**。主要用于将**私有线程和该线程存放的副本对象**做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。 **ThreadLocal的内部结构图** ![](https://box.kancloud.cn/de8892a4be9fdd5609eb0a0c2b8032cb_692x744.png) 从上面的结构图,我们已经窥见ThreadLocal的核心机制: * 每个Thread线程内部都有一个Map。 * Map里面存储线程本地对象(key)和线程的变量副本(value) * 但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。 所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。 ``` Thread线程内部的Map在类中描述如下: public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; } ``` ### 深入解析ThreadLocal ThreadLocal类提供如下几个核心方法: ~~~ public T get() public void set(T value) public void remove() ~~~ * get()方法用于获取当前线程的副本变量值。 * set()方法用于保存当前线程的副本变量值。 * initialValue()为当前线程初始副本变量值。 * remove()方法移除当前前程的副本变量值。 ### 应用场景 Hibernate的session获取场景 ``` ~~~ private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>(); //获取Session public static Session getCurrentSession(){ Session session = threadLocal.get(); //判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中 try { if(session ==null&&!session.isOpen()){ if(sessionFactory==null){ rbuildSessionFactory();// 创建Hibernate的SessionFactory }else{ session = sessionFactory.openSession(); } } threadLocal.set(session); } catch (Exception e) { // TODO: handle exception } return session; } ~~~ ``` 为什么?每个线程访问数据库都应当是一个独立的Session会话,如果多个线程共享同一个Session会话,有可能其他线程关闭连接了,当前线程再执行提交时就会出现会话已关闭的异常,导致系统异常。此方式能避免线程争抢Session,提高并发下的安全性。 使用ThreadLocal的典型场景正如上面的**数据库连接管理**,**线程会话管理**等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。 ### 原理 线程共享变量缓存如下: **Thread**.ThreadLocalMap; 1、**Thread: 当前线程,可以通过Thread.currentThread()获取。** 2、**ThreadLocal:我们的static**ThreadLocal**变量。** 3、**Object: 当前线程共享变量。** 我们调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap,然后根据当前ThreadLocal获取当前线程共享变量**Object。** ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。 这种存储结构的好处: 1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。 2、ThreadLocalMap键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。 关于**ThreadLocalMap弱引用问题**: 当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。 虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。 1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量; 2、JDK建议**ThreadLocal定义为private static**,这样ThreadLocal的弱引用问题则不存在了。 ## 简单介绍一下 Linux 文件系统 在Linux操作系统中,所有被操作系统管理的资源,例如网络接口卡、磁盘驱动器、打印机、输入输出设备、普通文 件或是目录都被看作是一个文件。 也就是说在LINUX系统中有一个重要的概念:**一切都是文件**。其实这是UNIX哲学的一个体现,而Linux是重写UNIX而 来,所以这个概念也就传承了下来。在UNIX系统中,把一切资源都看作是文件,包括硬件设备。UNIX系统把每个硬 件都看成是一个文件,通常称为设备文件,这样用户就可以用读写文件的方式实现对硬件的访问。 Linux支持5种文件类型 : ![](https://box.kancloud.cn/a8db4a9c71c3cb69e5b51655e7278ca3_1117x679.png) ## 一些常见的 Linux 命令 **目录的操作命令(增删改查) ** 1. mkdir 目录名称: 增加目录 2. ls或者ll (ll是ls -l的缩写,ll命令以看到该目录下的所有目录和文件的详细信息):查看目录信息 3. find 目录 参数: 寻找目录(查) 4. mv 目录名称 新目录名称: 修改目录的名称(改) 注意:mv的语法不仅可以对目录进行重命名而且也可以对各种文件,压缩包等进行 重命名的操作。mv命令用 来对文件或目录重新命名,或者将文件从一个目录移到另一个目录中。后面会介绍到mv命令的另一个用法。 5. mv 目录名称 目录的新位置: 移动目录的位置---剪切(改) 注意:mv语法不仅可以对目录进行剪切操作,对文件和压缩包等都可执行剪切操作。另外mv与cp的结果不 同,mv好像文件“搬家”,文件个数并未增加。而cp对文件进行复制,文件个数增加了。 6. cp -r 目录名称 目录拷贝的目标位置: 拷贝目录(改),-r代表递归拷贝 注意:cp命令不仅可以拷贝目录还可以拷贝文件,压缩包等,拷贝文件和压缩包时不 用写-r递归 7. rm [-rf] 目录: 删除目录(删) 注意:rm不仅可以删除目录,也可以删除其他文件或压缩包,为了增强大家的记忆, 无论删除任何目录或文 件,都直接使用rm -rf 目录/文件/压缩包 **文件的操作命令(增删改查) ** 1. touch 文件名称: 文件的创建(增) 2. cat/more/less/tail 文件名称 文件的查看(查) cat : 只能显示最后一屏内容 more : 可以显示百分比,回车可以向下一行, 空格可以向下一页,q可以退出查看 less : 可以使用键盘上的PgUp和PgDn向上 和向下翻页,q结束查看 tail-10 : 查看文件的后10行,Ctrl+C结束 注意:命令 tail -f 文件 可以对某个文件进行动态监控,例如tomcat的日志文件, 会随着程序的运行,日志会变 化,可以使用tail -f catalina-2016-11-11.log 监控 文 件的变化 3. vim 文件: 修改文件的内容(改) vim编辑器是Linux中的强大组件,是vi编辑器的加强版,vim编辑器的命令和快捷方式有很多,但此处不一一阐 述,大家也无需研究的很透彻,使用vim编辑修改文件的方式基本会使用就可以了。 在实际开发中,使用vim编辑器主要作用就是修改配置文件,下面是一般步骤: vim 文件------>进入文件----->命令模式------>按i进入编辑模式----->编辑文件 ------->按Esc进入底行模式----->输 入:wq/q! (输入wq代表写入内容并退出,即保存;输入q!代表强制退出不保存。) 4. rm -rf 文件: 删除文件(删) 同目录删除:熟记 rm -rf 文件 即可 **其他常用命令 ** pwd : 显示当前所在位置 grep 要搜索的字符串 要搜索的文件 --color : 搜索命令,--color代表高亮显示 ps -ef / ps aux : 这两个命令都是查看当前系统正在运行进程,两者的区别是展示格式不同。如果想要查看 特定的进程可以使用这样的格式: ps aux|grep redis (查看包括redis字符串的进程) 注意:如果直接用ps((Process Status))命令,会显示所有进程的状态,通常结合grep命令查看某进程的 状态。 kill -9 进程的pid : 杀死进程(-9 表示强制终止。) 先用ps查找进程,然后用kill杀掉 **网络通信命令: ** 查看当前系统的网卡信息:ifconfig 查看与某台机器的连接情况:ping 查看当前系统的端口使用:netstat -an shutdown : shutdown -h now : 指定现在立即关机; shutdown +5 "System will shutdown after 5 minutes" :指定5分钟后关机,同时送出警告信息给登入用户。 reboot : reboot : 重开机。reboot -w : 做个重开机的模拟(只有纪录并不会真的重开机)。 # Mybatis ## 什么是Mybatis Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。 MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。 通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和 statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。 ## Mybaits的优点 基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接; 很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持) 能够与Spring很好的集成; 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。 ## MyBatis框架的缺点 SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。 SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。 ## MyBatis与Hibernate有哪些不同 Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。 Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。  Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。 ## #{}和${}的区别是什么 :#{}是预编译处理,${}是字符串替换。 Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值; Mybatis在处理${}时,就是把${}替换成变量的值。 使用#{}可以有效的防止SQL注入,提高系统安全性。 ## 通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗 Dao接口即Mapper接口。接口的全限名,就是映射文件中的namespace的值;接口的方法名,就是映射文件中Mapper的Statement的id值;接口方法内的参数,就是传递给sql的参数。 Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个、、、标签,都会被解析为一个MapperStatement对象。 举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。 Mapper接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。 ## Mybatis是如何进行分页的?分页插件的原理是什么 Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。 分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。 ## Mybatis动态sql有什么用?执行原理?有哪些动态sql Mybatis动态sql可以在Xml映射文件内,以标签的形式编写动态sql,执行原理是根据表达式的值 完成逻辑判断并动态拼接sql的功能。 Mybatis提供了9种动态sql标签:trim | where | set | foreach | if | choose | when | otherwise | bind。 ## 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里 Hibernate属于全自动ORM映射工具,使用Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而Mybatis在查询关联对象或关联集合对象时,需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 ## Mybatis的一级、二级缓存 一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。 二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ; 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。