写点什么

为了面个好公司!拼了!3.5W 字的 Java 面试题整理(答案 + 学习路线)上!

发布于: 2020 年 09 月 11 日

前言:


本文是在网上看了各种面试指南收集的题目及答案。无意冒犯各位原创作者,如果在您的博客或者写作平台有相似问题答案可以跟我说,我给您链接加上,我只是为了方便以后自己需要的时候刷一刷,不用在到处找题。


  • Spring MVC(20 题)(Spring 全家桶学习路线图谱)

  • JVM(30 题)(学习路线图谱)

  • Mybatis(25 题)

  • Redis(29 题)(含学习图谱)

  • 并发编程(15 题)

  • HashMap(21 题)


如果你觉得我整理的这些东西对你有帮助的话,

记得点赞+评论+关注哈!感谢支持!



面试资料,学习路线,思维导图,java 知识点可以直接添加助手 vx:bjmsb10;获取完整 pdf 版


SpringMVC(20 题)(Spring 全家桶学习路线图谱)


1、SpringMVC 工作原理?


1)客户端发送请求到 DispatcherServlet


2)DispatcherServlet 查询 handlerMapping 找到处理请求的 Controller


3)Controller 调用业务逻辑后,返回 ModelAndView


4)DispatcherServlet 查询 ModelAndView,找到指定视图


5)视图将结果返回到客户端


2、SpringMVC 流程?


1)用户发送请求至前端控制器 DispatcherServlet。


2)DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器。


3)处理器映射器找到具体的处理器(可以根据 xml 配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。


4)DispatcherServlet 调用 HandlerAdapter 处理器适配器。


5)HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)。


6)Controller 执行完成返回 ModelAndView。


7)HandlerAdapter 将 controller 执行结果 ModelAndView 返回给 DispatcherServlet。


8)DispatcherServlet 将 ModelAndView 传给 ViewReslover 视图解析器。


9)ViewReslover 解析后返回具体 View。


10)DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。


11)DispatcherServlet 响应用户。



3、SpringMvc 的控制器是不是单例模式,如果是,有什么问题,怎么解决?


答:是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。


4、如果你也用过 struts2.简单介绍下 springMVC 和 struts2 的区别有哪些?


答:


1)springmvc 的入口是一个 servlet 即前端控制器,而 struts2 入口是一个 filter 过虑器。


2)springmvc 是基于方法开发(一个 url 对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2 是基于类开发,传递参数是通过类的属性,只能设计为多例。


3)Struts 采用值栈存储请求和响应的数据,通过 OGNL 存取数据,springmvc 通过参数解析器是将 request 请求内容解析,并给方法形参赋值,将数据和视图封装成 ModelAndView 对象,最后又将 ModelAndView 中的模型数据通过 reques 域传输到页面。Jsp 视图解析器默认使用 jstl。


5、SpingMvc 中的控制器的注解一般用那个,有没有别的注解可以替代?


答:一般用 @Conntroller 注解,表示是表现层,不能用用别的注解代替。


6、 @RequestMapping 注解用在类上面有什么作用?


答:是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。


7、怎么样把某个请求映射到特定的方法上面?


答:直接在方法上面加上注解 @RequestMapping,并且在这个注解里面写上要拦截的路径


8、如果在拦截请求中,我想拦截 get 方式提交的方法,怎么配置?


答:可以在 @RequestMapping 注解里面加上 method=RequestMethod.GET


9、怎么样在方法里面得到 Request,或者 Session?


答:直接在方法的形参中声明 request,SpringMvc 就自动把 request 对象传入


10、我想在拦截的方法里面得到从前台传入的参数,怎么得到?


答:直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样


11、如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象?


答:直接在方法中声明这个对象,SpringMvc 就自动会把属性赋值到这个对象里面。


12、SpringMvc 中函数的返回值是什么?


答:返回值可以有很多类型,有 String, ModelAndView,当一般用 String 比较好。


13、SpringMVC 怎么样设定重定向和转发的?


答:在返回值前面加"forward:“就可以让结果转发,譬如"forward:user.do?name=method4” 在返回值前面加"redirect:“就可以让返回值重定向,譬如"redirect:http://www.baidu.com


14、SpringMvc 用什么对象从后台向前台传递数据的?


答:通过 ModelMap 对象,可以在这个对象里面用 put 方法,把对象加到里面,前台就可以通过 el 表达式拿到。


15、SpringMvc 中有个类把视图和数据都合并的一起的,叫什么?


答:叫 ModelAndView。


16、怎么样把 ModelMap 里面的数据放入 Session 里面?


答:可以在类上面加上 @SessionAttributes 注解,里面包含的字符串就是要放入 session 里面的 key


17、SpringMvc 怎么和 AJAX 相互调用的?


答:通过 Jackson 框架就可以把 Java 里面的对象直接转化成 Js 可以识别的 Json 对象。


具体步骤如下 :


 1)加入Jackson.jar 
2)在配置文件中配置json的映射
3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解12345
复制代码


18、当一个方法向 AJAX 返回特殊对象,譬如 Object,List 等,需要做什么处理?


答:要加上 @ResponseBody 注解


19、SpringMvc 里面拦截器是怎么写的


答:有两种写法,一种是实现接口,另外一种是继承适配器类,然后在 SpringMvc 的配置文件中配置拦截器即可:


<!-- 配置SpringMvc的拦截器 --><mvc:interceptors>        <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 -->       <bean id="myInterceptor" class="com.et.action.MyHandlerInterceptor"></bean>        <!-- 只针对部分请求拦截 -->        <mvc:interceptor>               <mvc:mapping path="/modelMap.do" />               <bean class="com.et.action.MyHandlerInterceptorAdapter" />      </mvc:interceptor></mvc:interceptors>12345678910
复制代码


20、讲下 SpringMvc 的执行流程


答:系统启动的时候根据配置文件创建 spring 的容器, 首先是发送 http 请求到核心控制器 disPatherServlet,spring 容器通过映射器去寻找业务控制器,使用适配器找到相应的业务类,在进业务类时进行数据封装,在封装前可能会涉及到类型转换,执行完业务类后使用 ModelAndView 进行视图转发,数据放在 model 中,用 map 传递数据进行页面显示。


最后附上 Spring 全家桶学习图谱:



ps:资源过大,需要的话直接添加助手 vx:bjmsb10 免费获取


JVM(30 题)


1、什么是 Java 虚拟机?为什么 Java 被称作是“平台无关的编程语言”?


Java 虚拟机是一个可以执行 Java 字节码的虚拟机进程。Java 源文件被编译成能被 Java 虚拟机执行的字节码文件。Java 被设计成允许应用程序可以运行在任意的平台,而不需要程序员为每一个平台单独重写或者是重新编译。Java 虚拟机让这个变为可能,因为它知道底层硬件平台的指令长度和其他特性。


2、Java 内存结构?


方法区和对是所有线程共享的内存区域;而 java 栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。


3、内存模型以及分区,需要详细到每个区放什么?


JVM 分为堆区和栈区,还有方法区,初始化的对象放在堆里面,引用放在栈里面,class 类信息常量池(static 常量和 static 变量)等放在方法区。

new:


方法区:主要是存储类信息,常量池(static 常量和 static 变量),编译后的代码(字节码)等数据


堆:初始化的对象,成员变量 (那种非 static 的变量),所有的对象实例和数组都要在堆上分配


栈:栈的结构是栈帧组成的,调用一个方法就压入一帧,帧上面存储局部变量表,操作数栈,方法出口等信息,局部变量表存放的是 8 大基础类型加上一个应用类型,所以还是一个指向地址的指针


本地方法栈:主要为 Native 方法服务


程序计数器:记录当前线程执行的行号


4、堆里面的分区:Eden,survival (from+ to),老年代,各自的特点?


堆里面分为新生代和老生代(java8 取消了永久代,采用了 Metaspace),新生代包含 Eden+Survivor 区,survivor 区里面分为 from 和 to 区,内存回收时,如果用的是复制算法,从 from 复制到 to,当经过一次或者多次 GC 之后,存活下来的对象会被移动到老年区,当 JVM 内存不够用的时候,会触发 Full GC,清理 JVM 老年区当新生区满了之后会触发 YGC,先把存活的对象放到其中一个 Survice 区,然后进行垃圾清理。

因为如果仅仅清理需要删除的对象,这样会导致内存碎片,因此一般会把 Eden 进行完全的清理,然后整理内存。

那么下次 GC 的时候,就会使用下一个 Survive,这样循环使用。

如果有特别大的对象,新生代放不下,就会使用老年代的担保,直接放到老年代里面。

因为 JVM 认为,一般大对象的存活时间一般比较久远。


5 、解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法


通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用 JVM 中的栈空间;而通过 new 关键字和构造器创建的对象则放在堆空间,堆是垃圾收集器管理的主要区域,由于现在的垃圾收集器都采用分代收集算法,所以堆空间还可以细分为新生代和老生代,再具体一点可以分为 Eden、Survivor(又可分为 From Survivor 和 To Survivor)、Tenured;方法区和堆都是各个线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量、JIT 编译器编译后的代码等数据;程序中的字面量(literal)如直接书写的 100、”hello”和常量都是放在常量池中,常量池是方法区的一部分,。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,栈和堆的大小都可以通过 JVM 的启动参数来进行调整,栈空间用光了会引发 StackOverflowError,而堆和常量池空间不足则会引发 OutOfMemoryError。


String str = new String("hello");1
复制代码


上面的语句中变量 str 放在栈上,用 new 创建出来的字符串对象放在堆上,而”hello”这个字面量是放在方法区的。


补充 1:较新版本的 Java(从 Java 6 的某个更新开始)中,由于 JIT 编译器的发展和”逃逸分析”技术的逐渐成熟,栈上分配、标量替换等优化技术使得对象一定分配在堆上这件事情已经变得不那么绝对了。


补充 2:运行时常量池相当于 Class 文件常量池具有动态性,Java 语言并不要求常量一定只有编译期间才能产生,运行期间也可以将新的常量放入池中,String 类的 intern()方法就是这样的。看看下面代码的执行结果是什么并且比较一下 Java 7 以前和以后的运行结果是否一致。


String s1 = new StringBuilder("go")    .append("od").toString();System.out.println(s1.intern() == s1);String s2 = new StringBuilder("ja")    .append("va").toString();System.out.println(s2.intern() == s2);123456
复制代码


6、GC 的两种判定方法?


引用计数法:指的是如果某个地方引用了这个对象就+1,如果失效了就-1,当为 0 就 会回收但是 JVM 没有用这种方式,因为无法判定相互循环引用(A 引用 B,B 引用 A) 的情况。

引用链法:通过一种 GC ROOT 的对象(方法区中静态变量引用的对象等-static 变 量)来判断,如果有一条链能够到达 GC ROOT 就说明,不能到达 GC ROOT 就说明 可以回收


7、SafePoint 是什么?


比如 GC 的时候必须要等到 Java 线程都进入到 safepoint 的时候 VMThread 才能开始执行 GC

1.循环的末尾 (防止大循环的时候一直不进入 safepoint,而其他线程在等待它进入 safepoint)

2.方法返回前

3.调用方法的 call 之后

4.抛出异常的位置


8、GC 的三种收集方法:标记清除、标记整理、复制算法的原理与特点,分别用在什么地方,如果让你优化收集方法,有什么思路?


先标记,标记完毕之后再清除,效率不高,会产生碎片

复制算法:分为 8:1 的 Eden 区和 survivor 区,就是上面谈到的 YGC

标记整理:标记完毕之后,让所有存活的对象向一端移动


9、GC 收集器有哪些?CMS 收集器与 G1 收集器的特点?


并行收集器:串行收集器使用一个单独的线程进行收集,GC 时服务有停顿时间

串行收集器:次要回收中使用多线程来执行

CMS 收集器是基于“标记—清除”算法实现的,经过多次标记才会被清除

G1 从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的

10.Minor GC 与 Full GC 分别在什么时候发生?

新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC


11、几种常用的内存调试工具:jmap、jstack、jconsole、jhat?


jstack 可以看当前栈的情况,jmap 查看内存,jhat 进行 dump 堆的信息 mat(eclipse 的也要了解一下)


12、什么是类的加载


类的加载指的是将类的.class 文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的 Class 对象,Class 对象封装了类在方法区内的数据结构,并且向 Java 程序员提供了访问方法区内的数据结构的接口。


13.类加载器



  • 启动类加载器:Bootstrap ClassLoader,负责加载存放在 JDK\jre\lib(JDK 代表 JDK 的安装目录,下同)下,或被-Xbootclasspath 参数指定的路径中的,并且能被虚拟机识别的类库

  • 扩展类加载:Extension ClassLoader,该加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 DK\jre\lib\ext 目录中,或者由 java.ext.dirs 系统变量指定的路径中的所有类库(如 javax.*开头的类),开发者可以直接使用扩展类加载器。

  • 应用程序类加载器:Application ClassLoader,该类加载器由 sun.misc.Launcher$AppClassLoader 来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器


14、描述一下 JVM 加载 class 文件的原理机制?


JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它负责在运行时查找和装入类文件中的类。


由于 Java 的跨平台性,经过编译的 Java 源程序并不是一个可执行程序,而是一个或多个类文件。当 Java 程序需要使用某个类时,JVM 会确保这个类已经被加载、连接(验证、准备和解析)和初始化。类的加载是指把类的.class 文件中的数据读入到内存中,通常是创建一个字节数组读入.class 文件,然后产生与所加载类对应的 Class 对象。加载完成后,Class 对象还不完整,所以此时的类还不可用。当类被加载后就进入连接阶段,这一阶段包括验证、准备(为静态变量分配内存并设置默认的初始值)和解析(将符号引用替换为直接引用)三个步骤。最后 JVM 对类进行初始化,包括:


  • 1)如果类存在直接的父类并且这个类还没有被初始化,那么就先初始化父类;

  • 2)如果类中存在初始化语句,就依次执行这些初始化语句。


类的加载是由类加载器完成的,类加载器包括:根加载器(BootStrap)、扩展加载器(Extension)、系统加载器(System)和用户自定义类加载器(java.lang.ClassLoader 的子类)。

从 Java 2(JDK 1.2)开始,类加载过程采取了父亲委托机制(PDM)。PDM 更好的保证了 Java 平台的安全性,在该机制中,JVM 自带的 Bootstrap 是根加载器,其他的加载器都有且仅有一个父类加载器。类的加载首先请求父类加载器加载,父类加载器无能为力时才由其子类加载器自行加载。JVM 不会向 Java 程序提供对 Bootstrap 的引用。下面是关于几个类加载器的说明:


Bootstrap:一般用本地代码实现,负责加载 JVM 基础核心类库(rt.jar);


Extension:从 java.ext.dirs 系统属性所指定的目录中加载类库,它的父加载器是 Bootstrap;


System:又叫应用类加载器,其父类是 Extension。它是应用最广泛的类加载器。它从环境变量 classpath 或者系统属性 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。


15、Java 对象创建过程


1.JVM 遇到一条新建对象的指令时首先去检查这个指令的参数是否能在常量池中定义到一个类的符号引用。然后加载这个类(类加载过程在后边讲)


2.为对象分配内存。一种办法“指针碰撞”、一种办法“空闲列表”,最终常用的办法“本地线程缓冲分配(TLAB)”


3.将除对象头外的对象内存空间初始化为 0


4.对对象头进行必要设置


16、类的生命周期


类的生命周期包括这几个部分,加载、连接、初始化、使用和卸载,其中前三部是类的加载的过程,如下图;



java 类加载需要经历以下 几个过程:


  • 加载


加载时类加载的第一个过程,在这个阶段,将完成以下三件事情:

1.通过一个类的全限定名获取该类的二进制流。

2.将该二进制流中的静态存储结构转化为方法去运行时数据结构。

3.在内存中生成该类的 Class 对象,作为该类的数据访问入口。


  • 验证


验证的目的是为了确保 Class

文件的字节流中的信息不回危害到虚拟机.在该阶段主要完成以下四钟验证:

文件格式验证:验证字节流是否符合 Class

文件的规范,如主次版本号是否在当前虚拟机范围内,常量池中的常量是否有不被支持的类型.

元数据验证:对字节码描述的信息进行语义分析,如这个类是否有父类,是否集成了不被继承的类等。

字节码验证:是整个验证过程中最复杂的一个阶段,通过验证数据流和控制流的分析,确定程序语义是否正确,主要针对方法体的验证。 如: 方法中的类型转换是否正确,跳转指令是否正确等。

符号引用验证: 这个动作在后面的解析过程中发生,主要是为了确保解析动作能正确执行。


  • 准备


准备阶段是为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中进行分配。准备阶段不分配类中的实例变量的内存,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。


public static int value=123;1
复制代码


//在准备阶段 value 初始值为 0 。在初始化阶段才会变为123。12
复制代码


  • 解析


该阶段主要完成符号引用到直接引用的转换动作。解析动作并不一定在初始化动作完成之前,也有可能在初始化之后。


  • 初始化


初始化时类加载的最后一步,前面的类加载过程,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。 到了初始化阶段,才真正开始执行类中定义的 Java 程序。


17、简述 java 类加载机制?


虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的 java 类型。


18、Java 对象结构


Java 对象由三个部分组成:对象头、实例数据、对齐填充。

对象头由两部分组成,第一部分存储对象自身的运行时数据:哈希码、GC 分代年龄、锁标识状态、线程持有的锁、偏向线程 ID(一般占 32/64 bit)。

第二部分是指针类型,指向对象的类元数据类型(即对象代表哪个类)。如果是数组对象,则对象头中还有一部分用来记录数组长度。

实例数据用来存储对象真正的有效信息(包括父类继承下来的和自己定义的)

对齐填充:JVM 要求对象起始地址必须是 8 字节的整数倍(8 字节对齐)

19.Java 对象的定位方式

句柄池、直接指针。


20、如和判断一个对象是否存活?(或者 GC 对象的判定方法)


判断一个对象是否存活有两种方法:

1.引用计数法

所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。

引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A,B 对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。

2.可达性算法(引用链法)

该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。

在 java 中可以作为 GC Roots 的对象有以下几种:

虚拟机栈中引用的对象

方法区类静态属性引用的对象

方法区常量池引用的对象

本地方法栈 JNI 引用的对象

虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize()方法。当对象没有覆盖 finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。


如果该对象有必要执行 finalize()方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize()执行缓慢或者发生了死锁,那么就会造成 FQueue 队列一直等待,造成了内存回收系统的崩溃。 GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。


21、JVM 的永久代中会发生垃圾回收么?


垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免 Full GC 是非常重要的原因。请参考下 Java8:从永久代到元数据区 (注:Java8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区)


22、简述 java 内存分配与回收策率以及 Minor GC 和 Major GC?


1.对象优先在堆的 Eden 区分配。


2.大对象直接进入老年代.


3.长期存活的对象将直接进入老年代.,当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC.Minor Gc 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高,

回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之前进行一次 MinorGC 这样可以加快老年代的回收速度。


23、判断一个对象应该被回收


该对象没有与 GC Roots 相连

该对象没有重写 finalize()方法或 finalize()已经被执行过则直接回收(第一次标记)、否则将对象加入到 F-Queue 队列中(优先级很低的队列)在这里 finalize()方法被执行,之后进行第二次标记,如果对象仍然应该被 GC 则 GC,否则移除队列。(在 finalize 方法中,对象很可能和其他 GC Roots 中的某一个对象建立了关联,finalize 方法只会被调用一次,且不推荐使用 finalize 方法)


24、回收方法区


方法区回收价值很低,主要回收废弃的常量和无用的类。

如何判断无用的类:

该类所有实例都被回收(Java 堆中没有该类的对象)

加载该类的 ClassLoader 已经被回收

该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方利用反射访问该类


25、垃圾收集算法


GC 最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法,我们常用的垃圾回收器一般都采用分代收集算法。


  • 标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。

  • 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

  • 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

  • 分代收集算法,“分代收集”(Generational Collection)算法,把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。


26、垃圾回收器


Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。


ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。

Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。


Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法

CMS 收集器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。


G1 收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征


27、GC 日志分析


摘录 GC 日志一部分(前部分为年轻代 gc 回收;后部分为 full gc 回收):


2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]12
复制代码


通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen 属于 Parallel 收集器。其中 PSYoungGen 表示 gc 回收前后年轻代的内存变化;

ParOldGen 表示 gc 回收前后老年代的内存变化;PSPermGen 表示 gc 回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少 full gc 的次数


28、调优命令


Sun JDK 监控和故障处理命令有 jps jstat jmap jhat jstack jinfo


  • jps,JVM Process Status Tool,显示指定系统内所有的 HotSpot 虚拟机进程。

  • jstat,JVM statistics Monitoring 是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。

  • jmap,JVM Memory Map 命令用于生成 heap dump 文件

  • jhat,JVM Heap Analysis Tool 命令是与 jmap 搭配使用,用来分析 jmap 生成的 dump,jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看

  • jstack,用于生成 java 虚拟机当前时刻的线程快照。

  • jinfo,JVM Configuration info 这个命令作用是实时查看和调整虚拟机运行参数。


29、调优工具


常用调优工具分为两类,jdk 自带监控工具:jconsole 和 jvisualvm,第三方有:MAT(Memory Analyzer Tool)、GChisto。

jconsole,Java Monitoring and Management Console 是从 java5 开始,在 JDK 中自带的 java 监控和管理控制台,用于对 JVM 中内存,线程和类等的监控

jvisualvm,jdk 自带全能工具,可以分析内存快照、线程快照;监控内存变化、GC 变化等。

MAT,Memory Analyzer Tool,一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的 Java heap 分析工具,它可以帮助我们查找内存泄漏和减少内存消耗

GChisto,一款专业分析 gc 日志的工具


30、Minor GC 与 Full GC 分别在什么时候发生?


新生代内存不够用时候发生 MGC 也叫 YGC,JVM 内存不够的时候发生 FGC


最后附上学习图谱:



ps:资源过大,需要的话可以直接添加助手 vx:bjmsb10 免费获取


Mybatis(25 题)


1、什么是 Mybatis?


(1)Mybatis 是一个半 ORM(对象关系映射)框架,它内部封装了 JDBC,开发时只需要关注 SQL 语句本身,不需要花费精力去处理加载驱动、创建连接、创建 statement 等繁杂的过程。程序员直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高。

(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO 映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。

(3)通过 xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过 java 对象和 statement 中 sql 的动态参数进行映射生成最终执行的 sql 语句,最后由 mybatis 框架执行 sql 并将结果映射为 java 对象并返回。(从执行 sql 到返回 result 的过程)。


2、Mybaits 的优点:


(1)基于 SQL 语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL 写在 XML 里,解除 sql 与程序代码的耦合,便于统一管理;提供 XML 标签,支持编写动态 SQL 语句,并可重用。

(2)与 JDBC 相比,减少了 50%以上的代码量,消除了 JDBC 大量冗余的代码,不需要手动开关连接;

(3)很好的与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要 JDBC 支持的数据库 MyBatis 都支持)。

(4)能够与 Spring 很好的集成;

(5)提供映射标签,支持对象与数据库的 ORM 字段关系映射;提供对象关系映射标签,支持对象关系组件维护。

3、MyBatis 框架的缺点:

(1)SQL 语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写 SQL 语句的功底有一定要求。

(2)SQL 语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。


4、MyBatis 框架适用场合:


(1)MyBatis 专注于 SQL 本身,是一个足够灵活的 DAO 层解决方案。

(2)对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis 将是不错的选择。

5、MyBatis 与 Hibernate 有哪些不同?

(1)Mybatis 和 hibernate 不同,它不完全是一个 ORM 框架,因为 MyBatis 需要程序员自己编写 Sql 语句。

(2)Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。

(3)Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率。


6、#{}和 ${}的区别是什么?


#{}是预编译处理,${}是字符串替换。

Mybatis 在处理 #{}时,会将 sql 中的 #{}替换为?号,调用 PreparedStatement 的 set 方法来赋值;

Mybatis 在处理{}替换成变量的值。

使用 #{}可以有效的防止 SQL 注入,提高系统安全性。


7、当实体类中的属性名和表中的字段名不一样 ,怎么办 ?


第 1 种: 通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。


<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>   select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};    </select>123
复制代码


第 2 种: 通过来映射字段名和实体类属性名的一一对应的关系。


 <select id="getOrder" parameterType="int" resultMap="orderresultmap">   select * from orders where order_id=#{id}    </select>    <resultMap type=”me.gacl.domain.order” id=”orderresultmap”>    <!–用id属性来映射主键字段–>    <id property=”id” column=”order_id”>     <!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–>   <result property = “orderno” column =”order_no”/>    <result property=”price” column=”order_price” />    </reslutMap>123456789101112
复制代码


8、 模糊查询 like 语句该怎么写?


第 1 种:在 Java 代码中添加 sql 通配符。


    string wildcardname = “%smi%”;    list<name> names = mapper.selectlike(wildcardname);     <select id=”selectlike”> select * from foo where bar like #{value}    </select>123456
复制代码


第 2 种:在 sql 语句中拼接通配符,会引起 sql 注入


    string wildcardname = “smi”;    list<name> names = mapper.selectlike(wildcardname);     <select id=”selectlike”> select * from foo where bar like "%"#{value}"%"    </select>123456
复制代码


9、通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?


Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。

Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。


10、Mybatis 是如何进行分页的?分页插件的原理是什么?


Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。


11、Mybatis 是如何将 sql 执行结果封装为目标对象并返回的?都有哪些映射形式?


第一种是使用 resultMap 标签,逐一定义数据库列名和对象属性名之间的映射关系。

第二种是使用 sql 列的别名功能,将列的别名书写为对象属性名。

有了列名与属性名的映射关系后,Mybatis 通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。


12、如何执行批量插入?


首先,创建一个简单的 insert 语句:


    <insert id=”insertname”>    insert into names (name) values (#{value})    </insert>123
复制代码


然后在 java 代码中像下面这样执行批处理插入:



list<string> names = new arraylist(); names.add(“fred”); names.add(“barney”); names.add(“betty”); names.add(“wilma”); // 注意这里 executortype.batch sqlsession sqlsession = sqlsessionfactory.opensession(executortype.batch); try { namemapper mapper = sqlsession.getmapper(namemapper.class); for (string name : names) { mapper.insertname(name); } sqlsession.commit(); }catch(Exception e){ e.printStackTrace(); sqlSession.rollback(); throw e; }finally { sqlsession.close(); }1234567891011121314151617181920212223
复制代码


13、如何获取自动生成的(主)键值?


insert 方法总是返回一个 int 值 ,这个值代表的是插入的行数。


如果采用自增长策略,自动生成的键值在 insert 方法执行完后可以被设置到传入的参数对象中。


示例:


<insert id=”insertname” usegeneratedkeys=”true” keyproperty=”id”> insert into names (name) values (#{name})</insert>name name = new name();    name.setname(“fred”);     int rows = mapper.insertname(name);    // 完成后,id已经被设置到对象中    system.out.println(“rows inserted = ” + rows);    system.out.println(“generated key value = ” + name.getid());12345678910
复制代码


14、在 mapper 中如何传递多个参数?


1)第一种://DAO层的函数Public UserselectUser(String name,String area);  //对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。<select id="selectUser"resultMap="BaseResultMap">      select *  fromuser_user_t   whereuser_name = #{0} anduser_area=#{1}  </select>  (2)第二种: 使用 @param 注解:public interface usermapper {   user selectuser(@param(“username”) string username,@param(“hashedpassword”) string hashedpassword);}然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):<select id=”selectuser” resulttype=”user”> select id, username, hashedpassword from some_table where username = #{username} and hashedpassword = #{hashedpassword}</select>(3)第三种:多个参数封装成maptry{//映射文件的命名空间.SQL片段的ID,就可以调用对应的映射文件中的SQL//由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数Map<String, Object> map = new HashMap(); map.put("start", start); map.put("end", end); return sqlSession.selectList("StudentID.pagination", map); }catch(Exception e){ e.printStackTrace(); sqlSession.rollback();    throw e; }finally{ MybatisUtil.closeSqlSession(); }123456789101112131415161718192021222324252627282930313233
复制代码


15、Mybatis 的 Xml 映射文件中,不同的 Xml 映射文件,id 是否可以重复?


不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;

原因就是 namespace+id 是作为 Map<String, MapperStatement>的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。

但是,在以前的 Mybatis 版本的 namespace 是可选的,不过新版本的 namespace 已经是必须的了。


16、为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?


Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。


17、 一对一、一对多的关联查询 ?


<mapper namespace="com.lcb.mapping.userMapper">      <!--association  一对一关联查询 -->      <select id="getClass" parameterType="int" resultMap="ClassesResultMap">      select * from class c,teacher t where c.teacher_id=t.t_id and c.c_id=#{id}      </select>       <resultMap type="com.lcb.user.Classes" id="ClassesResultMap">     <!-- 实体类的字段名和数据表的字段名映射 -->     <id property="id" column="c_id"/>      <result property="name" column="c_name"/>      <association property="teacher" javaType="com.lcb.user.Teacher">      <id property="id" column="t_id"/>      <result property="name" column="t_name"/>      </association>      </resultMap>        <!--collection  一对多关联查询 -->      <select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">      select * from class c,teacher t,student s where c.teacher_id=t.t_id and c.c_id=s.class_id and c.c_id=#{id}      </select>       <resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">      <id property="id" column="c_id"/>      <result property="name" column="c_name"/>      <association property="teacher" javaType="com.lcb.user.Teacher">      <id property="id" column="t_id"/>      <result property="name" column="t_name"/>      </association>      <collection property="student" ofType="com.lcb.user.Student">      <id property="id" column="s_id"/>      <result property="name" column="s_name"/>      </collection>      </resultMap>  </mapper>123456789101112131415161718192021222324252627282930313233343536
复制代码


18、MyBatis 实现一对一有几种方式?具体怎么操作的?


有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次,通过在 resultMap 里面配置 association 节点配置一对一的类就可以完成;

嵌套查询是先查一个表,根据这个表里面的结果的 外键 id,去再另外一个表里面查询数据,也是通过 association 配置,但另外一个表的查询通过 select 属性配置。


19、MyBatis 实现一对多有几种方式,怎么操作的?


有联合查询和嵌套查询。联合查询是几个表联合查询,只查询一次,通过在 resultMap 里面的 collection 节点配置一对多的类就可以完成;嵌套查询是先查一个表,根据这个表里面的 结果的外键 id,去再另外一个表里面查询数据,也是通过配置 collection,但另外一个表的查询通过 select 节点配置。


20、Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?


答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。


它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。

当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。


21、Mybatis 的一级、二级缓存:


1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;

3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存 Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。


22、什么是 MyBatis 的接口绑定?有哪些实现方式?


接口绑定,就是在 MyBatis 中任意定义接口,然后把接口里面的方法和 SQL 语句绑定,我们直接调用接口方法就可以,这样比起原来了 SqlSession 提供的方法我们可以有更加灵活的选择和设置。

接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加上 @Select、@Update 等注解,里面包含 Sql 语句来绑定;另外一种就是通过 xml 里面写 SQL 来绑定,在这种情况下,要指定 xml 映射文件里面的 namespace 必须为接口的全路径名。当 Sql 语句比较简单时候,用注解绑定,当 SQL 语句比较复杂时候,用 xml 绑定,一般用 xml 绑定的比较多。


23、使用 MyBatis 的 mapper 接口调用时有哪些要求?


①Mapper 接口方法名和 mapper.xml 中定义的每个 sql 的 id 相同;

②Mapper 接口方法的输入参数类型和 mapper.xml 中定义的每个 sql 的 parameterType 的类型相同;

③Mapper 接口方法的输出参数类型和 mapper.xml 中定义的每个 sql 的 resultType 的类型相同;

④Mapper.xml 文件中的 namespace 即是 mapper 接口的类路径。


24、Mapper 编写有哪几种方式?


第一种:接口实现类继承 SqlSessionDaoSupport:使用此种方法需要编写 mapper 接口,mapper 接口实现类、mapper.xml 文件。

(1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置


<mappers>  <mapper resource="mapper.xml文件的地址" />  <mapper resource="mapper.xml文件的地址" /></mappers>1234
复制代码


(2)定义 mapper 接口


(3)实现类集成 SqlSessionDaoSupport


mapper 方法中可以 this.getSqlSession()进行数据增删改查。


(4)spring 配置


<bean id=" " class="mapper接口的实现">  <property name="sqlSessionFactory" ref="sqlSessionFactory"></property></bean>123
复制代码


第二种:使用 org.mybatis.spring.mapper.MapperFactoryBean:

(1)在 sqlMapConfig.xml 中配置 mapper.xml 的位置,如果 mapper.xml 和 mappre 接口的名称相同且在同一个目录,这里可以不用配置


<mappers>  <mapper resource="mapper.xml文件的地址" />  <mapper resource="mapper.xml文件的地址" /></mappers>1234
复制代码


(2)定义 mapper 接口:


①mapper.xml 中的 namespace 为 mapper 接口的地址


②mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致


③Spring 中定义


<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">  <property name="mapperInterface"  value="mapper接口地址" />  <property name="sqlSessionFactory" ref="sqlSessionFactory" /></bean>1234
复制代码


第三种:使用 mapper 扫描器:

(1)mapper.xml 文件编写:

mapper.xml 中的 namespace 为 mapper 接口的地址;

mapper 接口中的方法名和 mapper.xml 中的定义的 statement 的 id 保持一致;

如果将 mapper.xml 和 mapper 接口的名称保持一致则不用在 sqlMapConfig.xml 中进行配置。

(2)定义 mapper 接口:

注意 mapper.xml 的文件名和 mapper 的接口名称保持一致,且放在同一个目录

(3)配置 mapper 扫描器:


<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  <property name="basePackage" value="mapper接口包地址"></property> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/></bean>1234
复制代码


(4)使用扫描器后从 spring 容器中获取 mapper 的实现对象。


25、简述 Mybatis 的插件运行原理,以及如何编写一个插件。


答:Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可,记住,别忘了在配置文件中配置你编写的插件。


Redis(29 题)(含学习图谱)


1、Redis 的回收策略(淘汰策略)?


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(驱逐):禁止驱逐数据


注意这里的 6 种机制,volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的 lru、ttl 以及 random 是三种不同的淘汰策略,再加上一种 no-enviction 永不回收的策略。


使用策略规则:

(1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru


(2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random


2、为什么 edis 需要把所有数据放到内存中?


答 :Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以 redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘 I/O 速度为严重影响 redis 的性能。在内存越来越便宜的今天,redis 将会越来越受欢迎。如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。


3、Redis 的同步机制了解么?


答:Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次 bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。


4、Pipeline 有什么好处,为什么要用 pipeline?


答:可以将多次 IO 往返的时间缩减为一次,前提是 pipeline 执行的指令之间没有因果相关性。使用 redis-benchmark 进行压测的时候可以发现影响 redis 的 QPS 峰值的一个重要因素是 pipeline 批次指令的数目。


5、是否使用过 Redis 集群,集群的原理是什么?


(1)Redis Sentinal 着眼于高可用,在 master 宕机时会自动将 slave 提升为 master,继续提供服务。


(2)Redis Cluster 着眼于扩展性,在单个 redis 内存不足时,使用 Cluster 进行分片存储。


6、Redis 集群方案什么情况下会导致整个集群不可用?


答:有 A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点 B 失败了,那么整个集群就会以为缺少 5501-11000 这个范围的槽而不可用。


17、Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?

答:Redisson、Jedis、lettuce 等等,官方推荐使用 Redisson。


7、Jedis 与 Redisson 对比有什么优缺点?


答:Jedis 是 Redis 的 Java 实现的客户端,其 API 提供了比较全面的 Redis 命令的支持;Redisson 实现了分布式和可扩展的 Java 数据结构,和 Jedis 相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等 Redis 特性。


Redisson 的宗旨是促进使用者对 Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。


8、Redis 如何设置密码及验证密码?


设置密码:config set requirepass 123456


授权密码:auth 123456


9、说说 Redis 哈希槽的概念?


答:Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。


10、Redis 集群的主从复制模型是怎样的?


答:为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有 N-1 个复制品.


11、Redis 集群会有写操作丢失吗?为什么?


答 :Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。


12、Redis 集群之间是如何复制的?


答:异步复制


13、Redis 集群最大节点个数是多少?


答:16384 个。


14、Redis 集群如何选择数据库?


答:Redis 集群目前无法做数据库选择,默认在 0 数据库。


15、怎么测试 Redis 的连通性?


答:使用 ping 命令。


16、怎么理解 Redis 事务?


答:


(1)事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。


(2)事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。


17、Redis 事务相关的命令有哪几个?


答:MULTI、EXEC、DISCARD、WATCH


18、Redis key 的过期时间和永久有效分别怎么设置?


答:EXPIRE 和 PERSIST 命令。


19、Redis 如何做内存优化?


答:尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的所有信息存储到一张散列表里面。


20、Redis 回收进程如何工作的?


答:一个客户端运行了新的命令,添加了新的数据。Redi 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。一个新的命令被执行,等等。所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。


21、都有哪些办法可以降低 Redis 的内存使用情况呢?


答:如果你使用的是 32 位的 Redis 实例,可以好好利用 Hash,list,sorted set,set 等集合类型数据,因为通常情况下很多小的 Key-Value 可以用更紧凑的方式存放到一起。


22、Redis 的内存用完了会发生什么?


答:如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或者你可以将 Redis 当缓存来使用配置淘汰机制,当 Redis 达到内存上限时会冲刷掉旧的内容。


23、一个 Redis 实例最多能存放多少的 keys?List、Set、Sorted Set 他们最多能存放多少元素?


答:理论上 Redis 可以处理多达 2^32 的 keys,并且在实际中进行了测试,每个实例至少存放了 2 亿 5 千万的 keys。我们正在测试一些较大的值。任何 list、set、和 sorted set 都可以放 2^32 个元素。换句话说,Redis 的存储极限是系统中的可用内存值。


24、MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据?


答:Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。


相关知识:Redis 提供 6 种数据淘汰策略:


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(驱逐):禁止驱逐数据


25、Redis 最适合的场景?


1、会话缓存(Session Cache)


最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台 Magento 也提供 Redis 的插件。


2、全页缓存(FPC)


除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。 此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。


3、队列


Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。 如果你快速的在 Google 中搜索“Redis queues”,你马上就能找到大量的开源项目,这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。


4,排行榜/计数器


Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“userscores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行: ZRANGE userscores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。


5、发布/订阅


最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!


26、假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?


答:使用 keys 指令可以扫出指定模式的 key 列表。


对方接着追问:如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?


这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。


27、如果有大量的 key 需要设置同一时间过期,一般需要注意什么?


答:如果大量的 key 过期时间设置的过于集中,到过期的那个时间点,redis 可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。


28、使用过 Redis 做异步队列么,你是怎么用的?


一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。如果对方追问可不可以不用 sleep 呢?list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。如果对方追问能不能生产一次消费多次呢?使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。


如果对方追问 pub/sub 有什么缺点?


在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ 等。


如果对方追问 redis 如何实现延时队列?


我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用 sortedset,拿时间戳作为 score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。


29、使用过 Redis 分布式锁么,它是什么回事?


先拿 setnx 来争抢锁,抢到之后,再用 expire 给锁加一个过期时间防止锁忘记了释放。


这时候对方会告诉你说你回答得不错,然后接着问如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样?这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得 set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用的!


最后附上学习图谱:


ps:资源过大,需要的话可以直接添加助手 vx:bjmsb10;免费获取

并发编程(15 题)


1、现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?


这个线程问题通常会在第一轮或电话面试阶段被问到,目的是检测你对”join”方法是否熟悉。这个多线程问题比较简单,可以用 join 方法实现。


2、在 Java 中 Lock 接口比 synchronized 块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它?


lock 接口在多线程和并发编程中最大的优势是它们为读和写分别提供了锁,它能满足你写像 ConcurrentHashMap 这样的高性能数据结构和有条件的阻塞。Java 线程面试的问题越来越会根据面试者的回答来提问。我强烈建议在你去参加多线程的面试之前认真读一下 Locks,因为当前其大量用于构建电子交易终统的客户端缓存和交易连接空间。


3、在 java 中 wait 和 sleep 方法的不同?


通常会在电话面试中经常被问到的 Java 线程面试问题。最大的不同是在等待时 wait 会释放锁,而 sleep 一直持有锁。Wait 通常被用于线程间交互,sleep 通常被用于暂停执行。


4、用 Java 实现阻塞队列。


这是一个相对艰难的多线程面试问题,它能达到很多的目的。第一,它可以检测侯选者是否能实际的用 Java 线程写程序;第二,可以检测侯选者对并发场景的理解,并且你可以根据这个问很多问题。如果他用 wait()和 notify()方法来实现阻塞队列,你可以要求他用最新的 Java 5 中的并发类来再写一次。


5、用 Java 写代码来解决生产者——消费者问题。


与上面的问题很类似,但这个问题更经典,有些时候面试都会问下面的问题。在 Java 中怎么解决生产者——消费者问题,当然有很多解决方法,我已经分享了一种用阻塞队列实现的方法。有些时候他们甚至会问怎么实现哲学家进餐问题。


6、用 Java 编程一个会导致死锁的程序,你将怎么解决?


这是我最喜欢的 Java 线程面试问题,因为即使死锁问题在写多线程并发程序时非常普遍,但是很多侯选者并不能写 deadlock free code(无死锁代码?),他们很挣扎。只要告诉他们,你有 N 个资源和 N 个线程,并且你需要所有的资源来完成一个操作。为了简单这里的 n 可以替换为 2,越大的数据会使问题看起来更复杂。通过避免 Java 中的死锁来得到关于死锁的更多信息。


7、什么是原子操作,Java 中的原子操作是什么?


非常简单的 java 线程面试问题,接下来的问题是你需要同步一个原子操作。


8、 Java 中的 volatile 关键是什么作用?怎样使用它?在 Java 中它跟 synchronized 方法有什么不同?


自从 Java 5 和 Java 内存模型改变以后,基于 volatile 关键字的线程问题越来越流行。应该准备好回答关于 volatile 变量怎样在并发环境中确保可见性。


9、 什么是竞争条件?你怎样发现和解决竞争?


这是一道出现在多线程面试的高级阶段的问题。大多数的面试官会问最近你遇到的竞争条件,以及你是怎么解决的。有些时间他们会写简单的代码,然后让你检测出代码的竞争条件。可以参考我之前发布的关于 Java 竞争条件的文章。在我看来这是最好的 java 线程面试问题之一,它可以确切的检测候选者解决竞争条件的经验,or writing code which is free of data race or anyother race condition。关于这方面最好的书是《Concurrency practices in Java》。


10、 你将如何使用 threaddump?你将如何分析 Thread dump?


在 UNIX 中你可以使用 kill -3,然后 thread dump 将会打印日志,在 windows 中你可以使用”CTRL+Break”。非常简单和专业的线程面试问题,但是如果他问你怎样分析它,就会很棘手。


11、为什么我们调用 start()方法时会执行 run()方法,为什么我们不能直接调用 run()方法?


这是另一个非常经典的 java 多线程面试问题。这也是我刚开始写线程程序时候的困惑。现在这个问题通常在电话面试或者是在初中级 Java 面试的第一轮被问到。这个问题的回答应该是这样的,当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码。阅读我之前写的《start 与 run 方法的区别》这篇文章来获得更多信息。


12、Java 中你怎样唤醒一个阻塞的线程?


这是个关于线程和阻塞的棘手的问题,它有很多解决方法。如果线程遇到了 IO 阻塞,我并且不认为有一种方法可以中止线程。如果线程因为调用 wait()、sleep()、或者 join()方法而导致的阻塞,你可以中断线程,并且通过抛出 InterruptedException 来唤醒它。我之前写的《How to deal with blocking methods in java》有很多关于处理线程阻塞的信息。


13、在 Java 中 CycliBarriar 和 CountdownLatch 有什么区别?


这个线程问题主要用来检测你是否熟悉 JDK5 中的并发包。这两个的区别是 CyclicBarrier 可以重复使用已经通过的障碍,而 CountdownLatch 不能重复使用。


14、什么是不可变对象,它对写并发应用有什么帮助?


另一个多线程经典面试问题,并不直接跟线程有关,但间接帮助很多。这个 java 面试问题可以变的非常棘手,如果他要求你写一个不可变对象,或者问你为什么 String 是不可变的。


15、你在多线程环境中遇到的常见的问题是什么?你是怎么解决它的?


多线程和并发程序中常遇到的有 Memory-interface、竞争条件、死锁、活锁和饥饿。问题是没有止境的,如果你弄错了,将很难发现和调试。这是大多数基于面试的,而不是基于实际应用的 Java 线程问题。


HashMap(21 题)


1、HashMap 的数据结构?


A:哈希表结构(链表散列:数组+链表)实现,结合数组和链表的优点。当链表长度超过 8 时,链表转换为红黑树。


transient Node<K,V>[] table;1
复制代码


2、HashMap 的工作原理?


HashMap 底层是 hash 数组和单向链表实现,数组中的每个元素都是链表,由 Node 内部类(实现 Map.Entry 接口)实现,HashMap 通过 put & get 方法存储和获取对象。


存储对象时,将 K/V 键值传给 put() 方法:


①、调用 hash(K) 方法计算 K 的 hash 值,然后结合数组长度,计算得数组下标;

②、调整数组大小(当容器中的元素个数大于 capacity * loadfactor 时,容器会进行扩容 resize 为 2n);

③、i.如果 K 的 hash 值在 HashMap 中不存在,则执行插入,若存在,则发生碰撞;ii.如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 true,则更新键值对;iii. 如果 K 的 hash 值在 HashMap 中存在,且它们两者 equals 返回 false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式)。(JDK 1.7 之前使用头插法、JDK 1.8 使用尾插法)(注意:当碰撞导致链表大于 TREEIFY_THRESHOLD = 8 时,就把链表转换成红黑树)


获取对象时,将 K 传给 get() 方法:


①、调用 hash(K) 方法(计算 K 的 hash 值)从而获取该键值所在链表的数组下标;

②、顺序遍历链表,equals()方法查找相同 Node 链表中 K 值对应的 V 值。


hashCode 是定位的,存储位置;

equals 是定性的,比较两者是否相等。


3、当两个对象的 hashCode 相同会发生什么?


因为 hashCode 相同,不一定就是相等的(equals 方法比较),所以两个对象所在数组的下标相同,"碰撞"就此发生。又因为 HashMap 使用链表存储对象,这个 Node 会存储到链表中。


4、你知道 hash 的实现吗?为什么要这样实现?


JDK 1.8 中,是通过 hashCode() 的高 16 位异或低 16 位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度,功效和质量来考虑的,减少系统的开销,也不会造成因为高位没有参与下标的计算,从而引起的碰撞。


5、为什么要用异或运算符?


保证了对象的 hashCode 的 32 位值只要有一位发生改变,整个 hash() 返回值就会改变。尽可能的减少碰撞。


6、HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题?


①、table 数组大小是由 capacity 这个参数确定的,默认是 16,也可以构造时传入,最大限制是 1<<30;

②、loadFactor 是装载因子,主要目的是用来确认 table 数组是否需要动态扩展,默认值是 0.75,比如 table 数组大小为 16,装载因子为 0.75 时,threshold 就是 12,当 table 的实际大小超过 12 时,table 就需要动态扩容;

③、扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold)

④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。


7、HashMap 中 put 方法的过程?


A:调用哈希函数获取 Key 对应的 hash 值,再计算其数组下标;

如果没有出现哈希冲突,则直接放入数组;如果出现哈希冲突,则以链表的方式放在链表后面;

如果链表长度超过阀值( TREEIFY THRESHOLD==8),就把链表转成红黑树,链表长度低于 6,就把红黑树转回链表;

如果结点的 key 已经存在,则替换其 value 即可;

如果集合中的键值对大于 12,调用 resize 方法进行数组扩容。


8、数组扩容的过程?


创建一个新的数组,其容量为旧数组的两倍,并重新计算旧数组中结点的存储位置。结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小。


9、拉链法导致的链表过深问题为什么不用二叉查找树代替,而选择红黑树?为什么不一直使用红黑树?


之所以选择红黑树是为了解决二叉查找树的缺陷,二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成很深的问题),遍历查找会非常慢。

而红黑树在插入新数据后可能需要通过左旋,右旋、变色这些操作来保持平衡,引入红黑树就是为了查找数据快,解决链表查询深度的问题,我们知道红黑树属于平衡二叉树,但是为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少,所以当长度大于 8 的时候,会使用红黑树,如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。


10、说说你对红黑树的见解?


每个节点非红即黑

根节点总是黑色的

如果节点是红色的,则它的子节点必须是黑色的(反之不一定)

每个叶子节点都是黑色的空节点(NIL 节点)

从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)


11、jdk8 中对 HashMap 做了哪些改变?


在 java 1.8 中,如果链表的长度超过了 8,那么链表将转换为红黑树。(桶的数量必须大于 64,小于 64 的时候只会扩容,当容量达到 64 时才可以树化);

发生 hash 碰撞时,java 1.7 会在链表的头部插入,而 java 1.8 会在链表的尾部插入;

在 java 1.8 中,Entry 被 Node 替代(换了一个马甲)。


12、HashMap,LinkedHashMap,TreeMap 有什么区别?


LinkedHashMap 保存了记录的插入顺序,在用 Iterator 遍历时,先取到的记录肯定是先插入的;遍历比 HashMap 慢;

TreeMap 实现 SortMap 接口,能够把它保存的记录根据键排序(默认按键值升序排序,也可以指定排序的比较器)


13、HashMap & TreeMap & LinkedHashMap 使用场景?


一般情况下,使用最多的是 HashMap。

HashMap:在 Map 中插入、删除和定位元素时;

TreeMap:在需要按自然顺序或自定义顺序遍历键的情况下;

LinkedHashMap:在需要输出的顺序和输入的顺序相同的情况下。


14、HashMap 和 HashTable 有什么区别?


①、HashMap 是线程不安全的,HashTable 是线程安全的;

②、由于线程安全,所以 HashTable 的效率比不上 HashMap;

③、HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null,而 HashTable 不允许;

④、HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时,扩大两倍,后者扩大两倍+1;

⑤、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode


15、Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?


ConcurrentHashMap 类(是 Java 并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。

HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);

而针对 ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中直接采用了 CAS(无锁算法)+ synchronized。


16、HashMap & ConcurrentHashMap 的区别?


除了加锁,原理上无太大区别。另外,HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。


17、为什么 ConcurrentHashMap 比 HashTable 效率要高?


HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程竞争一把锁,容易阻塞;

ConcurrentHashMap:


JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。

JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结点)(实现 Map.Entry)。锁粒度降低了。


18、针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?


JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。

①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;

②、HashEntry 用来封装映射表的键-值对;

③、每个桶是由若干个 HashEntry 对象链接起来的链表



JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 HashEntry 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。



19、ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?


①、粒度降低了;

②、JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。

③、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

20.ConcurrentHashMap 简单介绍?

①、重要的常量:


private transient volatile int sizeCtl;1
复制代码


当为负数时,-1 表示正在初始化,-N 表示 N - 1 个线程正在进行扩容;


当为 0 时,表示 table 还没有初始化;


当为其他正数时,表示初始化或者下一次进行扩容的大小。


②、数据结构:


Node 是存储结构的基本单元,继承 HashMap 中的 Entry,用于存储数据;


TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储结构,用于红黑树中存储数据;


TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。


③、存储对象时(put() 方法):


如果没有初始化,就调用 initTable() 方法来进行初始化;


如果没有 hash 冲突就直接 CAS 无锁插入;


如果需要扩容,就先进行扩容;


如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;


如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一次进入循环;如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。


④、扩容方法 transfer():默认容量为 16,扩容时,容量变为原来的两倍。

helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。

⑤、获取对象时(get()方法):


计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;


如果遇到扩容时,会调用标记正在扩容结点 ForwardingNode.find()方法,查找该结点,匹配就返回;


以上都不符合的话,就往下遍历结点,匹配就返回,否则最后就返回 null。


21、ConcurrentHashMap 的并发度是什么?


程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。

当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际并发度则为 32)


用户头像

还未添加个人签名 2020.09.07 加入

还未添加个人简介

评论

发布
暂无评论
为了面个好公司!拼了!3.5W字的Java面试题整理(答案+学习路线)上!