Java高级工程师面试题汇总(三)

1.catch Exception 和catch Throwable的区别

 解析:Java的异常体系

Throwable: Java中所有异常和错误类的父类。只有这个类的实例(或者子类的实例)可以被虚拟机抛出或者被java的throw关键字抛出。同样,只有其或其子类可以出现在catch子句里面。

Error: Throwable的子类,表示严重的问题发生了,而且这种错误是不可恢复的。

Exception: Throwable的子类,应用程序应该要捕获其或其子类(RuntimeException例外),称为checked exception。比如:IOException, NoSuchMethodException...

RuntimeException: Exception的子类,运行时异常,程序可以不捕获,称为unchecked exception。比如:NullPointException.

其实只要是Throwable和其子类都是可以throw和catch的,那么如果在需要统一处理异常的地方,我们应该catch (Throwable th) 还是 catch (Exception)呢?

这两种处理的区别在于,catch throwable会把Error和其他继承Throwable的类捕捉到。而catch Exception只会捕捉Exception极其子类,捕捉的范围更小。先不考虑有其他的类继承了Throwable的情况下,第一种catch相当于比第二种catch多捕捉了把Error和其子类。

那么究竟Error是否需要捕捉呢?JDK中Error类的的注释里提到过,Error是一种严重的问题,应用程序不应该捕捉它,因为Exception发生后可以进行一些恢复工作的,但是Error发生后一般是不可恢复的

 详见:如何处理异常? catch Exception OR catch Throwable

2.mysql组合索引与字段顺序

 解析:规则:1、需要加索引的字段,要在where条件中;2、数据量少的字段不需要加索引;3、如果where条件中是OR关系,加索引不起作用;4、组合索引的最左优先原则:组合索引的第一个字段必须出现在查询组句中,这个索引才会被用到。如果有一个组合索引(col_a,col_b,col_c),where条件中下面的情况都会用到这个索引:

对于最后一条语句,mysql会自动优化成第三条的样子。下面的情况就不会用到索引:

联合索引又叫复合索引。对于复合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部分,但只能是最左侧部分。例如索引是key index (a,b,c). 可以支持a | a,b| a,b,c 3种组合进行查找,但不支持 b,c进行查找 。当最左侧字段是常量引用时,索引就十分有效。所以说创建复合索引时,应该仔细考虑列的顺序。对索引中的所有列执行搜索或仅对前几列执行搜索时,复合索引非常有用;仅对后面的任意列执行搜索时,复合索引则没有用处。




 在mysql中执行查询时,只能使用一个索引,如果我们在多个字段上分别建索引,执行查询时,只能使用一个索引,mysql会选择一个最严格(获得结果集记录数最少)的索引。


在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。

 详见:mysql组合索引与字段顺序

3.BlockingQueue原理

 解析:阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

 put(anObject):把anObject加到BlockingQueue里,如果BlockQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续。take():取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态,直到 BlockingQueue有新的数据被加入。

ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了。

LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。

阻塞队列实现阻塞同步的方式很简单,使用的就是是lock锁的多条件(condition)阻塞控制

ThreadPoolExecutor线程池中:

(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, 
ThreadFactory threadFactory,RejectedExecutionHandler handler)

workQueue任务队列:用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行

排序。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通

常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则

插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.

newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

4.通过Callable和Future创建线程

 解析:和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。call()方法可以有返回,可以声明抛出异常。

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。创建并启动有返回值的线程的步骤如下:

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。

(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args) {
        CallableThreadTest ctt = new CallableThreadTest();
        FutureTask<Integer> ft = new FutureTask<>(ctt);
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
            if (i == 20) {
                new Thread(ft, "有返回值的线程").start();
            }
        }
        try {
            System.out.println("子线程的返回值:" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
        return i;
    }
}

 详见:java创建线程的三种方式及其对比  | java中创建线程的三种方法以及区别

5.MySQL InnoDB存储的文件结构

 解析:.frm与表相关的元数据信息都存放在frm文件,包括表结构的定义信息等。

.ibd文件和.ibdata文件:这两种文件都是存放innodb数据的文件,之所以用两种文件来存放innodb的数据,是因为innodb的数据存储方式能够通过配置来决定是使用共享表空间存放存储数据,还是用独享表空间存放存储数据。独享表空间存储方式使用.ibd文件,并且每个表一个ibd文件,共享表空间存储方式使用.ibdata文件,所有表共同使用一个ibdata文件。

6.JVM垃圾回收机制,何时触发MinorGC等操作

 解析:从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。这一定义既清晰又易于理解。但是,当发生Minor GC事件的时候,有一些有趣的地方需要注意到:

年轻代分为3块,Eden区和2个Survivor区。大多数情况下,对象在新生区Eden区中分配,当Eden没有足够空间分配时,虚拟机将发起一次Minor GC,存活的对象移到其中一个Survivor区。一旦这个Survivor区已满,存活的对象移动到另外一个Survivor区。然后之前那个空间已满Survivor区将置为空,没有任何数据。经过重复多次这样的步骤后依旧存活的对象将被移到老年代。

默认的比例为:Eden:from:to=8:1:1。注:可通过-XX:SurvivorRatio=i来设置,默认i=8。当Survivor空间不够时,需要依赖其他内存(老年代)进行分配担保。内存的分配担保:如果另一块Survivor空间没有足够的内存空间存放上一次新生代收集下来的存活对象,那么这些对象将直接通过分配担保机制进入老年代。

  1.  详见:Minor GC、Major GC和Full GC之间的区别及JVM内存分布,JVM垃圾回收初解
  2. 7.新生代和老生代的内存回收策略
  3.  解析:虚拟机既然采用了分代收集的思想来管理内存,那么内存回收时就必须能识别对象放在新生代,哪些对象放在老年代。为了做到这点,虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过第一次MinorGC然后仍然存活,并且能被Survivor容纳的话,将被移到Survivor中,并且对象年龄设为1。对象在Survivor中每过一次MinorGC,年龄就增加一岁。当年龄增加到一定程度,就会晋升到老年代。对象晋升老年代的年龄阈值,可以通过参数:-XX:MaxTenuringThreshold设置。
    1.  新生代中采用的收集算法:复制算法。算法的思想是将可用内存分为大小相等的两块,每次使用其中一块,当一块内存用完了,就将还存活的对象分到另一块。然后把使用过的内存空间一次进行清理。这种算法缺点会明显,就是会浪费一半的空间。

       老年代中采用的收集算法:标记-整理算法。算法思想:首先标记需要回收的对象,然后让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。

  4. 8.JVM内存分代策略
  5.  解析:Java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代和永久代(对HotSpot虚拟机而言),这就是JVM的内存分代策略。
  6.  为什么要分代?

     堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,会严重影响我们的GC效率,这简直太可怕了。

     有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。

  7. 9.抽象工厂模式和工厂方法模式的区别

  8.  解析:工厂方法模式: 一个抽象产品类,可以派生出多个具体产品类。 一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类只能创建一个具体产品类的实例。 抽象工厂模式: 多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。 一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品类的实例。

  9. 不同点总结:

    抽象工程关键在于产品之间的抽象关系,所以至少要两个产品;工厂方法在于生成产品,不关注产品间的关系,所以可以只生成一个产品。

    抽象工厂中客户端把产品的抽象关系理清楚,在最终使用的时候,一般使用客户端(和其接口),产品之间的关系是被封装固定的;而工厂方法是在最终使用的时候,使用产品本身(和其接口)。

    工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。 工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。

  10. 10.HashMap怎么扩容
  11.   解析:什么时候扩容:当向容器添加元素的时候,会判断当前容器的元素个数,如果大于等于阈值--- 即当前数组的长度乘以加载因子的值的时候,就要自动扩容。
  12.  扩容(resize)就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,扩容后的HashMap容量是之前容量的两倍,并且重新计算每个元素在新数组中的位置。默认的负载因子0.75是对空间和时间效率的一个平衡选择。JDK1.8对HashMap底层的实现进行了优化,例如引入红黑树的数据结构和扩容的优化。当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。


  13. 这里就是使用一个容量更大的数组来代替已有的容量小的数组,将原有数组的元素拷贝到新数组里。

  14. 11.HashMap在并发下死循环
  15.  解析: 多线程同时put时,如果同时触发了rehash操作,会导致HashMap中的链表中出现循环节点,进而使得后面get的时候,会死循环。
  16.  这段代码中又调用了transfer()方法,而这种方法实现的机制就是将每一个链表转化到新链表,而且链表中的位置发生反转,而这在多线程情况下是非常容易造成链表回路。从而发生get()死循环。

  17. 12.ThreadLocal的使用

  18.  解析:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

  19. 当我们调用get方法的时候,其实每个当前线程中都有一个ThreadLocal。每次获取或者设置都是对该ThreadLocal进行的操作,是与其他线程分开的。

  20. 应用场景:当很多线程需要多次使用同一个对象,并且需要该对象具有相同初始化值的时候最适合使用ThreadLocal

  21. 详见:ThreadLocal和synchronized的区别 | ThreadLocal详解

  22. 13.CMS垃圾回收机制

  23.  解析:CMS收集器是JAVA虚拟机中垃圾收集器的一种。它运行在JAVA虚拟机的老年代中。CMS是(Concurrent MarkSweep)的首字母缩写。CMS收集器是一种以获取最短回收停顿时间为目标的收集器。比较适用于互联网等场合,可能是互联网中最重要的收集器模式;,CMS是一款并发、使用标记-清除算法的gc。

    1.  总体来说CMS的执行过程可以分为以下几个阶段:

      初始标记(STW)、并发标记、并发预清理、重标记(STW)、并发清理、重置。

      优点:由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。因此CMS是一款优秀的收集器,具备了并发收集、低停顿的优点。

      缺点:(1)CMS收集器对CPU资源非常敏感。面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。(2)CMS收集器无法处理浮动垃圾。由于CMS并发清理阶段用户线程还在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在本次收集中处理掉它们,只好留待下一次GC时再将其清理掉。这一部分垃圾就称为“浮动垃圾(Floating Garbage)”。也是由于在垃圾收集阶段用户线程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。(3)收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。

    2.   详见:JAVA垃圾收集器之CMS收集器 | 4款Java垃圾回收器

猜你喜欢

转载自blog.csdn.net/xiaoxiangzi520/article/details/78977310