在 上一篇 中,我们分享了几大互联网公司面试的题目,本文就来 详细分析面试题答案以及复习参考和整理的面试资料 ,小民同学的私藏珍品:dog:。
首先是面试题答案公布,在讲解时我们主要分成如下几块:语言的基础知识、中间件、操作系统、计算机网络、手写算法、开放题和项目经历。对面试题和涉及的知识点进行整理,这样更容易让各位同学理解。不会按照提问的顺序进行讲解,还请见谅。
其次是 Java 复习参考和整理的面试资料。由于内容比较多,学习有 道
非常重要,我们介绍一下其中的要点和目录,完整文件可以参见笔者提供的 pdf 资料。
Future 在异步编程中经常用到,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。
然而 Future 接口调用 get()方法取得处理的结果值时是 阻塞性 的,如果调用 Future 对象的 get()方法时,如果这个线程还没执行完成,就一直主线程main阻塞到此线程完成为止,就算和它同时进行的其它线程已经执行完了,也要等待这个耗时线程执行完才能获取结果,大大影响运行效率。那么使用多线程就没什么意义了。
接上一个问你题,鉴于 Future 的缺陷,JDK 1.8 并发包也提供了CompletionService接口可以解决这个问题,它的take()方法哪个线程先完成就先获取谁的 Futrue 对象。
出现 volatile,是因为多线程的场景下存在脏读。Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。变量的值何时从线程的工作内存写回主存,无法确定。
volatile关键字的作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。如以下代码片段,isShutDown被置为true后,doWork方法仍有执行。如用volatile修饰isShutDown变量,可避免此问题。
volatile只能保证变量的可见性,不能保证对volatile变量操作的原子性。
JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。
public static int value = 123; 复制代码
此时在准备阶段过后的初始值为0而不是123。
解析:把类型中的符号引用转换为直接引用。符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中;直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化:初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。
java中,对于初始化阶段,有且只有以下五种情况才会对要求类立刻“初始化”(加载,验证,准备,自然需要在此之前开始):
把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作交给虚拟机之外的类加载器来完成。这样的好处在于,我们可以自行实现类加载器来加载其他格式的类,只要是二进制字节流就行,这就大大增强了加载器灵活性。系统自带的类加载器分为三种:
双亲委派机制工作过程:
如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。
双亲委派模型的优点:java类随着它的加载器一起具备了一种带有优先级的层次关系.
例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱。
JDBC 加载机制:SPI ,全称为(Service Provider Interface) ,是JDK内置的一种服务提供发现机制;主要被框架的开发人员使用,比如java.sql.Driver接口,数据库厂商实现此接口即可,当然要想让系统知道具体实现类的存在,还需要使用固定的存放规则,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
SPI 服务机制破坏了双亲委派模型。可以看出双亲委派机制是一种至下而上的加载方式,那么SPI是如何打破这种关系?
以JDBC加载驱动为例:在JDBC4.0之后支持SPI方式加载java.sql.Driver的实现类。SPI实现方式为,通过ServiceLoader.load(Driver.class)方法,去各自实现Driver接口的lib的META-INF/services/java.sql.Driver文件里找到实现类的名字,通过Thread.currentThread().getContextClassLoader()类加载器加载实现类并返回实例。
驱动加载的过程大致如上,那么是在什么地方打破了双亲委派模型呢? 先看下如果不用Thread.currentThread().getContextClassLoader()加载器加载,整个流程会怎么样。
最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类。
因此 在父加载器加载的类中 , 去调用子加载器去加载类 :
一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在Java规范中被称为:由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照开发人员的意志进行初始化。
类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法 复制代码
策略模式是一种比较简单的模式,他的定义是:定义一组算法,将每个算法都封装起来,并且使他们之间可以互换。
Context封装角色,也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。
基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强 !Spring中AOP的有两种实现方式:JDK动态代理以及Cglib动态代理。
使用场景: 记录日志、监控方法运行时间 (监控性能)、权限控制、缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )、事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )
AOP 思想,Spring 统一异常处理有 3 种方式,分别为:
编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管是dao层、service层还是controller层,都有可能抛出异常,在springmvc中,能将所有类型的异常处理从各处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。
enum类是无法被继承的,编译器会自动把枚举用继承enum类来表示,但这一过程是由编译器完成的,枚举也不过是个语法糖。
如果一个类的实例是有限且确定的,那么可以使用枚举类。比如:季节类,只有春夏秋冬四个实例。
enum类默认被final修饰的情况下,是无法有子类的。enum本身不存在final、abstract的说法。就是不能被继承。运行时生成的class才有final、abstract的说法。
我们知道在编写自定义注解时,可以通过指定@Inherited注解,指明自定义注解是否可以被继承。但实现情况又可细分为多种。
@Inherited 只可控制 对类名上注解是否可以被继承。不能控制方法上的注解是否可以被继承。
JAVA内存结构:堆、栈、方法区;堆:存放所有 new出来的东西(堆空间是所有线程共享,虚拟机气动的时候建立);栈:存放局部变量(线程创建的时候 被创建);方法区:被虚拟机加载的类信息、常量、静态常量等。
程序计数器可以看作是当前线程所执行的字节码的行号指示器,是一块线程隔离的内存空间。在虚拟机的概念模型中,字节码解释器通过改变程序计数器的值来选取下一条执行的字节码命令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要计数器完成。每个线程都有独立的程序计数器内存空间,它们之间相互隔离、互不影响。当线程上下文进行切换时,线程独占的程序计数器也会被加载。
当线程在执行Java方法时,计数器中记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,计数器的值为空。程序计数器在Java虚拟机规范中没有规定如何的OutOfMemoryError情况的区域。
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等。每一个方法从调用到执行完成的过程,对应着一个栈帧从虚拟机栈中入栈到出栈的过程。
局部变量表中存放了编译期可知的各种基本的数据类型,对象引用和returnAddress类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配,方法运行期间不会改变局部变量表的大小。
当线程请求的栈深度大于虚拟机允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,将会抛出OutOfMemoryError异常。
本地方法栈描述虚拟机使用到的Native方法执行的内存模型,其作用与Java虚拟机栈类似,同样可能抛出StackOverflowError和OutOfMemoryError异常。
Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆作为运行时数据区域,存放着所有的类实例和数组,这是Java虚拟机规范中的规定。但是JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换等优化技术使得所有对象都在堆上分配变得不那么绝对。
Java堆是垃圾收集器管理的主要区域。从内存回收的角度来讲,现在的收集器基本都是采取分代收集算法,所以Java堆可以细分为新生代和老年代,再细致一点新生代中有Eden空间、From Survivor空间、 To Survivor空间等。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。
进一步划分的是为了更好地回收内存或者更快地分配内存。
如果在Java堆中没有内存完成实例分配,并且堆无法再扩展,将会抛出OutOfMemoryError异常。
方法区作为所有线程共享的内存区域,存储了被虚拟机加载的类信息、常量、静态变量、及时编译器编译后的代码等数据。
在HotSpot 1.8之前,HotSpot通过永久代的方式实现了方法区,GC分代收集的方式扩展到了方法区,减少了专门管理方法区内存管理代码的编写。在HotSpot 1.8中,方法区通过元数据区实现,永久代被废弃,在1.7时字符串常量池已经被迁移到堆空间中。
方法区中的内存回收目标主要是针对常量池的回收和对类型的卸载。
当方法区无法满足内存分配需求时,将会抛出OutOfMemoryError异常。
运行时常量池作为方法区的一部分存在。Class文件中的常量池用于存放编译期间生成的各种字面量和符号引用,在类加载后进入方法区的运行时常量池中存放。
运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,即在运行时也可以将新的常量放入池中(String#intern方法)。
直接内存并不是虚拟机运行时数据区的一部分。在JDK 1.4中新加入的NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样能够在一些场景中避免Java堆和Native堆中来回复制数据,提高性能。
一般来讲,本机直接内存的分配不会收到Java堆大小的限制,但是总会受到本机物理内存以及寻址空间的限制。如果各个内存区域的总和大于物理内存限制,容易导致动态扩展时出现OutOfMemoryError异常。
堆空间主要组成部分:
new出来的对象都会存放在堆内存中。新生代和老年代的存在主要用于垃圾回收机制,其中主要针对的是新生代,因为对象首先分配在eden区,在新生代回收后,如果对象还存活,则进入s0或s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,对象达到一定的年龄后,则进入老年代。
在JVM启动参数中,可以设置跟内存、垃圾回收相关的一些参数设置,默认情况不做任何设置JVM会工作的很好,但对一些配置很好的Server和具体的应用必须仔细调优才能获得最佳性能。通过设置我们希望达到一些目标:
要想GC时间小必须要一个更小的堆,要保证GC次数足够少,必须保证一个更大的堆,我们只能取其平衡。
增大堆内存(-Xms,-Xmx)会减少可创建的线程数量,增大线程栈内存(-Xss,32位系统中此参数值最小为60K)也会减少可创建的线程数量。
操作系统限制,系统最大可开线程数,主要受以下几个参数影响:
平衡树是一颗二叉搜索树,并且它的深度保持相对稳定,也就是不会退化成链的树。平衡树可以说是区间操作的数据结构中效率高的一种,它最大的用处自然是维护区间了。细分为:splay、有旋/无旋treap、AVL树、替罪羊树、二叉查找树(SBT)树等。
Redis sortedset 使用的是跳表。跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。
二叉树可以用来实现字典和有序表等抽象数据结构。在元素随机插入的场景,二叉树可以很好应对。然而,在有序插入的情况下,二叉树就退化了(链表),性能非常差。如果有办法对待插入元素进行随机排列,二叉树大概率可以运行良好。大部分情况下,插入是在线进行的,因此随机排列并不具有可行性。平衡树在操作时对树结构进行调整以满足平衡条件,因此获得理想性能。
跳表是一种概率性可行的平衡二叉树替代数据结构。跳表通过一个随机数生成器实现平衡。虽然跳表最坏情况下(worst-case)性能也很差,但是没有任何输入序列必然会导致最坏情况发生(这点类似划分元素(pivot point)随机选定的快排)。跳表极度不平衡发生的概率非常低(一个包含250个元素的字典,一次查找需要花3倍期望时间的概率小于百万分之一)。跳表平衡概率跟随机插入的二叉树差不多,好处是插入顺序不要求随机。
实现概率性平衡比严格控制平衡要简单得多。对很多应用来说,跳表用起来比平衡树更自然,而且算法更简单。跳表算法简单性意味着更容易实现,而且与平衡树和自适应树相比有常数倍数的性能提升。跳表在空间上也比较高效。平均每个元素只需要额外耗费个2指针(甚至可以配置得更低),并不需要在每个节点上都存与平衡和优先级相关的数据。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。
一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。这是就需要内存屏障来保证可见性了。
内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
从线程和进程的角度来说,进程是资源分配的最小单位,线程是独立调度的最小单位。
同一个进程中的多个线程之间可以并发执行,他们共享进程资源。 线程不拥有资源,线程可以访问隶属进程的资源,进程有自己的独立空间地址,线程没有自己的独立空间地址,但是线程有自己的堆栈和局部变量。
线程的栈、程序计数器、本地方法区也是存放在进程的地址空间上,只是这些栈、程序计数器、本地方法区都只能有某个特定的线程去访问、其他的线程访问不到。如果使用C/C++语言的话,数组越界后,很容易就访问到其他线程的栈了,以致有可能导致其他线程的异常。
多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。然而,在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。好在此时,强大的concurrent包横空出世了,而他也给我们带来了强大的BlockingQueue。(在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒)。
Tomcat是目前市场上主流Web服务器之一,是用Java语言开发的项目。Tomcat支持Servlet和JSP的规范,它由一组嵌套的层次和组件组成。所有组件都实现lifecycle生命周期方法,里面包含了init、start、stop、destroy等方法,用来控制生命周期。
Servlet是用Java编写的Server端程序,它与协议和平台无关。Servlet运行于Java-enabled Web Server中。Java Servlet可以动态地扩展Server的能力,并采用请求-响应模式提供Web服务。最早支持Servlet技术的是JavaSoft的Java Web Server。此后,一些其它的基于Java的Web Server开始支持标准的Servlet API。Servlet的主要功能在于交互式地浏览和修改数据,生成动态Web内容。
Tomcat服务器本质是通过ServerSocket与客户端进行通信,要进行通信首先就要进行TCP连接,Tomcat有两个核心组件,Connecter和Container,Connecter将在某个指定的端口上侦听客户请求,接收浏览器的发过来的 tcp 连接请求,创建一个 Request 和 Response 对象分别用于和请求端交换数据,Request包含了用户的请求信息,Response负责记录了服务器的答复内容。然后会产生一个线程来处理这个请求并把产生的 Request 和 Response 对象传给Container处理。
Connector 最重要的功能就是接收连接请求然后分配线程让 Container 来处理这个请求,所以这必然是多线程的,多线程的处理是 Connector 设计的核心。
当Connector处理完后会调用Container的invoke()方法,你可以想象Container容器里有一条管道,管道上有很多阀门,每个阀门都会根据request进行一些操作,request和response请求会依次经过这些阀门,而Servlet就是该管道的最后一道阀门,之前的阀门就是filter。
Tomcat容器也分有上下层级关系如下图,Tomcat的四层容器不都是必须的,一般简单的容器只有Context和Wrapper两层,Contenxt负责管理多个Wrapper,负责将映射转发到对应Wrapper,当然期间还要经过filter过滤。Wrapper是最低层的容器,它只包裹着一个Servlet,Wrapper负责加载并管理调用Servlet服务。
B树,即二叉搜索树,有如下特点:
B树的搜索,从根结点开始,如果查询的关键字与结点的关键字相等,那么就命中;否则,如果查询关键字比结点关键字小,就进入左儿子;如果比结点关键字大,就进入右儿子;如果左儿子或右儿子的指针为空,则报告找不到相应的关键字;如果B树的所有非叶子结点的左右子树的结点数目均保持差不多(平衡),那么B树的搜索性能逼近二分查找;但它比连续内存空间的二分查找的优点是,改变B树结构(插入与删除结点)不需要移动大段的内存数据,甚至通常是常数开销。
B-树,是一种多路搜索树(并不是二叉的):
*B树和B-树是同一种树,只不过英语中B-tree被中国人翻译成了B-树,让人以为B树和B-树是两种树,实际上,两者就是同一种树。此处单从算法的角度进行了划分,区别于 B+ 树,可以参见:* en.wikipedia.org/wiki/Binary…
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率。所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题,由于 M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占 M/2 的结点,删除结点时,需将两个不足M/2的兄弟节点合并。B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果 命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。
B+树,B+树是B-树的变体,也是一种多路搜索树,其定义基本与B-树同,除了:
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;更适合文件索引系统。
数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务拥有四个重要的特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。
SQL 标准定义的四种隔离级别:
MySQL的innodb引擎是如何实现MVCC的。innodb会为每一行添加两个字段,分别表示该行创建的版本和删除的版本,填入的是事务的版本号,这个版本号随着事务的创建不断递增。在repeated read的隔离级别下,具体各种数据库操作的实现:
事务开始,第一次不加锁SELECT时,InnoDB从全局事务链表中,筛选所有活动事务(事务trx_id严格递增),生成当前一致性视图。
根据当前一致性视图高低水位,计算事务可见性。
根据可见事务redo log,逆向算出历史版本。SELECT快照读,读之前版本数据。SELECT FOR UPDATE 或 UPDATE 当前读,加行锁读当前值,不会创建一致性视图,有其它事务更新时,等待其它事务提交。(2PL更新时加写锁,事务提交时才会释放)
InnoDB 行级锁是通过给索引上的索引项加锁来实现的,InnoDB行级锁只有通过索引条件检索数据,才使用行级锁;否则,InnoDB使用表锁。
在不通过索引(主键)条件查询的时候,InnoDB是表锁而不是行锁。也就是说,在没有使用索引的情况下,使用的就是表锁。
间隙锁可以理解为是对于一定范围内的数据进行锁定,如果说这个区间没有这条数据的话也是会锁住的;主要是解决幻读的问题,如果没有添加间隙锁。如果其他事务中添加 id 在 1 到 100 之间的某条记录,此时会发生幻读;另一方面,视为了满足其恢复和赋值的需求(幻读的概念在上面有提到)。
默认情况下,innodb_locks_unsafe_for_binlog是0(禁用),这意味着启用了间隙锁定:InnoDB使用下一个键锁进行搜索和索引扫描。若要启用该变量,请将其设置为1。这将导致禁用间隙锁定:InnoDB只使用索引记录锁进行搜索和索引扫描。
innodb自动使用间隙锁的条件:
间隙锁的目的是为了防止幻读,其主要通过两个方面实现这个目的:
hash索引的特点:
在MySQL的存储引擎中,MyISAM 不支持哈希索引,而 InnoDB 中的hash索引是存储引擎根据B-Tree索引自建的。因为hash索引本身只需要存储对应的hash值,所以索引的结构十分紧凑,这也让hash索引查找的速度非常快。然而,哈希索引也有限制,如下:
完全基于内存,绝大多数请求是纯粹的内存操作,非常快速。数据存储在内存中,类似于HashMap,具备较快的查找和操作的时间复杂度O(1)。
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的。 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用考虑各种锁的问题,不存在加锁释放锁(上下文的切换),没有因为可能出现死锁而导致的性能消耗。 使用多路I/O复用模型,非阻塞IO。
使用底层模型不同,它们之间底层实现方式以及与客户端之间的通信的应用协议不一样,Redis直接自己构建了VM机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求(用户态和内核态之间的切换)。
本来本文的标题是 抖音、腾讯、阿里、美团春招服务端开发岗位硬核面试(下)
,然题目比较多,限于篇幅,只能改成 二
,下篇我们继续。
另外帮忙插播一个内推岗位,有兴趣的同学可以投简历或者后台和我私聊。