Java面试题

Posted by Kaka Blog on March 3, 2020

Java多线程

1. 线程池的原理,为什么要创建线程池?

线程池的原理:

  • 核心线程(corePool):线程池最终执行任务的角色肯定还是线程,同时我们也会限制线程的数量,所以我们可以这样理解核心线程,有新任务提交时,首先检查核心线程数,如果核心线程都在工作,而且数量也已经达到最大核心线程数,则不会继续新建核心线程,而会将任务放入等待队列。
  • 等待队列 (workQueue):等待队列用于存储当核心线程都在忙时,继续新增的任务,核心线程在执行完当前任务后,也会去等待队列拉取任务继续执行,这个队列一般是一个线程安全的阻塞队列,它的容量也可以由开发者根据业务来定制。
  • 非核心线程:当等待队列满了,如果当前线程数没有超过最大线程数,则会新建线程执行任务,那么核心线程和非核心线程到底有什么区别呢?说出来你可能不信,本质上它们没有什么区别,创建出来的线程也根本没有标识去区分它们是核心还是非核心的,线程池只会去判断已有的线程数(包括核心和非核心)去跟核心线程数和最大线程数比较,来决定下一步的策略。
  • 线程活动保持时间 (keepAliveTime):线程空闲下来之后,保持存货的持续时间,超过这个时间还没有任务执行,该工作线程结束。
  • 饱和策略 (RejectedExecutionHandler):当等待队列已满,线程数也达到最大线程数时,线程池会根据饱和策略来执行后续操作,默认的策略是抛弃要加入的任务。

为什么要创建线程池:

  • 创建线程需要分配本地方法栈、虚拟机栈、程序计数器等内存空间;销毁线程需要回收所分配的资源;创建线程池可以减少两部分的消耗。
  • 周期任务,定时执行等与时间相关的功能;
  • 复用线程、控制最大并发数目;
  • 隔离线程环境。

2. 线程的生命周期,什么时候会出现僵死进程?

img

当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的全部资源,与堵塞状态不同)。进入这个状态后。是不能自己主动唤醒的,必须依靠其它线程调用notify()或notifyAll()方法才干被唤醒(因为notify()仅仅是唤醒一个线程,但我们由不能确定详细唤醒的是哪一个线程。或许我们须要唤醒的线程不可以被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池。等待获取锁标记。

在linux\unix中,正常情况下子进程是通过父进程创建的,子进程的而结束和父进程的运行是一个异步运行过程(父进程永远无法预测子进程什么时候结束)。当一个进程完成它的工作而终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态。一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或者waitpid获取子进程的状态信息,那么子进程的进程描述符等一系列信息还会保存在系统中。这种进程称之为僵死进程。

3. 什么是线程安全,如何实现线程安全?

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。

如何实现线程安全:

  • 第一种 : 互斥同步

使用Synchronized 关键字,Java中的每一个对象都可以作为锁。一个线程进入监视器(可以认为是一个只允许一个线程进入的盒子),其他线程必须等待,直到那个线程退出监视器为止。

  • 第二种:非阻塞同步

因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置,旧的预期值,新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。

  • 第三种:无同步方案

线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。经常使用的就是ThreadLocal类,最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等。

其实引起线程不安全最根本的原因就是:线程对于共享数据的更改会引起程序结果错误。线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。

4. 创建线程池有哪几个核心参数?如何合理配置线程池的大小?

  • corePoolSize(核心线程数)

    (1)核心线程会一直存在,即使没有任务执行; (2)当线程数小于核心线程数的时候,即使有空闲线程,也会一直创建线程直到达到核心线程数; (3)设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭。

  • queueCapacity(任务队列容量)

    也叫阻塞队列,当核心线程都在运行,此时再有任务进来,会进入任务队列,排队等待线程执行。

  • maxPoolSize(最大线程数)

    (1)如果当前运行的线程数大小小于corePoolSize,则不管是否有空闲线程,都会创建一个新的线程来运行任务。 (2)如果当前运行的线程池容量大于corePoolSize,小于maxPoolSize,只有在队列满的时候才会创建新的线程。 (3)

  • keepAliveTime(线程空闲时间)

    (1)当线程空闲时间达到keepAliveTime时,线程会退出(关闭),直到线程数等于核心线程数; (2)如果设置了allowCoreThreadTimeout=true,则线程会退出直到线程数等于零。

  • allowCoreThreadTimeout(允许核心线程超时)
  • rejectedExecutionHandler(任务拒绝处理器)

    定义了四种处理策略

一般需要根据任务的类型来配置线程池大小:

  • 如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
  • 如果是IO密集型任务,参考值可以设置为2*NCPU

5. synchronized、volatile区别、synchronized锁粒度、模拟死锁场景、原子性与可见性;

synchronized、volatile区别

1)volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞。 2)volatile仅能使用在变量级别,synchronized则可以使用在变量、方法。 3)volatile仅能实现变量修改的可见性,而synchronized则可以保证变量修改的可见性和原子性。《Java编程思想》上说,定义long或double时,如果使用volatile关键字(简单的赋值与返回操作),就会获得原子性。(常规状态下,这两个变量由于其长度,其操作不是原子的) 4)volatile不会造成线程阻塞,synchronized会造成线程阻塞。 5)使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域。

synchronized锁粒度

1、修饰方法:在范围操作符之后,返回类型声明之前使用。每次只能有一个线程进入该方法,此时线程获得的是成员锁。 2、修饰代码块:每次只能有一个线程进入该代码块,此时线程获得的是成员锁。 3、修饰对象:如果当前线程进入,那么其他线程在该类所有对象上的任何操作都不能进行,此时当前线程获得的是对象锁。 4、修饰类:如果当前线程进入,那么其他线程在该类中所有操作不能进行,包括静态变量和静态方法,此时当前线程获得的是对象锁。

模拟死锁场景

模拟死锁场景

原子性与可见性

原子性指一个操作不能被打断,要么全部执行完毕,要么不执行。除了 long 和 double 型变量,java 内存模型确保访问任意类型变量所对应的内存单元都是原子的,包括引用类型的字段。long 和 double 类型的变量是64位。在32位 JVM 中,64位数据的读写操作会分为2次32位的读写操作来进行,因此 long、double 类型的变量在32位虚拟机中是非原子操作(在64位 JVM 下具有原子性)。原子性问题只存在于对实例变量、静态变量、数组元素的读写操作,不包括局部变量。在 Java 中,可以通过 synchronized、Lock实现原子性。

可见性是指一个线程对共享变量做了修改之后,其它线程立即能够看到(感知到)该变量的变化。在 Java 中,可以通过 volatile、synchronized、Lock、final 实现可见性。

JVM相关

JVM内存模型

JVM内存空间分为五部分,分别是:方法区、堆、Java虚拟机栈、本地方法栈、程序计数器。

  • 方法区主要用来存放类信息、类的静态变量、常量、运行时常量池等,方法区的大小是可以动态扩展的。
  • 堆主要存放的是数组、类的实例对象、字符串常量池等。堆的大小可以通过-Xmx和-Xms来控制。
  • Java虚拟机栈是描述JAVA方法运行过程的内存模型,Java虚拟机栈会为每一个即将执行的方法创建一个叫做“栈帧”的区域,该区域用来存储该方法运行时需要的一些信息,包括:局部变量表、操作数栈、动态链接、方法返回地址等。
  • 本地方法栈结构上和Java虚拟机栈一样,只不过Java虚拟机栈是运行Java方法的区域,而本地方法栈是运行本地方法的内存模型。运行本地方法时也会创建栈帧,同样栈帧里也有局部变量表、操作数栈、动态链接和方法返回地址等。
  • 程序计数器是一个比较小的内存空间,用来记录当前线程正在执行的那一条字节码指令的地址。如果当前线程正在执行的是本地方法,那么此时程序计数器为空。程序计数器有两个作用,1、字节码解释器通过改变程序计数器来一次读取指令,从而实现代码的流程控制,比如我们常见的顺序、循环、选择、异常处理等。2、在多线程的情况下,程序计数器用来记录当前线程执行的位置,当线程切换回来的时候仍然可以知道该线程上次执行到了哪里。而且程序计数器是唯一一个不会出现OutOfMeroryError的内存区域。

方法区和堆都是线程共享的,在JVM启动时创建,在JVM停止时销毁,而Java虚拟机栈、本地方法栈、程序计数器是线程私有的,随线程的创建而创建,随线程的结束而死亡。

GC机制和原理;GC分哪两种;什么时候会触发Full GC?

对象在Eden Space创建,当Eden Space满了的时候,gc就把所有在Eden Space中的对象扫描一次,把所有有效的对象复制到第一个Survivor Space,同时把无效的对象所占用的空间释放。当Eden Space再次变满了的时候,就启动移动程序把Eden Space中有效的对象复制到第二个Survivor Space,同时,也将第一个Survivor Space中的有效对象复制到第二个Survivor Space。如果填充到第二个Survivor Space中的有效对象被第一个Survivor Space或Eden Space中的对象引用,那么这些对象就是长期存在的,此时这些对象将被复制到Permanent Generation。若垃圾收集器依据这种小幅度的调整收集不能腾出足够的空间,就会运行Full GC,此时JVM GC停止所有在堆中运行的线程并执行清除动作。

Minor GC触发条件:当Eden区满时,触发Minor GC。 Full GC触发条件: (1)调用System.gc()时,系统建议执行Full GC,但是不必然执行 (2)老年代空间不足 (3)方法区空间不足 (4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存 (5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。

JVM里的有几种classloader,为什么会有多种?

类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java执行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoader:根装载器、ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。其中,根装载器不是ClassLoader的子类,它使用C++编写,因此我们在Java中看不到它,根装载器负责装载JRE的核心类库,如JRE目标下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中ExtClassLoader负责装载JRE目录ext中的JAR类包;AppClassLoader负责装载ClassPath路径下的类包。

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中默认的类加载器。

什么是双亲委派机制?介绍一些运作过程,双亲委派模型的好处;

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.class,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

什么情况下我们需要破坏双亲委派模型;

双亲委派模型很好的解决了各个类加载器加载基础类的统一性问题。即越基础的类由越上层的加载器进行加载。 若加载的基础类中需要回调用户代码,而这时顶层的类加载器无法识别这些用户代码,怎么办呢?这时就需要破坏双亲委派模型了。 下面介绍两个例子来讲解破坏双亲委派模型的过程。

  1. JNDI破坏双亲委派模型 JNDI是Java标准服务,它的代码由启动类加载器去加载。但是JNDI需要回调独立厂商实现的代码,而类加载器无法识别这些回调代码(SPI)。 为了解决这个问题,引入了一个线程上下文类加载器。 可通过Thread.setContextClassLoader()设置。 利用线程上下文类加载器去加载所需要的SPI代码,即父类加载器请求子类加载器去完成类加载的过程,而破坏了双亲委派模型。

  2. Spring破坏双亲委派模型 Spring要对用户程序进行组织和管理,而用户程序一般放在WEB-INF目录下,由WebAppClassLoader类加载器加载,而Spring由Common类加载器或Shared类加载器加载。 那么Spring是如何访问WEB-INF下的用户程序呢? 使用线程上下文类加载器。 Spring加载类所用的classLoader都是通过Thread.currentThread().getContextClassLoader()获取的。当线程创建时会默认创建一个AppClassLoader类加载器(对应Tomcat中的WebAppclassLoader类加载器): setContextClassLoader(AppClassLoader)。 利用这个来加载用户程序。即任何一个线程都可通过getContextClassLoader()获取到WebAppclassLoader。

常见的JVM调优方法有哪些?可以具体到调整哪个参数,调成什么值?

1、将新对象预留在新生代

由于 Full GC 的成本要远远高于 Minor GC ,因此尽可能将对象分配在新生代,在JVM 调优中,可以为应用程序分配一个合理的新生代空间,以最大限度避免新对象直接进去老年代。

2、大对象进入老年代

大对象占用空间多,直接放入新生代中会扰乱新生代GC,新生代空间不足将会把大量的较小的年轻代对象移入到老年代中,这对GC来说是相当不利的。如果有短命大对象,对GC来说将会是一场灾难,原本存放于老年代的永久对象,被短命大对象塞满,扰乱了分代内存回收的基本思路,因此,在开发过程中,尽可能避免使用短命的大对象。使用参数 -XX:PretenureSizeThreshold 设置大对象直接进入老年代的阀值,当对象超过这个阀值时,将直接在老年代中分配。其中, -XX:PretenureSizeThreshold 只对串行收集器和新生代并行收集器有效,并行回收收集器不识别这个参数。

3、设置对象进入老年代的年龄

在堆中每个对象都有自己的年龄,如果对象在 eden 区,经过一次 GC 后还存活,则被移动到 survivor 区中,对象年龄加 1,以后每经过一次 GC 依然存活的,对象年龄就加 1。当对象年龄达到阀值时,就移动到老年代,这个阀值用以下参数设置:

-XX:MaxTenuringThreshold:默认值是15,这个参数是指定进入老年代的最大年龄值,对象实际进入老年代的年龄是 JVM 在运行时根据内存使用情况动态计算的。

4、稳定与震荡的堆大小

稳定的堆大小对垃圾回收是有利的,获得一个稳定堆大小的方法就是设置 -Xmx 和 -Xms 一样的值。不稳定的堆也不是木有用处,让堆大小在一个区间内震荡,在系统不需要使用大内存时压缩堆空间,使 GC 应对一个较小的堆,可以加快单次 GC 的速度。基于这种思想,JVM 提供了两个参数用于压缩和扩展堆空间,参数如下:

-XX:MinHeapFreeRatio:设置堆空间最小空闲比例,默认是 40 ,当堆空间的空闲比例小于这个值时,JVM 便会扩展堆空间

-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例,默认是 70,当堆空间的空闲比例大于这个值时,JVM 便会压缩堆空间,得到一个较小的堆

注意:当 -Xms 和 -Xmx 相等时,-XX:MinHeapFreeRatio 和 -XX:MaxHeapFreeRatio 这两个参数无效

5、吞吐量优先设置

机器配置是 4G 内存 和 32 核 CPU,配置参数如下: -Xms3800m -Xmx3800m(堆的初始值和最大值一样) -Xmn2g(新生代大小) -Xss128k(线程栈大小,减少它使剩余的系统内存支持更多的线程) -XX:+UseParallelGC(新生代使用并行回收收集器) -XX:ParallelGCThreads=20(垃圾回收的线程数) -XX:+UseParallelOldGC (老年代使用并行回收收集器)

6、使用大页案例

使用大的内存分页可以增强 CPU 的内存寻址能力,从而提高系统的性能,参数设置如下: -XX:LargePageSizeInBytes:设置大页的大小

7、降低停顿案例

为了降低应用软件在垃圾回收时的停顿,首先考虑的使用关注系统停顿的 CMS 回收器,为了减少 Full GC 的次数,应尽可能将对象预留在新生代,新生代 Minor GC 的成本远远小于老年代的 Full GC

-Xms3800m -Xmx3800m(堆的初始值和最大值一样) -Xmn2g(新生代大小) -Xss128k(线程栈大小,减少它使剩余的系统内存支持更多的线程) -XX:ParallelGCThreads=20(垃圾回收的线程数) -XX:+UseConcMarkSweepGC(老年代使用 CMS 收集器) -XX:+UseParNewGC(新生代使用并行收集器) -XX:SurvivorRatio=8(设置 eden : survivor = 8 : 1) -XX:TargetSurvivorRatio(设置 survivor 的使用率为 90%,默认是50%,提高了survivor 区的使用率,当存放的对象超过这个数值,则对象会向老年代压缩) -XX:MaxTenuringThreshold=31(设置年轻对象晋升到老年代的最大年龄是31,默认是15,设为31是尽可能地将对象留在新生代)

JVM垃圾收集算法

  • 引用计数法

引用计数法实现简单,效率较高,在大部分情况下是一个不错的算法。其原理是:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加1,当引用失效时,计数器减1,当计数器值为0时表示该对象不再被使用。需要注意的是:引用计数法很难解决对象之间相互循环引用的问题,主流Java虚拟机没有选用引用计数法来管理内存。

  • 标记-清除算法(Mark-Sweep)

这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。采用从根集合(GC Roots)进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收。但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

  • 复制算法(Copying)

为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。

  • 标记-整理算法(Mark-compact)

为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。

  • 分代收集算法 Generational Collection

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。目前大部分垃圾收集器对于新生代都采取Copying算法,而由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

垃圾收集器

常见有7种垃圾收集器,分别作用于不同的分代,如果两个收集器之间存在连续,就说明他们可以搭配使用。从JDK1.3到现在,从Serial收集器>Parallel收集器>CMS>G1,用户线程停顿时间不断缩短,但仍然无法完全消除。

1、Serial收集器(串行收集器)

它只会使用一个CPU或一条收集器线程去完成垃圾收集工作,更重要的是它在垃圾收集的时候,必须暂停其他所有工作的线程,直到它收集结束。是HotSpot虚拟机运行在Client模式下的默认新生代收集器。”-XX:+UseSerialGC”:添加该参数来显式的使用Serial垃圾收集器。

2、ParNew收集器

ParNew收集器是Serial收集器的多线程版本。除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Word、对象分配规则、回收策略等都与Serial收集器一样。

"-XX:+UseConcMarkSweepGC":指定使用CMS后,会默认使用ParNew作为新生代收集器。
"-XX:+UseParNewGC":强制指定使用ParNew。   
"-XX:ParallelGCThreads":指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。

3、Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法,且是并行的多线程收集器。关注点是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)),而其他收集器关注点在尽可能的缩短垃圾收集时用户线程的停顿时间。 一是控制最大垃圾收集停顿时间的 -XX:MaxGCPauseMillis参数,二是控制吞吐量大小的 -XX:GCTimeRatio参数

4、Serial Old收集器

Serial Old收集器是Seria收集器的老年代版本,他同样是一个单线程收集器,使用” 标记-整理” 算法。

5、Paraller Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。在JDK1.6中才出现。

6、CMS(Concurrent Mark Sweep)收集器

CMS垃圾回收器的全称是Concurrent Mark-Sweep Collector,从名字上可以看出两点,一个是使用的是并发收集,第二个是使用的收集算法是Mark-Sweep,是基于“标记-清除”算法实现。从而也可以推测出该收集器的特点是低延迟并且会有浮动垃圾的问题。CMS收集器是一种以获取最短回收停顿时间为目标的收集器。CMS默认启动的回收线程数是(CPU数量+3)/4。主要分为初始标记、并发标记、再次标记和并发清除阶段。

7、G1(Garbage-First)收集器

G1收集器是当今收集器技术发展的最前沿成果之一;面向服务端的垃圾收集器。

并行与并发:充分利用多核环境减少停顿时间,
分代收集:不需要配合其它收集器
空间整合:整体上看属于标记整理算法,局部(region之间)数据复制算法,运作期间不会产生空间碎片
停顿可预测,建立可以预测的停顿时间模型。

内存管理:

将整个java堆划分为多个大小形同的区域region,新生代和老年代都是region的集合。可以有计划的避免在全区域内进行垃圾收集。
回收方式:跟踪每一个region里面的垃圾堆积的价值大小(回收所得的空间大小以及所需耗费时间的经验值),维护一个优先列表,每次根据允许的回收时间,优先回收价值最大的region(GI名字由来),
region之间的引用,新生代和老年带之间的引用根据remebered set来避免全盘扫描,每一个region都维护一个remebered set,
初始标记-》并发标记-》最终标记-》筛选回收,类CMS

class文件结构是如何解析的;

java class文件结构是基于字节流的,用unicode进行编码。Class文件结构中只有2种数据类型:无符号数和表。无符号数, 属于基本的数据类型,以u1、u2、u4、u8来分别表示1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值和UTF-8编码构成的字符串;表,是由多个无符号数或其他表作为数据项构成的复合数据类型,所有的表都习惯已”_info”结尾(整个Class文件可以看成是一张表)。

Java扩展

1. 红黑树的实现原理和应用场景;

在了解红黑树前需要先了解什么是平衡二叉查找树,它的查找效率非常稳定,是O(log n),由于严格按照左右子树高度差不大于一的规则,插入和删除操作中需要大量的操作来保持树的平衡,比较耗时。主要操作时左旋和右旋:左旋就是左边的节点降下来,右旋就是右边的节点降下来,都成为中间节点的子树。

红黑树的产生就是通过降低平衡性的要求,来提高插入删除的效率。红黑树需要满足以下要求:

1.树中的节点有两种标记,红色节点和黑色节点;

2.根节点为黑色;

3.每个叶子节点都是空的黑色节点;

4.两个红色节点不能相邻;

5.每个节点,从它开始往叶子节点走的所有路径,每条路径都包含相同数量的黑色节点;

红黑树实际应用:

  • IO多路复用epoll的实现采用红黑树组织管理sockfd,以支持快速的增删改查.
  • ngnix中,用红黑树管理timer,因为红黑树是有序的,可以很快的得到距离当前最小的定时器.
  • java中TreeMap,jdk1.8的hashmap的实现.

模拟红黑树插入删除过程

NIO是什么?适用于何种场景?

NIO是为了弥补IO操作的不足而诞生的,NIO的一些新特性有:非阻塞I/O,选择器,缓冲以及管道。管道(Channel),缓冲(Buffer) ,选择器( Selector)是其主要特征。同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。

NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。

Java9比Java8改进了什么;

  • 私有接口方法:在接口中使用private私有方法。我们可以使用 private 访问修饰符在接口中编写私有方法。
  • 改进 Optional 类:java.util.Optional 添加了很多新的有用方法,Optional 可以直接转为 stream。
  • 集合工厂方法:List,Set 和 Map 接口中,新的静态工厂方法可以创建这些集合的不可变实例。
  • 改进的 Stream API:改进的 Stream API 添加了一些便利的方法,使流处理更容易,并使用收集器编写复杂的查询。

HashMap内部的数据结构是什么?底层是怎么实现的?

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。当新建一个HashMap的时候,就会初始化一个数组。Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它持有一个指向下一个元素的引用,这就构成了链表。

实现原理:

JDK7中HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,hashMap默认的初始化长度为16,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。 JDK8中采用的是位桶+链表/红黑树的方式,也是非线程安全的。当某个位桶的链表的长度达到某个阀值的时候,这个链表就将转换成红黑树。当同一个hash值的节点数大于8时,将不再以单链表的形式存储了,会被调整成一颗红黑树。

数组长度为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。

HashMap 线程不安全的出现场景

1、put的时候导致的多线程数据不一致。 这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)

说说反射的用途及实现,反射是不是很慢,我们在项目中是否要避免使用反射;

反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

一、Java反射框架主要提供以下功能:

1.在运行时判断任意一个对象所属的类;

2.在运行时构造任意一个类的对象;

3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);

4.在运行时调用任意一个对象的方法

二、主要用途 :

1、反射最重要的用途就是开发各种通用框架。

三、基本反射功能的实现(反射相关的类一般都在java.lang.relfect包里):

1、获得Class对象

使用Class类的forName静态方法
直接获取某一个对象的class
调用某个对象的getClass()方法

2、判断是否为某个类的实例

用instanceof关键字来判断是否为某个类的实例

3、创建实例

使用Class对象的newInstance()方法来创建Class对象对应类的实例。
先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。

4、获取方法

getDeclaredMethods()

5、获取构造器信息

getDeclaredMethods()
getMethods()
getMethod()

6、获取类的成员变量(字段)信息

getFiled: 访问公有的成员变量
getDeclaredField:所有已声明的成员变量。但不能得到其父类的成员变量
getFileds和getDeclaredFields用法

7、调用方法

invoke()

8、利用反射创建数组

Array.newInstance()

四、注意:

由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

说说自定义注解的场景及实现;

注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。程序运行时,通过反射获取类中所有的属性和方法上的注解,这个注解是个动态代理对象$Proxy1。通过代理对象调用注解中自定义的方法,完成注解功能。

Object 的 hashcode 方法重写了,equals 方法要不要改?

重写equals()方法主要是为了方便比较两个对象的内容是否相等。hashCode()方法用于返回调用该方法的对象的散列码值,此方法返回整数类型的散列码值。一个类如果重写了equals()方法,通常也有必要重写hsahCode()方法。相等的对象必须有相同的散列码,反之散列码相同则不一定对象相等,而且不相等的对象并不一定需要有不同的散列码。例如HashTable、HashMap和HashSet等,在使用这些集合时,首先会根据元素对象的散列码值确定其存储位置,然后再根据equals()方法结果判断元素对象是否已存在,最后根据判断结果执行不同的处理。因此,实际应用时如果重写了equals()方法,那么hashCode()方法也会被重写。

在我们只重写hashCode方法时,对于两个对象来说可能会得到相同的hash值(取决于重写的hashCode方法),Java将进行下一步调用Object类的equals方法比较。由于没有重写equals方法,Object类的equals方法默认会比较内存地址(hashCode方法默认返回内存地址),不同对象内存地址是不同的,将判定这两个对象不等。因此要确定这两个对象到底相不相等,需要将equals方法也进行重写,按照我们的比较逻辑进一步判断。

Spring

Spring AOP的实现原理和场景;(应用场景很重要)

AOP 思想: 基于代理思想,对原来目标对象,创建代理对象,在不修改原对象代码情况下,通过代理对象,调用增强功能的代码,从而对原有业务方法进行增强 !

AOP应用场景:

场景一: 记录日志
场景二: 监控方法运行时间 (监控性能)
场景三: 权限控制
场景四: 缓存优化 (第一次调用查询数据库,将查询结果放入内存对象, 第二次调用, 直接从内存对象返回,不需要查询数据库 )
场景五: 事务管理 (调用方法前开启事务, 调用方法后提交关闭事务 )

AOP的实现原理 那Spring中AOP是怎么实现的呢?Spring中AOP的有两种实现方式:

1、JDK动态代理
2、Cglib动态代理
    在实际开发中,可能需要对没有实现接口的类增强,用JDK动态代理的方式就没法实现。采用Cglib动态代理可以对没有实现接口的类产生代理,实际上是生成了目标类的子类来增强。

Spring bean的作用域和生命周期;

SpringBean的作用域:

scope:设置bean的作用范围
singleton:单例(创建只有一个实例)
prototype:原型(创建多个实例)
request:对request请求创建新的bean
session:一次会话中创建同一个bean
global-session:所有会话共享一个bean

SpringBean的生命周期:

1、实例化bean对象(通过构造方法或者工厂方法)
2、设置对象属性(setter等)(依赖注入)
3、如果Bean实现了BeanNameAware接口,工厂调用Bean的setBeanName()方法传递Bean的ID。(和下面的一条均属于检查Aware接口)
4、如果Bean实现了BeanFactoryAware接口,工厂调用setBeanFactory()方法传入工厂自身
5、将Bean实例传递给Bean的前置处理器的postProcessBeforeInitialization(Object bean, String beanname)方法
6、调用Bean的初始化方法
7、将Bean实例传递给Bean的后置处理器的postProcessAfterInitialization(Object bean, String beanname)方法
8、使用Bean
9、容器关闭之前,调用Bean的销毁方法

Spring Boot比Spring做了哪些改进?Spring 5比Spring4做了哪些改进;(惭愧呀,我们还在用Spring4,高版本的没关心过)

Spring IOC是什么?优点是什么?

Ioc—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。在Java开发中,Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;反转则是由容器来帮忙创建及注入依赖对象;

IoC不是一种技术,只是一种思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了IoC容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

中间件

Dubbo完整的一次调用链路介绍;

Dubbo支持几种负载均衡策略?

Dubbo Provider服务提供者要控制执行并发请求上限,具体怎么做?

Dubbo启动的时候支持几种配置方式?

了解几种消息中间件产品?各产品的优缺点介绍;

消息中间件如何保证消息的一致性和如何进行消息的重试机制?

Spring Cloud熔断机制介绍;

Spring Cloud对比下Dubbo,什么场景下该使用Spring Cloud?

数据库篇

锁机制介绍:行锁、表锁、排他锁、共享锁;

乐观锁的业务场景及实现方式;

事务介绍,分布式事物的理解,常见的解决方案有哪些,什么事两阶段提交、三阶段提交;

MySQL记录binlog的方式主要包括三种模式?每种模式的优缺点是什么?

三种模式:

  • Statement Level模式 5.5默认模式

    每一条修改数据的sql都会记录到master的bin_log中,slave在复制的时候sql进程会解析成master端执行过的相同的sql在slave库上再次执行。

    • 优点:解决了row level的缺点,不需要记录每一行的变化。日志量少,节约IO,从库应用日志块。
    • 缺点:由于它是记录执行语句,所以,为了让这些语句在slave端也能正确执行,那么它还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,来保证所有语句在slave端能够得到和在master端相同的执行结果。比如:sleep()函数在有些版本中就不能正确赋值,在存储过程中使用了last_insert_id()函数,可能会使slave和master上得到不一致的id等等。
  • Row Level模式 5.7默认模式

    日志中会记录成每一行数据修改的形式,然后在slave端再对相同的数据进行修改。

    • 优点:在row level的模式下,bin_log中可以不记录执行的sql语句的上下文信息,仅仅只需要记录哪一条记录被修改,修改成什么样。所以row level的日志内容会非常清楚的记录每一行数据修改的细节,非常容易理解。而且不会出现某些特定情况下的存储过程,或fuction,以及trigger的调用或处罚无法被正确复制的问题。
    • 缺点:日志量大,因为是按行来拆分。比如有这样一条update语句:update top1 set name=‘tony’ 这条语句不是记录的一条,而是修改每一条的都会记录下来。
  • Mixed模式(混合模式)

    实际上就是前两种模式的结合,在mixed模式下,mysql会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也是在statement和row之间选择一种。

    • 优点:记录的简单,内容少
    • 缺点:导致主从不一致

查看当前使用类型

mysql> show variables like ‘%binlog_format%’;

MySQL锁,悲观锁、乐观锁、排它锁、共享锁、表级锁、行级锁;

分布式事务的原理2阶段提交,同步\异步\阻塞\非阻塞;

数据库事务隔离级别,MySQL默认的隔离级别

MySQL数据库为我们提供的事务的四种隔离级别:

1.Read uncommitted (读未提交):最低级别,任何情况都无法保证。

2.Read committed (读已提交):可避免脏读的发生。

3.Repeatable read (可重复读):可避免脏读、不可重复读的发生。

4.Serializable (串行化):可避免脏读、不可重复读、幻读的发生。

脏读:指在一个事务处理过程里读取了另一个未提交的事务中的数据。

不可重复读:指某个数据在一个事务范围内多次查询却返回了不同的数据值,也就是读取了前一事务提交的数据。

幻读:一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读)。 查看MySQL数据库当前事务的隔离级别:

select @@tx_isolation;

Spring如何实现事务

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。 Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务有两种方式,一种是在配置文件(xml)中做相关的事务规则声明,另一种是基于 @Transactional 注解的方式。

第一步,在 xml 配置文件中添加事务配置信息。除了用配置文件的方式,@EnableTransactionManagement 注解也可以启用事务管理功能。

第二步,将@Transactional 注解添加到合适的方法上,并设置合适的属性信息。除此以外,@Transactional 注解也可以添加到类级别上。当把@Transactional 注解放在类级别时,表示所有该类的公共方法都配置相同的事务属性信息。

在应用系统调用声明@Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据@Transactional 的属性配置信息,这个代理对象决定该声明@Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑, 最后根据执行情况是否出现异常,利用抽象事务管理器AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。不同的事务管理器管理不同的数据资源 DataSource,比如 DataSourceTransactionManager 管理 JDBC 的 Connection。

JDBC如何实现事务、嵌套事务实现、分布式事务实现;

Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。除此之外,从JDBC 3.0中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。

SQL的整个解析、执行过程原理、

SQL行转列;

行转列一般通过CASE WHEN 语句来实现,也可以通过 SQL SERVER 的运算符PIVOT来实现。

SELECT

  UserName,

  MAX(CASE Subject WHEN '语文' THEN Score ELSE 0 END) AS '语文',

  MAX(CASE Subject WHEN '数学' THEN Score ELSE 0 END) AS '数学',

  MAX(CASE Subject WHEN '英语' THEN Score ELSE 0 END) AS '英语',

  MAX(CASE Subject WHEN '生物' THEN Score ELSE 0 END) AS '生物'

FROM dbo.[StudentScores]

GROUP BY UserName

Redis

Redis为什么这么快?

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

redis采用多线程会有哪些问题?

Redis支持哪几种数据结构;

一 string(字符串)

  string是最简单的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value,其上支持的操作与Memcached的操作类似。但它的功能更丰富。

二 list(双向链表)

  list是一个链表结构,主要功能是push、pop、获取一个范围的所有值等等。之所以说它是双向的,因为它可以在链表左,右两边分别操作

三 dict(hash表)

  set是集合,和我们数学中的集合概念相似,对集合的操作有添加删除元素,有对多个集合求交并差等操作。操作中key理解为集合的名字

四 zset(排序set)

  zset是set的一个升级版本,他在set的基础上增加了一个顺序属性,这一属性在添加修改元素的时候可以指定,每次指定后,zset会自动重新按新的值调整顺序。 可以对指定键的值进行排序权重的设定,它应用排名模块比较多

五 Hash类型

Redis能够存储key对多个属性的数据(比如user1.uname user1.passwd),当然,你完成可以把这些属性以json格式进行存储,直接把它当作string类型进行操作,但这样性能上是对影响的,所以redis提出的Hash类型。

Redis跳跃表的问题;

Redis 的五种基本结构中,有一个叫做 有序列表 zset 的数据结构,它类似于 Java 中的 SortedSet 和 HashMap 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 排序 的目的。它的内部实现就依赖了一种叫做 「跳跃列表」 的数据结构。

单进程单线程的Redis如何能够高并发?

采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求

如何使用Redis实现分布式锁?

Redis分布式锁操作的原子性,Redis内部是如何实现的?