Java开发校招面经

面试

当前面经欠缺:docker,k8s容器,spark,flink,hbase,hive,java网络编程(netty)

一,JAVA

重写和重载

重写:方法签名要完全相同(方法签名就是参数列表和返回类型)

重载:参数列表的个数和类型和顺序不同。重载与方法的返回值无关。

抽象类和接口的比较

参数 抽象类 接口
默认的方法实现 可以有默认的方法实现 完全抽象,根本不存在方法的实现
实现方式 子类用extends关键字来继承抽象类,如果子类不是抽象类的话,它需要实现父级抽象类中所有抽象方法,父类中非抽象方法可重写也可不重写 子类用implements去实现接口,需要实现接口中所有方法
构造器 抽象类可以有构造器(构造器不能用abstract修饰) 接口不能有构造器
与正常Java类的区别 正常Java类可被实例化,抽象类不能被实例化,其他区别见上下文 接口和正常java类是不同的类型
访问修饰符 抽象方法可以用public、protected、default修饰 接口默认是public、不能用别的修饰符去修饰
main方法 抽象类中可以有main方法,可以运行它 接口中不能有main方法,因此不能运行它
多继承 抽象类可继承一个类和实现多个接口 接口只能继承一个或者多个接口
速度 抽象类比接口速度快 接口稍微慢点,因为它需要去寻找类中实现的它的方法
添加新方法 如果在抽象类中添加新非abstract的方法,可以直接添加,因为非abstract方法无需在子类中实现,如果是abstact方法,则需要改变子类的代码,也要实现这个方法 只要在接口中添加方法,实现它的类就要改变,去实现这个新添加的方法

接口主要对行为的抽象 抽象类主要对事物的抽象

1.JVM

1.1JVM内存区域

Java开发校招面经_第1张图片

JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。

程序计数器:一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器。

虚拟机栈:是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

本地方法区:本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务。

:创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域,99%的垃圾都在堆中。

方法区/永久代:即我们常说的永久代(Permanent Generation), 用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

1.2垃圾处理

1.2.1垃圾回收算法

如何确定垃圾?

有两种方法

1.引用计数法

官方回答:在 Java 中,引用和对象是有关联的。如果要操作对象则必须用引用进行。因此,很显然一个简单的办法是通过引用计数来判断一个对象是否可以回收。简单说,即一个对象如果没有任何与之关联的引用,即他们的引用计数都不为 0,则说明对象不太可能再被用到,那么这个对象就是可回收对象。

通俗的话来讲:一个对象没有任何关联引用,即该对象的引用计数为0,所有该对象为垃圾。

2.可达性分析

官方回答:为了解决引用计数法的循环引用问题,Java 使用了可达性分析的方法。通过一系列的“GC roots”对象作为起点搜索。如果在“GC roots”和一个对象之间没有可达路径,则称该对象是不可达的。要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。

通俗的话来讲:一个对象和“GC root”没有可达路径,则该对象为不可达,但需要两次都为不可达,该对象才可能是垃圾。


垃圾回收算法有哪些?

1.标记清除算法

最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。该算法最大的问题是内存碎片化严重,后续可能发生大对象不能找到可利用空间的问题。

2.复制算法

为了解决 Mark-Sweep 算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉。这种算法虽然实现简单,内存效率高,不易产生碎片,但是最大的问题是可用内存被压缩到了原本的一半。且存活对象增多的话,效率会大大降低

3.标记复制法

结合了以上两个算法,为了避免缺陷而提出。标记阶段和 Mark-Sweep 算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。

4.分代收集法

一般情况下将 GC 堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。

新生代与复制算法和标记清除算法

分为Eden区和两个Survivor区 ,Survivor区分为SurvivorFrom区和SurvivorTo区,其三个区的空间分为8:1:1, Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。

老年代与标记复制算法

老年代因为每次只回收少量对象,因而采用 Mark-Compact 算法。

5.分区收集法

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收.这样做的好处是可以控制一次回收多少个小区间 , 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次 GC 所产生的停顿。

6.java四种引用类型

强引用:当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。

软引用:当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。

弱引用:它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

虚引用:虚引用的主要作用是跟踪对象被垃圾回收的状态。

1.2.2垃圾收集器

Java开发校招面经_第2张图片

Serial垃圾收集器

官方回答:Serial(英文连续)是最基本垃圾收集器,使用复制算法,曾经是JDK1.3.1 之前新生代唯一的垃圾收集器。Serial 是一个单线程的收集器,它不但只会使用一个 CPU 或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。Serial 垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个 CPU 环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此 Serial垃圾收集器依然是 java 虚拟机运行在 Client 模式下默认的新生代垃圾收集器。

通俗一点来讲:在jdk1.3.1之前就出现的,serial垃圾收集器是一个单线程的垃圾收集器,所以它只使用一个cpu或者单个线程去完成垃圾回收工作,同时他需要暂停其它所有线程的工作直到垃圾回收结束。对于限定单个CPU环境来说,没有线程交互的开销,serial的效率是最高的,所以它仍在JVM运行在client模式下的默认垃圾收集器。

ParNew垃圾收集器

官方回答:ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,也使用复制算法,除了使用多线程进行垃圾收集之外,其余的行为和 Serial 收集器完全一样,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew 收集器默认开启和 CPU 数目相同的线程数,可以通过-XX:ParallelGCThreads 参数来限制垃圾收集器的线程数。【Parallel:平行的】ParNew虽然是除了多线程外和Serial 收集器几乎完全一样,但是ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。

通俗一点来讲:ParNew 垃圾收集器其实是 Serial 收集器的多线程版本,ParNew 垃圾收集器在垃圾收集过程中同样也要暂停所有其他的工作线程。ParNew垃圾收集器是很多 java虚拟机运行在 Server 模式下新生代的默认垃圾收集器。

Parallel Scavenge垃圾收集器

官方回答:Parallel Scavenge 收集器也是一个新生代垃圾收集器,同样使用复制算法,也是一个多线程的垃圾收集器,它重点关注的是程序达到一个可控制的吞吐量(Thoughput,CPU 用于运行用户代码的时间/CPU 总消耗时间,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)),高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

通俗一点来讲:它重点关注的是程序达到一个可控制的吞吐量,主要适用于在后台运算而不需要太多交互的任务。自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别。

Serial Old收集器

Serial Old 是 Serial 垃圾收集器年老代版本,它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在 Client 默认的 java 虚拟机默认的年老代垃圾收集器。 垃圾收集过程中需要暂停所有的工作线程。

在 Server 模式下,主要有两个用途:

  1. 在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。

  2. 作为年老代中使用 CMS 收集器的后备垃圾收集方案。

Parallel Old收集器

Parallel Old 收集器是Parallel Scavenge的年老代版本,使用多线程的标记-整理算法,在 JDK1.6才开始提供。

在 JDK1.6 之前,新生代使用 ParallelScavenge 收集器只能搭配年老代的 Serial Old 收集器,只能保证新生代的吞吐量优先,无法保证整体的吞吐量,Parallel Old 正是为了在年老代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。

CMS收集器

Concurrent mark sweep(CMS)收集器是一种年老代垃圾收集器,其最主要目标是获取最短垃圾回收停顿时间,和其他年老代使用标记-整理算法不同,它使用多线程的标记-清除算法。最短的垃圾收集停顿时间可以为交互比较高的程序提高用户体验。CMS 工作机制相比其他的垃圾收集器来说更复杂,整个过程分为以下 4 个阶段:

初始标记:只是标记一下 GC Roots 能直接关联的对象,速度很快,仍然需要暂停所有的工作线程。

并发标记:进行 GC Roots 跟踪的过程,和用户线程一起工作,不需要暂停工作线程。

重新标记:为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。

并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行

G1收集器

Garbage first 垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比与 CMS 收集器,G1 收集器两个最突出的改进是:

  1. 基于标记-整理算法,不产生内存碎片。

  2. 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1 收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保 G1 收集器可以在有限时间获得最高的垃圾收集效率。

Java开发校招面经_第3张图片

1.3.IO/NIO

1.3.1 IO
阻塞IO模型

最传统的一种 IO 模型,即在读写数据过程中会发生阻塞现象。当用户线程发出 IO 请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出 CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为:data = socket.read();如果数据没有就绪,就会一直阻塞在 read 方法。

非阻塞IO模型

当用户线程发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。所以事实上,在非阻塞 IO 模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞 IO不会交出 CPU,而一直占用 CPU。

多路复用IO模型

多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO。在多路复用 IO模型中,会有一个线程不断去轮询多个 socket 的状态,只有当 socket 真正有读写事件时,才真正调用实际的 IO 读写操作。因为在多路复用 IO 模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket 读写事件进行时,才会使用 IO 资源,所以它大大减少了资源占用。在 Java NIO 中,是通过 selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。多路复用 IO 模式,通过一个线程就可以管理多个 socket,只有当socket 真正有读写事件发生才会占用资源来进行实际的读写操作。因此,多路复用 IO 比较适合连接数比较多的情况。另外多路复用 IO 为何比非阻塞 IO 模型的效率高是因为在非阻塞 IO 中,不断地询问 socket 状态时通过用户线程去进行的,而在多路复用 IO 中,轮询每个 socket 状态是内核在进行的,这个效率要比用户线程要高的多。不过要注意的是,多路复用 IO 模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

信号驱动IO模型

在信号驱动 IO 模型中,当用户线程发起一个 IO 请求操作,会给对应的 socket 注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。

异步IO模型

异步 IO 模型才是最理想的 IO 模型,在异步 IO 模型中,当用户线程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,它会立刻返回,说明 read 请求已经成功发起了,因此不会对用户线程产生任何 block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示 IO 操作已经完成,可以直接去使用数据了。

1.3.2 NIO

NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字符流进行操作,而 NIO 基于 Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

Channel

Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream,而 Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。

Buffer

Buffer,故名思意,缓冲区,实际上是一个容器,是一个连续数组。Channel 提供从文件、网络读取数据的渠道,但是读写或者写入的数据必须经过Buffer。

Selector

Selector 类是 NIO 的核心类,Selector 能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。

1.4JVM类加载

1.4.1类加载机制

JVM类加载机制分为5个部分:加载,验证,准备,解析,初始化

加载:是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对 象,作为方法区这个类的各种数据的入口。

验证:这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

解析:是指虚拟机将常量池中的符号引用替换为直接引用的过程。

初始化:是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

1.4.2启动类加载器

负责加载 JAVA_HOME\lib 目录中的。

1.4.3扩展类加载器

负责加载 JAVA_HOME\lib\ext 目录中的。

1.4.4应用程序类加载器

负责加载用户路径(classpath)上的类库。

1.4.5双亲委派

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class),子类加载器才会尝试自己去加载。

Java开发校招面经_第4张图片

2.Java集合

2.1 Collection

Collection:Collection 是集合 List、Set、Queue 的最基本的接口。

2.1.1 List

ArrayList

ArrayList 是最常用的 List 实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要将已经有数组的数据复制到新的存储空间中。当从 ArrayList 的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。

Vector

Vector 与 ArrayList 一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写 Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问 ArrayList 慢。

LinkedList

LinkedList 是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了 List 接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

2.1.2 Set

Set 注重独一无二的性质,该体系集合用于存储无序(存入和取出的顺序不一定相同)元素,值不能重复。对象的相等性本质是对象 hashCode 值(java 是依据对象的内存地址计算出的此序号)判断的,如果想要让两个不同的对象视为相等的,就必须覆盖 Object 的 hashCode 方法和 equals 方法.


HashSet

哈希表边存放的是哈希值。HashSet 存储元素的顺序并不是按照存入时的顺序(和 List 显然不同) 而是按照哈希值来存的所以取数据也是按照哈希值取得。元素的哈希值是通过元素的hashcode 方法来获取的, HashSet 首先判断两个元素的哈希值,如果哈希值一样,接着会比较equals 方法 如果 equls 结果为 true ,HashSet 就视为同一个元素。如果 equals 为 false 就不是同一个元素.

TreeSet

  1. TreeSet()是使用二叉树的原理对新 add()的对象按照指定的顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。

  2. Integer 和 String 对象都可以进行默认的 TreeSet 排序,而自定义类的对象是不可以的,自己定义的类必须实现 Comparable 接口,并且覆写相应的 compareTo()函数,才可以正常使用。

  3. 在覆写 compare()函数时,要返回相应的值才能使 TreeSet 按照一定的规则来排序

  4. 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零或正整数

LinkHashSet

对于 LinkedHashSet 而言,它继承与 HashSet、又基于 LinkedHashMap 来实现的。LinkedHashSet 底层使用 LinkedHashMap 来保存所有元素,它继承与 HashSet,其所有的方法操作上又与 HashSet 相同,因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,底层构造一个 LinkedHashMap 来实现,在相关操作上与父类 HashSet 的操作相同,直接调用父类 HashSet 的方法即可。

2.1.3 Queue

在两端输入list,所有也可以用数组或链表来实现

2.2 Map

Map:是映射表的基础接口

2.2.1 HashMap

key是通过hashcode来存储的,它的内部结构是数组,链表,在jdk1.8后为了优化hashmap的性能加入了红黑树。

hashmap维护了一个数组,数组中的存储类型是entry,每个entry需要用hash算法活动hashcode,然后对hashcode进行去摸操作,然后根据取模的值存入数组,如果hash算法比较优秀的话那么每个entry在数组中会分散的比较均匀,如果hash算法比较差,可能计算出来的hashcode取模结果都是一样的,那么整个数组将会退化成一个链表(查询效率变高),当链表数量超过8时(8是因为泊松分布),将会转化成红黑树(查询速度是logn)。

hashmap扩容机制

hashmap默认初始长度是16,负载因子是0.75。扩容阈值是长度乘负载因子,也就是16*0.75=12

当hashmap的使用长度到达75%时,我们就会对hashmap进行扩容,扩容是一个比较耗资源的操作,但是好处是数组变得更长了使得个数碰撞的几率变小了,就提高了查询效率,为什么负载因子是0.75,是因为个数碰撞的几率是服从泊松分布的,发现在0.75处碰撞的几率较小。

2.2.2 HashTable

Hashtable 是遗留类,很多映射的常用功能与 HashMap 类似,不同的是它承自 Dictionary 类,并且是线程安全的,任一时间只有一个线程能写 Hashtable,并发性不如 ConcurrentHashMap,因为 ConcurrentHashMap 引入了分段锁。Hashtable 不建议在新代码中使用,不需要线程安全的场合可以用 HashMap 替换,需要线程安全的场合可以用 ConcurrentHashMap 替换。


注意:在局部变量下不是多个线程同时访问一个资源的情况下优先hashmap,在全局变量,多个线程共享访问选择concurrenthashmap


2.2.3 TreeMap

TreeMap 实现 SortedMap 接口,能够把它保存的记录根据键排序,默认是按键值的升序排序。

3.多线程

3.1 线程基础

3.1.1 线程的生命周期

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。


创建:当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值

就绪:当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

运行:如果处于就绪状态的线程获得了 CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

阻塞:阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。阻塞的情况分三种:

  1. 等待阻塞:运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue)

    中。

  2. 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线

    程放入锁池(lock pool)中。

  3. 其它阻塞:运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。

死亡:线程会以下面三种方式结束,结束后就是死亡状态。

  1. 正常结束:run()或 call()方法执行完成,线程正常结束。
  2. 异常结束:线程抛出一个未捕获的 Exception 或 Error。
  3. 调用stop:直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。
3.1.2 sleep和wait
  1. 对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。

  2. sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

  3. 在调用 sleep()方法的过程中,线程不会释放对象锁。

  4. 而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

3.1.3 start和run
  1. start()方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  2. 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  3. 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行结束, 此线程终止。然后 CPU 再调度其它线程。
3.1.4 线程池

当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

Java通过Executors提供四种线程池,分别为:

1、newSingleThreadExecutor

创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

2、newFixedThreadPool

创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

3、newScheduledThreadPool

创建一个可定期或者延时执行任务的定长线程池,支持定时及周期性任务执行。

4、newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

5。线程池. https://www.bilibili.com/video/BV1MJ411e7SA

Java开发校招面经_第5张图片

3.2 java锁

3.2.1 线程安全

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的。

什么时候需要考虑线程安全?

1,多个线程访问同一个资源

2,资源是有状态的,比如我们上述讲的字符串拼接,这个时候数据是会有变化的


String,StringBuffer,StringBuilder

字符串:

String 跟其他两个类的区别是

String是final类型,每次声明的都是不可变的对象,
所以每次操作都会产生新的String对象,然后将指针指向新的String对象。

StringBuffer,StringBuilder都是在原有对象上进行操作

所以,如果需要经常改变字符串内容,则建议采用这两者。

StringBuffer vs StringBuilder

前者是线程安全的,后者是线程不安全的。
线程不安全性能更高,所以在开发中,优先采用StringBuilder.
StringBuilder > StringBuffer > String

数组list:

ArrayList:线程不安全,效率高,常用
Vector:线程安全的,效率低

Map:

hashmap:线程不安全

hashtable:线程安全,是一个遗留类,不怎么使用

concurrenthashmap:线程安全,且适合高吞吐量,采用分段锁

3.2.2 乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。

一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会+1。当线程A要更新数据值时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。

3.2.3 悲观锁

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制,悲观锁主要分为共享锁和独占锁:

共享锁:共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

独占锁:就是不能与其他锁并存,如果一个事务获取了一个数据行的独占锁,其他事务就不能再获取该行的其他锁,包括共享锁和独占锁,但是获取独占锁的事务是可以对数据行读取和修改。

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

3.2.4 CAS

CAS 是项乐观锁技术,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

3.2.5 AQS

AQS(AbstractQueuedSynchronizer),是 JDK 下提供的一套用于实现基于 FIFO 等待队列的阻塞锁和相关的同步器的一个同步框架。它使用了一个原子的int value status来作为同步器的状态(如:独占锁,1代表已占有,0代表未占有),通过该类提供的原子修改 status 方法(getState setState and compareAnsSetState),可以把它作为同步器的基础框架类来实现各种同步器。AQS 还定义了一个实现了 Condition 接口的 ConditionObject 内部类。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。
简单来说,就是 Condition 提供类似于 Object 的 wait、notify 的功能 signal 和 await,都是可以使一个正在执行的线程挂起(推迟执行),直到被其他线程唤醒。但是 Condition 更加强大,如支持多个条件谓词、保证线程唤醒的顺序和在挂起时不需要拥有锁。这个抽象类被设计为作为一些可用原子 int 值来表示状态的同步器的基类。如果有看过类似 CountDownLatch 类的源码实现,会发现其内部有一个继承了 AbstractQueuedSynchronizer 的内部类 Sync。可见 CountDownLatch 是基于 AQS 框架来实现的一个同步器。类似的同步器在 JUC 下还有不少。

3.2.6 自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

自旋锁的优缺点:自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cup 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁;自旋锁时间阈值(1.6** **引入了适应性自旋锁)**自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。但是如何去选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!JVM 对于自旋周期的选择,jdk1.5 这个限度是一定的写死的,在 1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时 JVM 还针对当前 CPU 的负荷情况做了较多的优化,如果平均负载小于 CPUs 则一直自旋,如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞,如果正在自旋的线程发现 Owner 发生了变化则延迟自旋时间(自旋计数)或进入阻塞,如果 CPU 处于节电模式则停止自旋,自旋时间的最坏情况是 CPU的存储延迟(CPU A 存储了一个数据,到 CPU B 得知这个数据直接的时间差),自旋时会适当放弃线程优先级之间的差异。

Java开发校招面经_第6张图片

3.2.7公平锁和非公平锁(带完成)

自旋锁的开启

JDK1.6 中-XX:+UseSpinning 开启;

-XX:PreBlockSpin=10 为自旋次数;

JDK1.7 后,去掉此参数,由 jvm 控制;

锁升级

级别由低到高依次为:无锁状态偏向锁状态轻量级锁状态重量级锁状态

4.Spring框架

核心的IOC容器技术(控制反转),帮助我们自动管理依赖的对象,不需要我们自己创建和管理依赖对象,从而实现了层与层之间的解耦,所以重点是解耦!

核心的AOP技术(面向切面编程),方便我们将一些非核心业务逻辑抽离,从而实现核心业务和非核心业务的解耦,比如添加一个商品信息,那么核心业务就是做添加商品信息记录这个操作,非核心业务比如,事务的管理,日志,性能检测,读写分离的实现等等


spring的bean模式

1,默认是singleton,即单例模式

2,prototype,每次从容器调用bean时都会创建一个新的对象,比如整合Struts2框架的时候,spring管理action对象则需要这么设置。

3,request,每次http请求都会创建一个对象

4,session,同一个session共享一个对象

5,global-session

spring是线程安全吗

线程不安全构成的三要素:

1,多线程环境

2,访问同一个资源

3,资源具有状态性

那么Spring的bean模式是单例,而且后端的程序,天然就处于一个多线程的工作环境。

那么是安全的吗?

关键看第3点,我们的bean基本是无状态的,所以从这个点来说,是安全的。

所谓无状态就是没有存储数据,即没有通过数据的状态来作为下一步操作的判断依据

spring MVC工作流程

Java开发校招面经_第7张图片

动态代理

SpringAOP(面向切面编程),AOP分离核心业务逻辑和非核心业务逻辑,其背后动态代理的思想,

主要的实现手段有两种

1,JDK的动态代理,是基于接口的实现

2,基于CGLIB的动态代理,是基于继承当前类的子类来实现的(所以,这个类不能是final)。我们项目结构是没有接口的情况下,如果实现动态代理,那么就需要使用这种方法。

所以,我们的Spring默认会在以上两者根据代码的关系自动切换,当我们采用基于接口的方式编程时,则默认采用JDK的动态代理实现。如果不是接口的方式,那么会自动采用CGLIB。

SpringAOP的背后实现原理就是动态代理机制。

spring事务

required,supports,mandatory,requires_new,not_supported,never

PROPAGATION_REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

PROPAGATION_SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起

PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

说一下 spring 的事务隔离?

事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:

  • 脏读:一个事务读到另一个事务未提交的更新数据。
  • 幻读:例如第一个事务对一个表中的数据进行了修改,同时,第二个事务也修改这个表中的数据。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。
  • 不可重复读:比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。

MyBatis-缓存机制,从一级缓存到二级缓存

一级缓存总结:

1,一级缓存模式是开启状态
2,一级缓存作用域在于SqlSession(大家可以关闭SqlSession,然后创建一个新的,再获取对象,观察实验结果)
3,如果中间有对数据的更新操作,则将清空一级缓存。

下面,我们来看二级缓存(重点)

要使用二级缓存,需要经历两个步骤
1,开启二级缓存(默认处于开启状态)

2,在Mapper.xml中,配置二级缓存(也支持在接口配置)
在标签下面添加标签即可
默认的二级缓存配置会有如下特点:
2.1 所有的Select语句将会被缓存
2.2 所有的更新语句(insert、update、delete)将会刷新缓存
2.3 缓存将采用LRU(Least Recently Used 最近最少使用)算法来回收
2.4 缓存会存储1024个对象的引用
回收算法建议采用LRU,当然,还提供了FIFO(先进先出),SOFT(软引用),WEAK(弱引用)等其他算法。

二级缓存关键说明:
当关闭了SqlSession之后,才会将查询数据保存到二级缓存中(SqlSessionFactory)中,所以才有了上述的缓存命中率。MyBatis的二级缓存默认采用的是Map的实现。

5.mysql数据库

MySQL索引数据结构为什么使用B+树:

https://www.bilibili.com/video/BV1yT4y1w7FS?t=595

MySQL索引底层原理:

https://www.bilibili.com/video/BV1aE41117sk?from=search&seid=15137089913446883578

5.1什么是索引?

索引是一种数据结构,可以帮助我们快速的进行数据的查找.

5.2索引是个什么样的数据结构呢?

索引的数据结构和具体存储引擎的实现有关, 在MySQL中使用较多的索引有Hash索引,B+树索引等,而我们经常使用的InnoDB存储引擎的默认索引实现为:B+树索引.

5.3Hash索引和B+树所有有什么区别或者说优劣呢?

首先要知道Hash索引和B+树索引的底层实现原理:

hash索引底层就是hash表,进行查找时,调用一次hash函数就可以获取到相应的键值,之后进行回表查询获得实际数据.B+树底层实现是多路平衡查找树.对于每一次的查询都是从根节点出发,查找到叶子节点方可以获得所查键值,然后根据查询判断是否需要回表查询数据.

那么可以看出他们有以下的不同:

  • hash索引进行等值查询更快(一般情况下),但是却无法进行范围查询.

因为在hash索引中经过hash函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询.而B+树的的所有节点皆遵循(左节点小于父节点,右节点大于父节点,多叉树也类似),天然支持范围.

  • hash索引不支持使用索引进行排序,原理同上.
  • hash索引不支持模糊查询以及多列索引的最左前缀匹配.原理也是因为hash函数的不可预测.AAAAAAAAB的索引没有相关性.
  • hash索引任何时候都避免不了回表查询数据,而B+树在符合某些条件(聚簇索引,覆盖索引等)的时候可以只通过索引完成查询.
  • hash索引虽然在等值查询上较快,但是不稳定.性能不可预测,当某个键值存在大量重复的时候,发生hash碰撞,此时效率可能极差.而B+树的查询效率比较稳定,对于所有的查询都是从根节点到叶子节点,且树的高度较低.

因此,在大多数情况下,直接选择B+树索引可以获得稳定且较好的查询速度.而不需要使用hash索引.

5.4四种隔离级别

  1. Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
  2. Repeatable read (可重复读):可避免脏读、不可重复读的发生。
  3. Read committed (读已提交):可避免脏读的发生。
  4. Read uncommitted (读未提交):最低级别,任何情况都无法保证。
  • 脏读:一个事务读到另一个事务未提交的更新数据。
  • 幻读:例如第一个事务对一个表中的数据进行了修改,同时,第二个事务也修改这个表中的数据。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行,就好象发生了幻觉一样。
  • 不可重复读:比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。

5.5Mysql 中 MyISAM 和 InnoDB 的区别有哪些?

区别:

  1. InnoDB支持事务,MyISAM不支持

    对于InnoDB每一条SQL语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条SQL语言放在begin和commit之间,组成一个事务;

  2. InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MYISAM会失败;

  3. InnoDB是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。

    但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此主键不应该过大,因为主键太大,其他索引也都会很大。

    而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

  4. InnoDB不保存表的具体行数,执行select count(*) from table时需要全表扫描。而MyISAM用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

  5. Innodb不支持全文索引,而MyISAM支持全文索引,查询效率上MyISAM要高;

如何选择:

  1. 是否要支持事务,如果要请选择innodb,如果不需要可以考虑MyISAM;
  2. 如果表中绝大多数都只是读查询,可以考虑MyISAM,如果既有读写也挺频繁,请使用InnoDB
  3. 系统奔溃后,MyISAM恢复起来更困难,能否接受;
  4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的,如果你不知道用什么,那就用InnoDB,至少不会差。
  5. 案例:!!!!博客系统:innoDB 数据可视化系统:MyISAM

5.6sql优化的几种方式

SQL优化的一些方法

1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。

2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0

3.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。

4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10
union all
select id from t where num=20

5.in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3

6.下面的查询也将导致全表扫描:
select id from t where name like ‘%abc%’

7.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2

8.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)=‘abc’–name以abc开头的id
应改为:
select id from t where name like ‘abc%’

9.不要在 where 子句中的“=”左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

12.很多时候用 exists 代替 in 是一个好的选择:
select num from a where num in(select num from b)
用下面的语句替换:
select num from a where exists(select 1 from b where num=a.num)

13.并不是所有索引对查询都有效,SQL是根据表中数据来进行查询优化的,当索引列有大量数据重复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female几乎各一半,那么即使在sex上建了索引也对查询效率起不了作用。

14.索引并不是越多越好,索引固然可以提高相应的 select 的效率,但同时也降低了 insert 及 update 的效率,
因为 insert 或 update 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。

15.尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

16.尽可能的使用 varchar 代替 char ,因为首先变长字段存储空间小,可以节省存储空间,
其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

17.任何地方都不要使用 select * from t ,用具体的字段列表代替“*”,不要返回用不到的任何字段。

18.避免频繁创建和删除临时表,以减少系统表资源的消耗。

19.临时表并不是不可使用,适当地使用它们可以使某些例程更有效,例如,当需要重复引用大型表或常用表中的某个数据集时。但是,对于一次性事件,最好使用导出表。

20.在新建临时表时,如果一次性插入数据量很大,那么可以使用 select into 代替 create table,避免造成大量 log ,
以提高速度;如果数据量不大,为了缓和系统表的资源,应先create table,然后insert。

21.如果使用到了临时表,在存储过程的最后务必将所有的临时表显式删除,先 truncate table ,然后 drop table ,这样可以避免系统表的较长时间锁定。

22.尽量避免使用游标,因为游标的效率较差,如果游标操作的数据超过1万行,那么就应该考虑改写。

23.使用基于游标的方法或临时表方法之前,应先寻找基于集的解决方案来解决问题,基于集的方法通常更有效。

24.与临时表一样,游标并不是不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其他逐行处理方法,尤其是在必须引用几个表才能获得所需的数据时。
在结果集中包括“合计”的例程通常要比使用游标执行的速度快。如果开发时间允许,基于游标的方法和基于集的方法都可以尝试一下,看哪一种方法的效果更好。

25.尽量避免大事务操作,提高系统并发能力。

26.尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。

5.6sql执行顺序

  1. from
  2. join
  3. on
  4. where
  5. group by(开始使用select中的别名,后面的语句中都可以使用)
  6. avg,sum…
  7. having
  8. select
  9. distinct
  10. order by
  11. limit

6.Redis数据库

6.1Redis持久化机制

RDB持久化操作时,子进程拷贝父进程的数据副本用于持久化,不会增加内存消耗吗?
我们知道了解到,Redis的RDB持久化是基于多进程COW机制实现的。我们也简单的讲解了Copy On Write是什么。但是Redis的多进程COW机制也并非就仅仅是复制数据副本来生成数据快照这么简单的,之前说的这么简单,只是为了更容易理解

那么,那么复杂的情况是怎么个样子呢?

select * from data left join Tmp on data.id=Tmp.id where data.Age>10;

RDB持久化操作期间,由子进程在做数据持久化,子进程并不会修改现有的内存数据结构,它只是对内存数据结构进行遍历读取,然后序列化写到磁盘中。然而父进程则不同,因为即使持久化期间,也可能会多个客户端还在不断的向Redis发送读写请求,所以父进程需要不断的对内存数据结构进程修改和更新操作。
因为子进程生成的数据快照肯定是某个时间节点的,而父进程可能又在时刻改变子进程要扫描的内存数据结构,那怎么办呢?

COW机制中的父子进程并非是完全独立的进程,他们之间的关系更像是一个连体婴儿,虽然是两个进程,但是实际在子进程刚刚被fork出来的时候,父子进程实际共享的是同一段内存空间,就相当于有两个头,但却共享一个身体,不过之后还是会慢慢剥离的,前期只是为了尽量共享,节约资源。
此时Redis,就会使用操作系统的COW机制进程数据段页面的分离(数据段是有很多操作系统的页面组合而成)。当父进程对其中一个页面的数据进行修改的时候,会将被共享的数据页面复制一份分离出来,然后对这个复制的页面进行修改。此时子进程想应的页面是没有变化的,依然是子进程产生时那一刻的数据
随着父进程接收的写请求越来越多,要复制出来修改的共享页面也越来越多。所以此时内存就会持续增长,但是由于被修改数据的比例一般占Redis总数据的比例不会太大,所以内存也不会增加原来两倍的情况。当然如果你在持久化期间,全部数据都被修改了个遍,那么两倍内存还是有希望的。

Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rdb文件中,可以通过配置dir和dbfilename两个参数分别指定快照文件的存储路径和文件名。快照的过程如下。

(1)Redis使用fork函数复制一份当前进程(父进程)的副本(子进程);
(2)父进程继续接收并处理客户端发来的命令,而子进程开始将内存中的数据写入硬盘中的临时文件;
(3)当子进程写入完所有数据后会用该临时文件替换旧的 RDB 文件,至此一次快照操作完成。

在执行 fork 的时候操作系统(类 Unix 操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻父子进程共享同一内存数据,当父进程要更改其中某片数据时(如执行一个写命令),操作系统会将该片数据复制一份以保证子进程的数据不受影响,所以新的RDB文件存储的是执行fork一刻的内存数据。

写时复制策略也保证了在 fork 的时刻虽然看上去生成了两份内存副本,但实际上内存的占用量并不会增加一倍。这就意味着当系统内存只有2 GB,而Redis数据库的内存有1.5 GB时,执行 fork后内存使用量并不会增加到3 GB(超出物理内存)。为此需要确保 Linux 系统允许应用程序申请超过可用内存(物理内存和交换分区)的空间,方法是在/etc/sysctl.conf 文件加入 vm.overcommit_memory = 1,然后重启系统或者执行 sysctl vm.overcommit_memory=1 确保设置生效。

另外需要注意的是,当进行快照的过程中,如果写入操作较多,造成 fork 前后数据差异较大,是会使得内存使用量显著超过实际数据大小的,因为内存中不仅保存了当前的数据库数据,而且还保存着 fork 时刻的内存数据。进行内存用量估算时很容易忽略这一问题,造成内存用量超限。

通过上述过程可以发现Redis在进行快照的过程中不会修改RDB文件,只有快照结束后才会将旧的文件替换成新的,也就是说任何时候 RDB 文件都是完整的。这使得我们可以通过定时备份 RDB 文件来实现 Redis 数据库备份。RDB 文件是经过压缩(可以配置rdbcompression 参数以禁用压缩节省CPU占用)的二进制格式,所以占用的空间会小于内存中的数据大小,更加利于传输。

RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。
当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。

6.2Redis缓存穿透,缓存雪崩

缓存雪崩

在缓存失效的期间,有大量数据访问到MySQL,导致MySQL挂掉

解决方案:

随机生存缓存失效时间,这样所有缓存就不会同一时间失效。

缓存穿透

缓存和数据库都没有数据,使请求不断的打到数据库上,导致数据库挂掉。

解决方案:

1.最常见的则是采用布隆过滤器

2.还有就是请求到数据库后将结果放到redis,

缓存击穿:

类比缓存雪崩,就是击穿到某个热点key

解决方案:

上分布式锁

6.3Memcache与Redis的区别都有哪些?

1)、存储方式 Memecache把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis有部份存在硬盘上,redis可以持久化其数据
2)、数据支持类型 memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型 ,提供list,set,zset,hash等数据结构的存储
3)、使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
4). value 值大小不同:Redis 最大可以达到 512M;memcache 只有 1mb。
5)redis的速度比memcached快很多
6)Redis支持数据的备份,即master-slave模式的数据备份。

6.4redis的过期策略以及内存淘汰机制

redis采用的是定期删除+惰性删除策略
为什么不用定时删除策略?
定时删除,用一个定时器来负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求下,CPU要将时间应用在处理请求,而不是删除key,因此没有采用这一策略.
定期删除+惰性删除是如何工作的呢?
定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
于是,惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
采用定期删除+惰性删除就没其他问题了么?
不是的,如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置

maxmemory-policy volatile-lru
1

该配置就是配内存淘汰策略的(什么,你没配过?好好反省一下自己)
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。

6.5Redis 集群方案应该怎么做?都有哪些方案?

1.twemproxy,大概概念是,它类似于一个代理方式, 使用时在本需要连接 redis 的地方改为连接 twemproxy, 它会以一个代理的身份接收请求并使用一致性 hash 算法,将请求转接到具体 redis,将结果再返回 twemproxy。
缺点: twemproxy 自身单端口实例的压力,使用一致性 hash 后,对 redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。

2.codis,目前用的最多的集群方案,基本和 twemproxy 一致的效果,但它支持在 节点数量改变情况下,旧节点数据可恢复到新 hash 节点

3.redis cluster3.0 自带的集群,特点在于他的分布式算法不是一致性 hash,而是 hash 槽的概念,以及自身支持节点设置从节点。具体看官方文档介绍。

6.5有没有尝试进行多机redis 的部署?如何保证数据一致的?

主从复制,读写分离
一类是主数据库(master)一类是从数据库(slave),主数据库可以进行读写操作,当发生写操作的时候自动将数据同步到从数据库,而从数据库一般是只读的,并接收主数据库同步过来的数据,一个主数据库可以有多个从数据库,而一个从数据库只能有一个主数据库。

7.HTTP、 HTTPS、TCP/IP、 三次握手四次挥手过程

阿里面试: HTTP、 HTTPS、TCP/IP、 三次握手四次挥手过程?:

https://www.bilibili.com/video/BV1KZ4y1H7sY/?spm_id_from=333.788.videocard.3

拥塞控制

https://www.bilibili.com/video/BV19g4y1i7sf?from=search&seid=3592946296406781169

Java开发校招面经_第8张图片

Java开发校招面经_第9张图片

Socket 传输的特点:

优点

1) 传输数据为字节级,传输数据可自定义,数据量小(对于手机应用讲:费用低)

2)传输数据时间短,性能高

3)适合于客户端和服务器端之间信息实时交互

4)可以加密,数据安全性强

Java开发校招面经_第10张图片

Java开发校招面经_第11张图片

TCP三次握手协议

客户端:给服务端发送SYN请求

服务端:接收客户端SYN请求,返回SYN-ACK请求

客户端:接收服务器SYN-ACK请求,返回ACK请求

TCP四次挥手协议

客户端:发送FIN请求给服务端,并且进入FIN_WAIT_1状态

服务器:收到FIN请求后,发送ACK给客户端

客户端:收到ACK请求后进入FIN_WAIT_2状态,等待服务器发来FIN请求

服务器:发送ACK给客户端后,一段时间后,发送FIN给客户端

客户端:收到FIN请求后进入FIN_WAIT状态,等待一段时间后关闭连接

TCP协议中报文SYN、ACK、FIN、RST、PSH、URG详解

1、 SYN:同步连接序号,TCP SYN报文就是把这个标志设置为1,来请求建立连接;
2、 ACK:请求/应答状态。0为请求,1为应答;
3、 FIN:结束连线。如果FIN为0是结束连线请求,FIN为1表示结束连线;
4、 RST:连线复位,首先断开连接,然后重建;
5、 PSH:通知协议栈尽快把TCP数据提交给上层程序处理。
6、 URG : 紧急标志置位。

我们先来看看 TCP 三次握手是怎样的。

Java开发校招面经_第12张图片

第一次握手丢失了,会发生什么?

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。

在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制。

不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。

当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?

在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍

当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。

所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。

第二次握手丢失了,会发生什么?

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手的确认报文;
  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;

所以,如果第二次握手丢了,就会发送比较有意思的事情,具体会怎么样呢?

因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文

然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。

那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定。;
  • 服务端会重传 SYN-AKC 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。

第三次握手丢失了,会发生什么?

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。

因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文

TCP 四次挥手期间的异常

我们再来看看 TCP 四次挥手的过程。

Java开发校招面经_第13张图片

第一次挥手丢失了,会发生什么?

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。

正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2 状态。

如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。

当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,直接进入到 close 状态。

第二次挥手丢失了,会发生什么?

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。

对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。

这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。

第三次挥手丢失了,会发生什么?

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。

此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。

如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

第四次挥手丢失了,会发生什么?

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。

在 Linux 系统,TIME_WAIT 状态会持续 60 秒后(2MSL)才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。

如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

https与http的区别,https是如何做到安全性的**

答: 区别:
1、加密:http协议对传输的数据不进行加密;https协议对传输的数据***L安全协议进行加密,https加密需要CA签发的证书。
2、端口:http协议使用TCP的80端口;https协议使用TCP的443端口
3、网络分层模型:http可以明确是位于应用层;https是在http的基础上加上了SSL安全协议,而SSL是运输层协议,所以https是应用层和传输层的结合

https通过SSL安全协议来保障安全性。具体体现在密钥和证书验证上。
密钥
1、服务端生成一对公钥和私钥,将公钥和证书发送给客户端;
​ 2、客户端验证证书通过后生成一个对称加密的密钥,并使用服务器生成的公钥加密,发送给服务器;
​ 3、服务器使用私钥解密获得对称加密密钥。
​ 4、客户端和服务器相互发送消息认可对称加密密钥,至此加密通道建立。
​ 5、开始数据传输,在检验数据完整性的基础上,使用对称加密密钥进行加密解密。
证书验证
一般来说浏览器都内置了权威CA的根证书,客户端使用根证书的密钥对服务器发来的证书进行解密验证,若域名、有效期、签发机关等验证项符合则通过,否则认定证书无效,断开连接。

​ **另外,****目前的https基本都是使用TSL了,

操作系统

用户态和内核态的区别

Java开发校招面经_第14张图片

Java开发校招面经_第15张图片

Java开发校招面经_第16张图片

Java开发校招面经_第17张图片

中断的概念

Java开发校招面经_第18张图片

内外中断的区别

Java开发校招面经_第19张图片

Java开发校招面经_第20张图片

进程的状态

Java开发校招面经_第21张图片

Java开发校招面经_第22张图片

进程状态的转换

Java开发校招面经_第23张图片

Java开发校招面经_第24张图片

进程与线程的区别

Java开发校招面经_第25张图片

进程是最小的资源分配单位(提高系统并发量)。线程是最小的调度单位(降低不同任务间的切换代价)
• 线程在进程下行进(单纯的车厢无法运行)
• 一个进程可以包含多个线程(一辆火车可以有多个车厢)
• 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
• 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
• 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
• 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
• 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
• 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”

线程间通信的几种实现方式

首先,要短信线程间通信的模型有两种:共享内存和消息传递,以下方式都是基本这两种模型来实现的。
方式一:使用 volatile 关键字
基于 volatile 关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务。这也是最简单的一种实现方式
2. 使用 ReentrantLock 结合 Condition

Java开发校招面经_第26张图片

进程间通信的几种方式

Java开发校招面经_第27张图片

1.管道

管道分为有名管道和无名管道

无名管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系一般指的是父子关系。无明管道一般用于两个不同进程之间的通信。当一个进程创建了一个管道,并调用fork创建自己的一个子进程后,父进程关闭读管道端,子进程关闭写管道端,这样提供了两个进程之间数据流动的一种方式。

有名管道也是一种半双工的通信方式,但是它允许无亲缘关系进程间的通信。

2.信号量

信号量是一个计数器,可以用来控制多个线程对共享资源的访问.,它不是用于交换大批数据,而用于多线程之间的同步.它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源.因此,主要作为进程间以及同一个进程内不同线程之间的同步手段.

Linux提供了一组精心设计的信号量接口来对信号进行操作,它们不只是针对二进制信号量,下面将会对这些函数进行介绍,但请注意,这些函数都是用来对成组的信号量值进行操作的。它们声明在头文件sys/sem.h中。

semget函数

它的作用是创建一个新信号量或取得一个已有信号量

semop函数

它的作用是改变信号量的值

semctl函数

该函数用来直接控制信号量信息

3.信号

信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生.

4.消息队列

消息队列是消息的链表,存放在内核中并由消息队列标识符标识.消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点.消息队列是UNIX下不同进程之间可实现共享资源的一种机制,UNIX允许不同进程将格式化的数据流以消息队列形式发送给任意进程.对消息队列具有操作权限的进程都可以使用msget完成对消息队列的操作控制.通过使用消息类型,进程可以按任何顺序读信息,或为消息安排优先级顺序.

5.共享内存

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问.共享内存是最快的IPC(进程间通信)方式,它是针对其它进程间通信方式运行效率低而专门设计的.它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步与通信.

6.套接字

socket,即套接字是一种通信机制,凭借这种机制,客户/服务器(即要进行通信的进程)系统的开发工作既可以在本地单机上进行,也可以跨网络进行。也就是说它可以让不在同一台计算机但通过网络连接计算机上的进程进行通信。也因为这样,套接字明确地将客户端和服务器区分开来。

套接字的特性由3个属性确定,它们分别是:域、类型和协议。

可用于不同及其间的进程通信

进程的调度算法有哪些?

调度算法是指:根据系统的资源分配策略所规定的资源分配算法。常用的调度算法有:先来先服务调度算法、时间片轮转调度法、短作业优先调度算法、最短剩余时间优先、高响应比优先调度算法、优先级调度算法等等。

先来先服务调度算法

先来先服务调度算法是一种最简单的调度算法,也称为先进先出或严格排队方案。当每个进程就绪后,它加入就绪队列。当前正运行的进程停止执行,选择在就绪队列中存在时间最长的进程运行。该算法既可以用于作业调度,也可以用于进程调度。先来先去服务比较适合于常作业(进程),而不利于段作业(进程)。

时间片轮转调度算法

时间片轮转调度算法主要适用于分时系统。在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,即先来先服务的原则,但仅能运行一个时间片。

短作业优先调度算法

短作业优先调度算法是指对短作业优先调度的算法,从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。 短作业优先调度算法是一个非抢占策略,他的原则是下一次选择预计处理时间最短的进程,因此短进程将会越过长作业,跳至队列头。

最短剩余时间优先调度算法

最短剩余时间是针对最短进程优先增加了抢占机制的版本。在这种情况下,进程调度总是选择预期剩余时间最短的进程。当一个进程加入到就绪队列时,他可能比当前运行的进程具有更短的剩余时间,因此只要新进程就绪,调度程序就能可能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。

高响应比优先调度算法

image-20220120141939565

高响应比优先调度算法主要用于作业调度,该算法是对 先来先服务调度算法和短作业优先调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。

优先级调度算法

优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。

堆和栈的区别

1、堆栈空间分配

栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

2、堆栈缓存方式

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

3、效率比较

栈由系统自动分配,速度较快。但程序员是无法控制的。

堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

4、存储内容

栈: 在函数调用时,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向函数的返回地址,也就是主函数中的下一条指令的地址,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

线程上下文切换和进程上下文切换的区别

进程切换分两步
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文。

对于linux来说,线程和进程的最大区别就在于地址空间。
对于线程切换,第1步是不需要做的,第2是进程和线程切换都要做的。所以明显是进程切换代价大

线程上下文切换和进程上下问切换一个最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是不同的。这两种上下文切换的处理都是通过操作系统内核来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是当你改变虚拟内存空间的时候,处理的页表缓冲(processor’s Translation Lookaside Buffer (TLB))或者相当的神马东西会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题。

Java开发校招面经_第28张图片

Java开发校招面经_第29张图片

死锁四个必要条件

当然死锁的产生是必须要满足一些特定条件的:
1.互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放
2.请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
3.不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用
4.循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

分页和分段

Java开发校招面经_第30张图片

Java开发校招面经_第31张图片

多级页表

Java开发校招面经_第32张图片

Java开发校招面经_第33张图片

Java开发校招面经_第34张图片

Java开发校招面经_第35张图片

CPU 的寻址方式

寻址方式就是处理器根据指令中给出的地址信息来寻找有效地址的方式,是确定本条指令的数据地址以及下一条要执行的指令地址的方法。在存储器中,操作数或指令字写入或读出的方式,有地址指定方式、相联存储方式和堆栈存取方式。几乎所有的计算机,在内存中都采用地址指定方式。当采用地址指定方式时,形成操作数或指令地址的方式称为寻址方式。寻址方式分为两类,即指令寻址方式和数据寻址方式,前者比较简单,后者比较复杂。值得注意的是,在传统方式设计的计算机中,内存中指令的寻址与数据的寻址是交替进行的。

一、指令寻址

指令的寻址方式有以下两种:

  • 顺序寻址

    由于指令地址在内存中按顺序安排,当执行一段程序时,通常是一条指令接一条指令地顺序进行。也就是说,从存储器取出第1条指令,然后执行这条指令;接着从存储器取出第2条指令,再执行第二条指令;接着再取出第3条指令。这种程序顺序执行的过程,称为指令的顺序寻址方式。为此,必须使用PC来计数指令的顺序号,该顺序号就是指令在内存中的地址。

  • 跳跃寻址

    当程序转移执行的顺序时,指令的寻址就采取跳跃寻址方式。所谓跳跃,是指下条指令的地址码不是由程序计数器给出,而是由本条指令给出。注意,程序跳跃后,按新的指令地址开始顺序执行。因此,程序计数器的内容也必须相应改变,以便及时跟踪新的指令地址。采用指令跳跃寻址方式,可以实现程序转移或构成循环程序,从而能缩短程序长度,或将某些程序作为公共程序引用。指令系统中的各种条件转移或无条件转移指令,就是为了实现指令的跳跃寻址而设置的。注意是否跳跃可能受到状态寄存器的操作数的控制,而跳跃到的地址分为绝对地址(由标记符直接得到)和相对地址(对于当前指令地址的偏移量),跳跃的结果是当前指令修改PC的值,所以下一条指令仍是通过PC给出。

二、操作数寻址

形成操作数的有效地址的方法称为操作数的寻址方式。由于大型机、小型机、微型机和单片机结构不同,从而形成了各种不同的操作数寻址方式。下面介绍一些比较典型又常用的操作数寻址方式。

  • 隐含寻址
    这种类型的指令,不是明显地给出操作数的地址。而是在指令中隐含着操作数的地址。例如,单地址的指令格式,就不明显地在地址字段中指出第2操作数的地址,而是规定累加寄存器AC作为第2操作数地址。指令格式明显指出的仅是第1操作数的地址D。因此,累加寄存器AC对单地址指令格式来说是隐含地址。
  • 立即寻址
    指令的地址字段指出的不是操作数的地址,而是操作数本身,这种寻址方式称为立即寻址。立即寻址方式的特点是指令执行时间很短,因为它不需要访问内存取数,从而节省了访问内存的时间。 如:MOV AX,#5678H 注意:立即数只能作为源操作数,不能作为目的操作数。
  • 直接寻址
    直接寻址是一种基本的寻址方法,其特点是:在指令格式的地址的字段中直接指出操作数在内存的地址。由于操作数的地址直接给出而不需要经过某种变换,所以称这种寻址方式为直接寻址方式。在指令中直接给出参与运算的操作数及运算结果所存放的主存地址,即在指令中直接给出有效地址。
  • 间接寻址
    间接寻址是相对直接寻址而言的,在间接寻址的情况下,指令地址字段中的形式地址不是操作数的真正地址,而是操作数地址的指示器,或者说此形式地址单元的内容才是操作数的有效地址。
  • 寄存器寻址方式和寄存器间接寻址方式
    当操作数不放在内存中,而是放在CPU的通用寄存器中时,可采用寄存器寻址方式。显然,此时指令中给出的操作数地址不是内存的地址单元号,而是通用寄存器的编号(可以是8位也可以是16位(AX,BX,CX,DX))。指令结构中的RR型指令,就是采用寄存器寻址方式的例子。如:MOV DS,AX寄存器间接寻址方式与寄存器寻址方式的区别在于:指令格式中的寄存器内容不是操作数,而是操作数的地址,该地址指明的操作数在内存中。
  • 相对寻址
    相对寻址是把程序计数器PC的内容加上指令格式中的形式地址D而形成操作数的有效地址。程序计数器的内容就是当前指令的地址。“相对”寻址,就是相对于当前的指令地址而言。采用相对寻址方式的好处是程序员无须用指令的绝对地址编程,因而所编程序可以放在内存的任何地方。令格式:MOV AX,[BX+1200H] 操作数物理地址PA=(DS/SS)*16H+EA EA=(BX/BP/SI/DI)+(6/8)位偏移量Disp 对于BX,SI,DI寄存器来说段寄存器默认为DS,对于BP来说,段寄存器默认为SS
  • 基址寻址
    在基址寻址方式中将CPU中的基址寄存器的内容,加上变址寄存器的内容而形成操作数的有效地址。基址寻址的优点是可以扩大寻址能力,因为与形式地址相比,基址寄存器的位数可以设置得很长,从而可以在较大的存储空间中寻址。
  • 变址寻址
    变址寻址方式与基址寻址方式计算有效地址的方法很相似,它把CPU中某个变址寄存器的内容与偏移量D相加来形成操作数有效地址。但使用变址寻址方式的目的不在于扩大寻址空间,而在于实现程序块的规律变化。为此,必须使变址寄存器的内容实现有规律的变化(如自增1、自减1、乘比例系数)而不改变指令本身,从而使有效地址按变址寄存器的内容实现有规律的变化。

8.JWT

JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

  • 简洁(Compact)

    可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

  • 自包含(Self-contained)

    负载中包含了所有用户所需要的信息,避免了多次查询数据库

JWT 组成

Java开发校招面经_第36张图片

  • Header 头部

头部包含了两部分,token 类型和采用的加密算法

{
  "alg": "HS256",
  "typ": "JWT"
}

它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。

Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

  • Payload 负载

这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。

{
    "iss": "lion1ou JWT",
    "iat": 1441593502,
    "exp": 1441594722,
    "aud": "www.example.com",
    "sub": "[email protected]"
}

同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

  • Signature 签名

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9`.`eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ`.`PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

  • 签名的目的

最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

  • 信息暴露

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

JWT 使用

Java开发校招面经_第37张图片

  1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。
  3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)
  5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

和Session方式存储id的差异

Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

  • 单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.comnv.taobao.comnz.taobao.comlogin.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

总结

JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。

1、jwt和security的区别

  • jwt ( json web token)优点:

无状态,json格式简单,不需要在服务端存储

  • 缺点

一旦创建无法销毁或者修改状态,没有办法做权限 控制

  • security优点:

和spring无缝结合,可以对用户状态坐控制,可以做权限控制

  • 缺点

用起来比较复杂,权限控制需要一连串过滤连,消耗服务器内存空间

2、如何销毁jwt

生成token时,有效期设置短一些

可以和redis结合使用,退出时销毁redis的token。判断redis的token即可

还可以添加黑名单功能,对其进行拦截

3、如果token有效期过短,如何保持会话

token要到期时,在重新生成一份token

4、如何防止cookie被篡改

可以设置cokkie的状态为httponly

可以在请求中添加防盗链

可以把token存放在请求头中或者local storage中,由于无状态的token过长,很容易达到4kb

由于容易被篡改,payload中不要放用户重要信息,如密码

5、如果用户禁用cookie怎么办

普通用户一般不知道cookie,也不会禁用,如何禁用我们对其友好提示,让其开启

6、如何防止token异地登陆

token是没办法做异地的判断的,这时我们可以采取传统的session方式

7、你们怎么做权限控制的

我们有一套权限控制服务,保存用户的角色和路径信息、当用户发起请求的时候,通过网关的的全局pre过滤器拦截所有请求,获取请求信息,解析jwt参数,根据参数获取用户,根据用户明去查询数据库的用户是否存在,以及当前用户有无访问此地址的权限,没有即拦截

8、你们微服务地址对外暴漏了怎么办

首先因为用户访问的时候走的是我们nginx的代理,微服务对外暴漏的可能性很低,即使暴漏了,我们每个微服务也有一套的鉴权校验策略;首先有一张表存每个微服务的id,密钥;服务器启动的时候使用id和密钥去授权中心生成jwt令牌,去访问其他的时候访问携带令牌

9、如何防止token被篡改

jwt使用了header和payload参数来校验是否被篡改,header存放算法类型和类型是jwt方式的参数,payload是存放用户自定义的参数,还有签名结合来确认token是否被篡改

二,大数据

1.hadoop

1.1 HDFS架构

Java开发校招面经_第38张图片

HDFS:Namenode,Datanode,SecondaryNamenode

Client:请求读写操作

Namenode:metadata元数据管理协调工作(单点问题 ==》HA)

Datanode:存储数据,一般datanode有多个

Java开发校招面经_第39张图片

HDFS读流程

image-20220201181229896

  1. client跟namenode通信查询元数据,namenode通过查询元数据,找到文件块所在的datanode服务器
  2. 挑选一台datanode(就近原则,然后随机)服务器,请求建立socket流
  3. datanode开始发送数据(从磁盘里面读取数据放入流,以packet为单位来做校验,大小为64k)
  4. 客户端以packet为单位接收,现在本地缓存,然后写入目标文件

HDFS写流程

Java开发校招面经_第40张图片

  1. 客户端跟namenode通信请求上传文件,namenode检查目标文件是否已存在,父目录是否存在,用户是否有权限等
  2. namenode返回是否可以上传
  3. client请求第一个 block该传输到哪些datanode服务器上
  4. namenode返回3个datanode服务器ABC
  5. client请求3台dn中的一台A上传数据(本质上是一个RPC调用,建立pipeline),A收到请求会继续调用B,然后B调用C,将整个pipeline建立完成,逐级返回客户端
  6. client开始往A上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,A收到一个packet就会传给B,B传给C;A每传一个packet会放入一个应答队列等待应答
  7. 当一个block传输完成之后,client再次请求namenode上传第二个block的服务器。

1.2 HDFS High Availability 架构

Java开发校招面经_第41张图片

HA不可用问题:小文件过多,导致性能很差,耗费很长时间去找元数据

1.什么是hadoop

Hadoop 是一个开源软件框架,用于存储大量数据,并发处理/查询在具有多个商用硬件(即低成本硬件)节点的集群上的那些数据。

HDFS(HadoopDistributed File System,Hadoop 分布式文件系统):HDFS 允许你以一种分布式和冗余的方式存储大量数据。例如,1 GB(即 1024 MB)文本文件可以拆分为 16 * 128MB 文件,并存储在 Hadoop 集群中的 8 个不同节点上。每个分裂可以复制 3 次,以实现容错,以便如果 1 个节点故障的话,也有备份。HDFS 适用于顺序的“一次写入、多次读取”的类型访问。

MapReduce:一个计算框架。它以分布式和并行的方式处理大量的数据。当你对所有年龄>18 的用户在上述 1 GB 文件上执行查询时,将会有“8 个映射”函数并行运行,以在其 128 MB 拆分文件中提取年龄> 18 的用户,然后“reduce”函数将运行以将所有单独的输出组合成单个最终结果。

2.请列出正常工作的hadoop集群中hadoop都需要启动哪些进程,他们的作用分别是什么?

  1. NameNode: HDFS的守护进程,负责记录文件是如何分割成数据块,以及这些数据块分别被存储到那些数据节点上,它的主要功能是对内存及IO进行集中管理

  2. Secondary NameNode:辅助后台程序,与NameNode进行通信,以便定期保存HDFS元数据的快照。

  3. DataNode:负责把HDFS数据块读写到本地的文件系统。

  4. JobTracker:负责分配task,并监控所有运行的task。

  5. TaskTracker:负责执行具体的task,并与JobTracker进行交互。

3.请列出你所知道的hadoop调度器,并简要说明其工作方法?

比较流行的三种调度器有:默认调度器FIFO,计算能力调度器CapacityScheduler,公平调度器Fair Scheduler

  1. 默认调度器FIFO

hadoop中默认的调度器,采用先进先出的原则

  1. 计算能力调度器CapacityScheduler

选择占用资源小,优先级高的先执行

  1. 公平调度器FairScheduler

同一队列中的作业公平共享队列中所有资源

4.简答说一下hadoop的map-reduce编程模型

首先map task会从本地文件系统读取数据,转换成key-value形式的键值对集合

使用的是hadoop内置的数据类型,比如longwritable、text等

将键值对集合输入mapper进行业务处理过程,将其转换成需要的key-value在输出

之后会进行一个partition分区操作,默认使用的是hashpartitioner,可以通过重写hashpartitioner的getpartition方法来自定义分区规则

之后会对key进行进行sort排序,grouping分组操作将相同key的value合并分组输出,在这里可以使用自定义的数据类型,重写WritableComparator的Comparator方法来自定义排序规则,重写RawComparator的compara方法来自定义分组规则

之后进行一个combiner归约操作,其实就是一个本地段的reduce预处理,以减小后面shufle和reducer的工作量

reduce task会通过网络将各个数据收集进行reduce处理,最后将数据保存或者显示,结束整个job

5、为什么要用flume导入hdfs,hdfs的构架是怎样的

flume可以实时的导入数据到hdfs中,当hdfs上的文件达到一个指定大小的时候会形成一个文件,或者超过指定时间的话也形成一个文件

文件都是存储在datanode上面的,namenode记录着datanode的元数据信息,而namenode的元数据信息是存在内存中的,所以当文件切片很小或者很多的时候会卡死

6、map-reduce程序运行的时候会有什么比较常见的问题

比如说作业中大部分都完成了,但是总有几个reduce一直在运行

这是因为这几个reduce中的处理的数据要远远大于其他的reduce,可能是因为对键值对任务划分的不均匀造成的数据倾斜

解决的方法可以在分区的时候重新定义分区规则对于value数据很多的key可以进行拆分、均匀打散等处理,或者是在map端的combiner中进行数据预处理的操作

7、Hive中存放是什么?

表存的是和hdfs的映射关系,hive是逻辑上的数据仓库,实际操作的都是hdfs上的文件,HQL就是用sql语法来写的mr程序。

8、Hive与关系型数据库的关系?

没有关系,hive是数据仓库,不能和数据库一样进行实时的CURD操作。
是一次写入多次读取的操作,可以看成是ETL工具。

9、Sqoop工作原理是什么?

hadoop生态圈上的数据传输工具。
可以将关系型数据库的数据导入非结构化的hdfs、hive或者bbase中,也可以将hdfs中的数据导出到关系型数据库或者文本文件中。
使用的是mr程序来执行任务,使用jdbc和关系型数据库进行交互。
import原理:通过指定的分隔符进行数据切分,将分片传入各个map中,在map任务中在每行数据进行写入处理没有reduce。
export原理:根据要操作的表名生成一个java类,并读取其元数据信息和分隔符对非结构化的数据进行匹配,多个map作业同时执行写入关系型数据库

10、Hadoop性能调优?

调优可以通过系统配置、程序编写和作业调度算法来进行。
hdfs的block.size可以调到128/256(网络很好的情况下,默认为64)
调优的大头:mapred.map.tasks、mapred.reduce.tasks设置mr任务数(默认都是1)
mapred.tasktracker.map.tasks.maximum每台机器上的最大map任务数
mapred.tasktracker.reduce.tasks.maximum每台机器上的最大reduce任务数
mapred.reduce.slowstart.completed.maps配置reduce任务在map任务完成到百分之几的时候开始进入
这个几个参数要看实际节点的情况进行配置,reduce任务是在33%的时候完成copy,要在这之前完成map任务,(map可以提前完成)
mapred.compress.map.output,mapred.output.compress配置压缩项,消耗cpu提升网络和磁盘io
合理利用combiner
注意重用writable对象

11、Spark Streaming和Storm有何区别?

一个实时毫秒一个准实时亚秒,不过storm的吞吐率比较低。

2.spark(代完成)

2.1spark的运行原理

Java开发校招面经_第42张图片

Worker的功能:定时和master通信;调度并管理自身的executor

executor:由Worker启动的,程序最终在executor中运行,(程序运行的一个容器)

spark-submit命令执行时,会根据master地址去向 Master发送请求,

Master接收到Dirver端的任务请求之后,根据任务的请求资源进行调度,(打散的策略),尽可能的 把任务资源平均分配,然后向WOrker发送指令

Worker收到Master的指令之后,就根据相应的资源,启动executor(cores,memory)

executor会向dirver端建立请求,通知driver,任务已经可以运行了

driver运行任务的时候,会把任务发送到executor中去运行。

kafka

Kafka 架构中的一般概念:

  • Producer: 生产者,也就是发送消息的一方。生产者负责创建消息,然后将其发送到 Kafka。
  • Consumer: 消费者,也就是接受消息的一方。消费者连接到 Kafka 上并接收消息,进而进行相应的业务逻辑处理。
  • Consumer Group: 一个消费者组可以包含一个或多个消费者。使用多分区+多消费者方式可以极大提高数据下游的处理速度。 同一消费组中的消费者不会重复消费消息,同样的,不同消费组中的消费者消息消息时互不影响。Kafka 就是通过消费组的方式来实现消息 P2P 模式和广播模式。
  • Broker: 服务代理节点。Broker 是 Kafka 的服务节点,即 Kafka 的服务器。
  • Partition: Topic 是一个逻辑的概念,它可以细分为多个分区,每个分区只属于单个主题。 同一个主题下不同分区包含的消息是不同的,分区在存储层面可以看作一个可追加的日志(Log)文件,消息在被追加到分区日志文件的时候都会分配一个特定的偏移量(Offset)。
  • Offset: Offset 是消息在分区中的唯一标识,Kafka 通过它来保证消息在分区内的顺序性,不过 Offset 并不跨越分区,也就是说,Kafka 保证的是分区有序性而不是主题有序性。
  • Replication: 副本,是 Kafka 保证数据高可用的方式,Kafka 同一 Partition 的数据可以在多 Broker 上存在多个副本,通常只有主副本对外提供读写服务,当主副本所在 Broker 崩溃或发生网络一场,Kafka 会在 Controller 的管理下会重新选择新的 Leader 副本对外提供读写服务。
  • Record: 实际写入 Kafka 中并可以被读取的消息记录。每个 Record 包含了 key、value 和 timestamp。

三,数据结构与算法

数据结构:数组、链表、栈、队列的理解

数组、链表、栈和队列是最基本的数据结构,任何程序语言都会涉及到其中的一种或多种。

数组

数组是数据结构中很基本的结构,很多编程语言都内置数组。

在java中当创建数组时会在内存中划分出一块连续的内存,然后当有数据进入的时候会将数据按顺序的存储在这块连续的内存中。当需要读取数组中的数据时,需要提供数组中的索引,然后数组根据索引将内存中的数据取出来,返回给读取程序。在Java中并不是所有的数据都能存储到数组中,只有相同类型的数据才可以一起存储到数组中。

img

所有的数据结构都支持几个基本操作:读取、插入、删除。

因为数组在存储数据时是按顺序存储的,存储数据的内存也是连续的,所以他的特点就是寻址读取数据比较容易,插入和删除比较困难。简单解释一下为什么,在读取数据时,只需要告诉数组要从哪个位置(索引)取数据就可以了,数组会直接把你想要的位置的数据取出来给你。插入和删除比较困难是因为这些存储数据的内存是连续的,要插入和删除就需要变更整个数组中的数据的位置。举个例子:一个数组中编号0->1->2->3->4这五个内存地址中都存了数组的数据,但现在你需要往4中插入一个数据,那就代表着从4开始,后面的所有内存中的数据都要往后移一个位置。这可是很耗时的。

Java开发校招面经_第43张图片

链表

在java中创建链表的过程和创建数组的过程不同,不会先划出一块连续的内存。因为链表中的数据并不是连续的,链表在存储数据的内存中有两块区域,一块区域用来存储数据,一块区域用来记录下一个数据保存在哪里(指向下一个数据的指针)。当有数据进入链表时候,会根据指针找到下一个存储数据的位置,然后把数据保存起来,然后再指向下一个存储数据的位置。这样链表就把一些碎片空间利用起来了,虽然链表是线性表,但是并不会按线性的顺序存储数据。

img

由于链表是以这种方式保存数据,所以链表在插入和删除时比较容易,读取数据时比较麻烦。举个例子:一个链表中0->1->2->3->4这五个内存地址中都存了数据,现在需要往2中插入一条数据,那么只需要更改1号和2号中记录下一个数据的位置就行了,对其他数据没有影响。删除一条数据与插入类似,很高效。但是如果是想要在链表其中取出一条数据,就需要从0号开始一个一个的找,直到找到想要的那条数据为止。

Java开发校招面经_第44张图片

链表中插入

Java开发校招面经_第45张图片

链表中删除

栈是一种先进后出的数据结构,数组和链表都可以生成栈。当数据进入到栈时会按照规则压入到栈的底部,再次进入的数据会压在第一次的数据上面,以此类推。

在取出栈中的数据的时候会先取出最上面的数据,所以是先进后出。

Java开发校招面经_第46张图片

由于数组和链表都可以组成栈,所以操作特点就需要看栈是由数组还是链表生成的了,然后就会继承相应的操作特点。

队列

队列是一种先进先出的数据结构,数组和链表也都可以生成队列。当数据进入到队列中时也是先进入的在下面后进入的再上面,但是出队列的时候是先从下面出,然后才是上面的数据出,最晚进入的队列的,最后出。

Java开发校招面经_第47张图片

举个简单的例子:可以把栈和队列看成是两根管子,这两根管子是用来存储数据的,有可能是数组生成的也有可能是链表生成的,栈的这根管子有一头是封死的,所以像这个管子放数据只能从一个口进,拿出数据的时候也只能从这一个口拿出来。而队列这根管子呢两个口都是敞开的,一个口负责进数据,另一个口负责出数据,所以从一进口先进去的数据,在出口处会先被拿出来。

(1) 红黑树的了解(平衡树,二叉搜索树),使用场景

把数据结构上几种树集中的讨论一下:

1.AVLtree

定义:最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下都是O(log n)。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。

节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。
一般我们所看见的都是排序平衡二叉树。

AVLtree使用场景:AVL树适合用于插入删除次数比较少,但查找多的情况。插入删除导致很多的旋转,旋转是非常耗时的。AVL 在linux内核的vm area中使用。

2.二叉搜索树

二叉搜索树也是一种树,适用与一般二叉树的全部操作,但二叉搜索树能够实现数据的快速查找。

二叉搜索树满足的条件:

1.非空左子树的所有键值小于其根节点的键值
2.非空右子树的所有键值大于其根节点的键值
3.左右子树都是二叉搜索树

二叉搜索树的应用场景:如果是没有退化称为链表的二叉树,查找效率就是lgn,效率不错,但是一旦退换称为链表了,要么使用平衡二叉树,或者之后的RB树,因为链表就是线性的查找效率。

3.红黑树的定义

红黑树是一种二叉查找树,但在每个结点上增加了一个存储位表示结点的颜色,可以是RED或者BLACK。通过对任何一条从根到叶子的路径上各个着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。

当二叉查找树的高度较低时,这些操作执行的比较快,但是当树的高度较高时,这些操作的性能可能不比用链表好。红黑树(red-black tree)是一种平衡的二叉查找树,它能保证在最坏情况下,基本的动态操作集合运行时间为O(lgn)。

红黑树必须要满足的五条性质:

性质一:节点是红色或者是黑色; 在树里面的节点不是红色的就是黑色的,没有其他颜色,要不怎么叫红黑树呢,是吧。

性质二:根节点是黑色; 根节点总是黑色的。它不能为红。

性质三:每个叶节点(NIL或空节点)是黑色;

性质四:每个红色节点的两个子节点都是黑色的(也就是说不存在两个连续的红色节点); 就是连续的两个节点不能是连续的红色,连续的两个节点的意思就是父节点与子节点不能是连续的红色。

性质五:从任一节点到其每个叶节点的所有路径都包含相同数目的黑色节点。从根节点到每一个NIL节点的路径中,都包含了相同数量的黑色节点。

红黑树的应用场景:红黑树是一种不是非常严格的平衡二叉树,没有AVLtree那么严格的平衡要求,所以它的平均查找,增添删除效率都还不错。广泛用在C++的STL中。如map和set都是用红黑树实现的。

4.B树定义

B树和平衡二叉树稍有不同的是B树属于多叉树又名平衡多路查找树(查找路径不只两个),不属于二叉搜索树的范畴,因为它不止两路,存在多路。

B树满足的条件:

(1)树种的每个节点最多拥有m个子节点且m>=2,空树除外(注:m阶代表一个树节点最多有多少个查找路径,m阶=m路,当m=2则是2叉树,m=3则是3叉);
(2)除根节点外每个节点的关键字数量大于等于ceil(m/2)-1个小于等于m-1个,非根节点关键字数必须>=2;(注:ceil()是个朝正无穷方向取整的函数 如ceil(1.1)结果为2)
(3)所有叶子节点均在同一层、叶子节点除了包含了关键字和关键字记录的指针外也有指向其子节点的指针只不过其指针地址都为null对应下图最后一层节点的空格子
(4)如果一个非叶节点有N个子节点,则该节点的关键字数等于N-1;
(5)所有节点关键字是按递增次序排列,并遵循左小右大原则;
Java开发校招面经_第48张图片
B树的应用场景:构造一个多阶的B类树,然后在尽量多的在结点上存储相关的信息,保证层数尽量的少,以便后面我们可以更快的找到信息,磁盘的I/O操作也少一些,而且B类树是平衡树,每个结点到叶子结点的高度都是相同,这也保证了每个查询是稳定的。

5.B+树

B+树是B树的一个升级版,B+树是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。

相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。为什么说B+树查找的效率要比B树更高、更稳定;我们先看看两者的区别

(1)B+跟B树不同,B+树的非叶子节点不保存关键字记录的指针,这样使得B+树每个节点所能保存的关键字大大增加;
(2)B+树叶子节点保存了父节点的所有关键字和关键字记录的指针,每个叶子节点的关键字从小到大链接;
(3)B+树的根节点关键字数量和其子节点个数相等;
(4)B+的非叶子节点只进行数据索引,不会存实际的关键字记录的指针,所有数据地址必须要到叶子节点才能获取到,所以每次数据查询的次数都一样;

Java开发校招面经_第49张图片

特点:
在B树的基础上每个节点存储的关键字数更多,树的层级更少所以查询数据更快,所有指关键字指针都存在叶子节点,所以每次查找的次数都相同所以查询速度更稳定;

应用场景: 用在磁盘文件组织 数据索引和数据库索引。

6.Trie树(字典树)

trie,又称前缀树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一个特定的整数。Trie 可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。
键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示 trie 的原理。

Java开发校招面经_第50张图片

trie树的优点:利用字符串的公共前缀来节约存储空间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。缺点:Trie树是一种比较简单的数据结构.理解起来比较简单,正所谓简单的东西也得付出代价.故Trie树也有它的缺点,Trie树的内存消耗非常大.

其基本性质可以归纳为:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同。

典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。字典树与字典很相似,当你要查一个单词是不是在字典树中,首先看单词的第一个字母是不是在字典的第一层,如果不在,说明字典树里没有该单词,如果在就在该字母的孩子节点里找是不是有单词的第二个字母,没有说明没有该单词,有的话用同样的方法继续查找.字典树不仅可以用来储存字母,也可以储存数字等其它数据。

冒泡排序(Bubble Sort)

算法描述

1.比较相邻的元素,如果前一个比后一个大,就把它们两个对调位置。

2.对排序数组中每一对相邻元素做同样的工作,直到全部完成,此时最后的元素将会是本轮排序中最大的数。

3.对剩下的元素继续重复以上的步骤,直到没有任何一个元素需要比较。

代码

void Swap(int arr[], int i, int j)
{
    int temp=arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

/**
* 冒泡排序
* 最差时间复杂度 O(n^2)
* 最优时间复杂度 O(n)
* 平均时间复杂度 O(n^2)
* 空间复杂度    O(1)
* 稳定性       稳定
* 效率         对于少数元素之外的数列排序是很没有效率的
* @param arr 数组
* @param n 长度
*/
void BubbleSort(int arr[], int n) {
    for (int i=0;i<n-1;i++)//每次最大元素就像气泡一样浮到数组的最后
    {
        for(int j=0;j<n-1-i;j++) //依次比较相邻的两个元素,使较大的那个向后移
        {
            if(arr[j]>arr[j+1]) //如果条件改为arr[j] >= arr[j+1] 则变为不稳定的排序算法
            {
                Swap(arr, j, j + 1);
            }
        }
    }
}


使用冒泡排序为一列数字进行排序的过程如右图所示:
Java开发校招面经_第51张图片

冒泡排序的改进:鸡尾酒排序

此算法与冒泡排序的不同处在于从低到高然后从高到低,而冒泡排序则仅从低到高去比较数组序列里的每个元素。它可以得到比冒泡排序稍微好一点的效能。

代码

void Swap(int arr[], int i, int j)
{
    int temp=arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

/**
* 冒泡排序
* 最差时间复杂度 O(n^2)
* 最优时间复杂度 O(n)
* 平均时间复杂度 O(n^2)
* 空间复杂度    O(1)
* 稳定性       稳定
* 效率         对于少数元素之外的数列排序是很没有效率的
* @param arr 数组
* @param n 长度
*/
void BubbleSort(int arr[], int n) {
    for (int i=0;iarr[j+1]) //如果条件改为arr[j] >= arr[j+1] 则变为不稳定的排序算法
            {
                Swap(arr, j, j + 1);
            }
        }
    }
}


使用鸡尾酒排序为一列数字进行排序的过程如下图所示:
Java开发校招面经_第52张图片

插入排序(Insertion Sort)

算法描述

1.从第一个元素开始,即用该元素初始化已排序序列。
2.取出下一个元素,在已经排序的元素序列中从后向前扫描。
3.如果该元素(已排序)大于新元素,则将该元素移到下一个位置。
4.重复步骤3,直到找到已经排序的元素小于或者等于新元素的位置。
5.将新元素插入到该位置
6.重复步骤2~5,直到完成所有数据排序。

代码

/**
* 插入排序
* 最差时间复杂度 当输入序列是降序排列的,此时时间复杂度O(n^2)
* 最优时间复杂度 当输入序列是升序排列的,此时时间复杂度O(n)
* 平均时间复杂度 O(n^2)
* 空间复杂度     O(1)
* 稳定性         稳定
* 效率
* @param arr 数组
* @param n 长度
*/
void InsertionSort(int arr[],int n)
{
    for (int i = 1; i < n; i++)
    {
        int get = arr[i];  //右手抓的牌
        int j = i - 1;     //已排序好的最右端
        while (j >= 0 && arr[j] > get) {
            arr[j + 1] = arr[j]; // 如果该手牌比抓到的牌大,就将其右移
            j--;
        }
        arr[j + 1] = get;//将右手抓的牌插入到左手序列中
    }
    print(arr,n);
}

元素序列{3,44,38,5,47,15,36,26,27,2,46,4,19,50,48}的插入排序动态流程如下:

Java开发校招面经_第53张图片

希尔排序(Shell Sort)

https://www.bilibili.com/video/BV1RW411U7wR?from=search&seid=16756739163147622477

算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  1. 选择一个增量序列t1,t2,ti, tj …,1,其中ti>tj
  2. 按增量序列个数k,对序列进行k 趟排序;
  3. 每趟排序,根据对应的增量,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。当增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

Java开发校招面经_第54张图片

代码

/**
 * 希尔排序  是插入排序的改进,效率较高
 * 最差时间复杂度 根据步长序列的不同而不同。已知最好的为O(n(logn)^2)
 * 最优时间复杂度 O(n)
 * 平均时间复杂度 O(nlogn)
 * 空间复杂度     O(1)
 * 稳定性         不稳定
 * 效率
 * @param arr 数组
 * @param n 长度
 */
void ShellSort(int arr[],int n)
{
    //01 生成初始增量
    int h=0;
    while (h < n) {
        h = h * 3 + 1;
    }
    if(h>1){
        h=(h-1)/3;
    }

    //02将输入序列根据增量h分割成若干个子序列,并对每个子序列进行插入排序
    //逐渐缩小增量,直到小于零
    while (h>0)
    {
        //03 对子序列进行插入排序
        for (int i = h; i < n; i++) {
            int get=arr[i];   //待插入的元素
            int j=i-h;        //
            while(j>=0 && arr[j]>get)
            {
                arr[j+h]=arr[j];
                j=j-h;       //切换到子序列中的下一个元素
            }
            arr[j+h]=get;    //将元素插入到该位置
        }
        h=(h-1)/3;   //递减增量
    }
}


选择排序(Selection Sort)

算法描述

初始时在序列中找到最小(大)元素,放到序列的起始位置作为已排序序列;
然后,再从剩余未排序元素中继续寻找最小(大)元素,放到已排序序列的末尾。
以此类推,直到所有元素均排序完毕。

选择排序C/C++代码如下

void Swap(int arr[], int i, int j)
{
    int temp=arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

 /**
  * 选择排序
  * 最差时间复杂度 O(n^2)
  * 最优时间复杂度 O(n^2)
  * 平均时间复杂度 O(n^2)
  * 空间复杂度     O(1)
  * 稳定性         不稳定
  * @param arr 数组
  * @param n 长度
  */
void SelectionSort(int arr[],int n)
{
    for (int i = 0; i < n-1; i++)//i 为已排序序列的末尾,由于排序n-1次后最后一个值必为最大值 所以只需遍历n-1次
    {
        int MinIndex=i;//记录当前选择的位置
        for (int j = i + 1; j < n; j++)//遍历未排序的序列
        {
            if (arr[j] < arr[MinIndex])
            {
                MinIndex=j;//记录较小值的索引
            }
        }

        if(i != MinIndex)
        {
            Swap(arr,i,MinIndex);//与较小值进行互换
        }
    }
}

12345678910111213141516171819202122232425262728293031323334353637

上述代码对序列{ 8, 5, 2, 6, 9, 3, 1, 4, 0, 7 }进行选择排序的实现过程如下图:
在这里插入图片描述

快速排序(Quick Sort)

https://www.bilibili.com/video/BV1at411T75o/?spm_id_from=333.788.videocard.0

算法描述

1.先从元素序列中取一个数作为基准数。
2.将比这个基准数大的元素全放到它的右边,小于或者等于基准数的元素放到它的左边。这个称为分区(partition)操作。最终的结果就是元素序列分成了如下左右两个区间:
Java开发校招面经_第55张图片
3.对左右区间递归的进行步骤1,2,直到各区间只有一个元素时结束递归操作,这时整个元素序列就已经完成排序了。

快速排序C/C++代码如下:

void Swap(int arr[], int i, int j)
{
    int temp=arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

/**
 * 根据基准值调整数组为三个区域,返回基准值所在的位置
 * @param arr
 * @param left
 * @param right
 * @return 基准值所在的位置
 */
int AdjustArray(int arr[], int left, int right)
{
    int left_index=left-1; //记录左区间最后一个元素的位置
    int pivot = arr[right]; //选择最后一个数作为基准值
    for (int i = left; i < right; i++) {
        if (arr[i] <= pivot) {
            Swap(arr, ++left_index, i); //将小于等于基准值的元素放到左区间末尾,剩下的就是大于基准值的元素自动排在右区间
        }
    }
    Swap(arr, left_index + 1, right); //将基准值与右区间的第一个元素交换,该操作可能会把后面元素的稳定性打乱,所以快速排序是不稳定的算法。
    return left_index + 1;
}
/**
 * 快速排序
 * 最差时间复杂度 当每次选取的基准都是最大或最小的元素,导致每次只划分出了一个分区,需要进行n-1次划分才能结束递归,时间复杂度为O(n^2)
 * 最优时间复杂度 每次选取的基准都是中位数,这样每次都均匀的划分出两个分区,只需要logN次划分就能结束递归,时间复杂度为O(nlogN)
 * 平均时间复杂度 O(nlogn)
 * 空间复杂度     主要是递归造成的栈空间的使用(用来保存left和right等局部变量),取决于递归树的深度,一般为O(logn),最差为O(n)
 * 稳定性         不稳定
 * @param arr
 * @param left
 * @param right
 */
void QuickSort(int arr[], int left, int right) {
    if(left==right || left>right || left<0 || right <0)return;

    int pivot_index = AdjustArray(arr, left, right); //调整基准值在元素序列中的位置
    QuickSort(arr, left, pivot_index-1);             //对左区间进行快速排序
    QuickSort(arr, pivot_index + 1, right);          //对右区间进行快速排序
}

归并排序(Merge Sort)

https://www.bilibili.com/video/BV1et411N7Ac

算法描述

把长度为n的输入序列分成两个长度为n/2的子序列;
对这两个子序列分别采用归并排序,直到子序列无法再分。
将两个排序好的子序列合并成一个最终的排序序列。

归并排序C/C++代码

/**
 * 合并操作
 * @param arr 元素数组
 * @param left 左子数组起点
 * @param mid 左子数组终点
 * @param right 右子数组终点
 */
void Merge(int arr[],int left,int mid,int right)
{
    int len=right-left+1;
    int *temp = new int[len];  //缓存临时元素序列

    int index=0;
    int left_index=left;        //保存左子序列的索引
    int right_index=mid+1;      //保存右子序列的索引

    while(left_index<=mid && right_index <= right)
    {
        temp[index]=arr[left_index]<=arr[right_index]?arr[left_index++]:arr[right_index++]; // 带等号可保证归并排序的稳定性
        index++;
    }

    while (left_index<=mid)
    {
        temp[index]=arr[left_index++];
        index++;
    }

    while (right_index<=right)
    {
        temp[index]=arr[right_index++];
        index++;
    }

    for (int i = 0; i < len; i++) {
        arr[left + i] = temp[i];
    }
}

/**
* 归并排序
* 最差时间复杂度 O(nlogn)
* 最优时间复杂度 O(nlogn)
* 平均时间复杂度 O(nlogn)
* 空间复杂度     O(n)
* 稳定性         稳定
* @param arr 数组
* @param size 长度
*/
void MergeSort(int arr[], int left,int right)
{
    if(left==right)return;        //当待排序的序列的左索引和右索引相同,即该序列的长度为1时,递归开始回溯,进行Merge操作
    int mid = (left + right) / 2; //将元素序列分为两个子序列
    MergeSort(arr, left, mid);    //对左序列进行归并排序
    MergeSort(arr, mid+1, right); //对右序列进行归并排序
    Merge(arr,left,mid,right);    //对左右序列进行合并

    //打印具体排序流程
    for(int i = left;i<=right;i++)
    {
        std::cout << arr[i] <<" ";
    }
    cout<<endl;
}


元素序列{3,44,38,5,47,15,36,26,27,2,46,4,19,50,48}的归并排序动态流程图如下:
Java开发校招面经_第56张图片

堆排序(Heap Sort)

https://www.bilibili.com/video/BV1K4411X7fq

算法描述

  1. 将待排序的元素序列(R1,R2….Rn)构建成最大堆,此堆为初始的无序区。(关于最大堆的详细构建过程请点这里)
  2. 将最大堆的堆顶元素R1(当前堆树中的最大值)与最后一个元素Rn交换。此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  3. 由于交换后新的堆顶R1可能违反了最大堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)重新调整为最大堆,然后再次将R1与无序区最后一个元素交换,得到新的无序区(R1,R2,……Rn-2)和有序区(Rn-1,Rn)。不断重复此过程直到无序区的元素个数为1;

堆排序是不稳定的排序算法,不稳定发生在堆顶元素与A[i]交换的时刻。
比如序列:{ 10, 4, 6, 4 },堆顶元素是10,堆排序下一步将10和第二个4进行交换,得到序列 { 4, 4, 6, 10 },再进行堆调整得到{ 6, 4, 4, 10 },重复之前的操作最后得到{ 4, 4, 6, 10 }从而改变了两个4的相对次序。

堆排序C/C++代码:

void Swap(int arr[], int i, int j)
{
    int temp=arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}


/**
* 将父节点为aar[i]的子树调整为最大堆
* @param arr 堆数组
* @param size 堆数组长度
* @param i 节点索引
*/
void AdjustHeap(int arr[],int size,int i)
{
    int left_child = 2*i+1;    //左子节点索引
    int right_child = 2*i+2;   //右子节点索引
    int max = i;               //选出当前结点与其左右孩子三者之中的最大值
    if (left_child < size && arr[left_child] > arr[max]) {
        max = left_child;
    }

    if (right_child < size && arr[right_child] > arr[max]) {
        max = right_child;
    }

    if (max != i) {
        Swap(arr, i, max);    //将最大值节点与父节点互换
        AdjustHeap(arr, size, max); //递归调用,继续从当前节点向下进行堆调整
    }
}

/**
* 根据输入的数组构建一个最大堆
* @param arr 堆数组
* @param size 堆数组长度
* @return 堆数组长度
*/
int BuildMaxHeap(int arr[], int size) {
    //对每一个非叶节点开始向下进行最大堆调整
    for (int i = size / 2 - 1; i >= 0; i--)
    {
        AdjustHeap(arr, size, i);
    }
    return size;
}


/**
* 堆排序
* 最差时间复杂度 O(nlogn)
* 最优时间复杂度 O(nlogn)
* 平均时间复杂度 O(nlogn)
* 空间复杂度     O(1)
* 稳定性         不稳定
* @param arr 数组
* @param size 长度
*/
void HeapSort(int arr[], int size)
{
    int heap_size = BuildMaxHeap(arr, size);
    while (heap_size > 1) {  //堆(无序区)元素个数大于1,未完成排序
        //将堆顶元素与堆的最后一个元素进行交换,并从堆中去掉最后一个元素
        //由于交换后的新的堆顶可能违反堆的性质,所以需要对该堆进行重新调整为最大堆
        //此处交换操作很有可能把后面元素的稳定性打乱,所以堆排序是不稳定的排序算法
        Swap(arr, 0, --heap_size);
        AdjustHeap(arr, heap_size, 0); // 从新的堆顶元素开始向下进行堆调整,时间复杂度O(logn)
    }
}


元素序列{ 1, 3, 4, 5, 2, 6, 9, 7, 8, 0 }的堆排序流程示意图如下:
Java开发校招面经_第57张图片
堆排序后的输出的元素序列为:{9,8,7,6,5,4,3,2,1,0}

计数排序(Counting Sort)

https://www.bilibili.com/video/BV1rJ41167CF?from=search&seid=15429910067554840159

算法描述

第一步:找出原数组中元素值最大的,记为max

第二步:创建一个新数组count,其长度是max加1,其元素默认值都为0。

第三步:遍历原数组中的元素,以原数组中的元素作为count数组的索引,以原数组中的元素出现次数作为count数组的元素值。

第四步:创建结果数组result,起始索引index

第五步:遍历count数组,找出其中元素值大于0的元素,将其对应的索引作为元素值填充到result数组中去,每处理一次,count中的该元素值减1,直到该元素值不大于0,依次处理count中剩下的元素。

第六步:返回结果数组result

Java开发校招面经_第58张图片

桶排序(Bucket Sort)重点看看

https://www.bilibili.com/video/BV1h741167Ni?from=search&seid=4261234377401736215

Java开发校招面经_第59张图片

Java开发校招面经_第60张图片

基数排序(Radix Sort)

https://www.bilibili.com/video/BV1rJ411t748?p=1

设计模式

1.单例模式

在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

Java开发校招面经_第61张图片

单例的实现主要是通过以下两个步骤

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
  2. 在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。

实际场景:

  1. windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。(我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。)
  2. 网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。
  3. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制
  4. 应用程序的日志应用
  5. web开发中读取配置文件,如HttpApplication

适用场景:

  • 1.需要生成唯一序列的环境
  • 2.需要频繁实例化然后销毁的对象。
  • 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
  • 4.方便资源相互通信的环境
优点 缺点
在内存中只有一个对象,节省内存空间; 不适用于变化频繁的对象;
避免频繁的创建销毁对象,可以提高性能; 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
避免对共享资源的多重占用,简化访问; 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

单例模式的实现

// 饿汉式单例
public class Singleton1 {
 
    // 指向自己实例的私有静态引用,主动创建
    private static Singleton1 singleton1 = new Singleton1();
 
    // 私有的构造方法
    private Singleton1(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton1 getSingleton1(){
        return singleton1;
    }
}

Java开发校招面经_第62张图片

优点 缺点
在类装载的时候就完成实例化。避免了线程同步问题。 在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
// 懒汉式单例
public class Singleton2 {
 
    // 指向自己实例的私有静态引用
    private static Singleton2 singleton2;
 
    // 私有的构造方法
    private Singleton2(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton2 getSingleton2(){
        // 被动创建,在真正需要使用时才去创建
        if (singleton2 == null) {
            singleton2 = new Singleton2();
        }
        return singleton2;
    }
}
优点 缺点
单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。 只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
//双重加锁机制
public class Singleton
    {
        private static Singleton instance;
        private Singleton() { }
        public static Singleton GetInstance()
        {
            //先判断是否存在,不存在再加锁处理
            if (instance == null)
            {
                //在同一个时刻加了锁的那部分程序只有一个线程可以进入
                synchronized (Singleton.class)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
//双重加锁机制
public class Singleton
    {
        private static Singleton instance;
        //程序运行时创建一个静态只读的进程辅助对象
        private static readonly object syncRoot = new object();
        private Singleton() { }
        public static Singleton GetInstance()
        {
            //先判断是否存在,不存在再加锁处理
            if (instance == null)
            {
                //在同一个时刻加了锁的那部分程序只有一个线程可以进入
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }

这里写图片描述

综合了懒汉式和饿汉式两者的优缺点整合而成。看上面代码实现中,特点是在synchronized关键字内外都加了一层 if 条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。

优点:线程安全;延迟加载;效率较高。

缺点:加锁耗费资源

你可能感兴趣的:(实用技巧,java,java,开发语言,后端)