# 阿里面试题

# 如何理解线程的五态以及转化过程一面

参考答案

线程的五态

  • 新建态:进程在创建时需要申请一个空白进程管理块,向其中填写控制和管理进程的信息,完成资源分配;
  • 就绪态:进程已经准备好,已经分配到所需资源,只要分配到CPU就能够立刻运行;
  • 运行态:进程处于就绪状态被调度后,进入运行态;
  • 阻塞态:正在执行的进程由于某些某些事件(如I/O调用)而无法运行,进程受到阻塞;
  • 终止态:进程结束,或出现错误,或被系统终止,进入终止状态。

转化过程

# hashmap和concurrenthashmap的size方法怎么实现的

参考答案


//最大值是 Integer 类型的最大值,但是 Map 的 size 可能超过 MAX_VALUE
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
           (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n);
}
//因此JDK 的建议使用 mappingCount() 而不是size()
public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; 
    }

//无论是 size() 还是 mappingCount(), 计算大小的核心方法都是 sumCount()
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
       for (int i = 0; i < as.length; ++i) {
           if ((a = as[i]) != null)
               sum += a.value;
           }
       }
    return sum;
}


总结

  • JDK1.7 和 JDK1.8 对 size 的计算是不一样的。 1.7 中是先不加锁计算三次,如果三次结果不一样在加锁。
  • JDK1.8 size 是通过对 baseCount 和 counterCell 进行 CAS 计算,最终通过 baseCount 和 遍历 CounterCell 数组得出 size。
  • JDK 8 推荐使用mappingCount 方法,因为这个方法的返回值是 long 类型,不会因为 size 方法是 int 类型限制最大值。

# JDK中的sort排序方法

参考答案

JDK中主要针对Collections.sort()和Arrays.sort()这两个排序算法的实现。

  • Collections.sort()
     public static <T> void sort(T[] a, Comparator<? super T> c) {
          if (c == null) {
              sort(a);
          } else {
              //如果LegacyMergeSort.userRequested为true的话就会使用归并排序
              //boolean可以通过如下方式来设置:
              //Properties properties = new Properties();
              //properties.setProperty("java.util.Arrays.useLegacyMergeSort","true");
              //System.setProperties(properties);
              if (LegacyMergeSort.userRequested)
                  legacyMergeSort(a, c);
              else
              //如果不为true的话就会用一个叫TimSort的排序算法
                  TimSort.sort(a, 0, a.length, c, null, 0, 0);
          }
      }
    
  • Arrays.sort()
//sort方法拥有很多的重载,有十几种,以int参数的排序方法源码如下
public static void sort(int[] a) {
   //双轴快速排序
   DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}

//如果数组的长度小于QUICKSORT_THRESHOLD的话就会使用这个双轴快速排序,而这个值是286
//如果大于286,会判断数组的连续升序和连续降序性好不好,好就用归并排序,否则用快速排序
if (right - left < QUICKSORT_THRESHOLD) {
   sort(a, left, right, true);
   return;
}

//如果数组长度小于INSERTION_SORT_THRESHOLD(值为47)的话,那么就会用插入排序了,不然再用双轴快速排序!
if (length < INSERTION_SORT_THRESHOLD) {
  
}

总结:JDK1.8中的Arrays.sort()的排序方法就是使用的快排,当待排序数量大于286的时候,看连续升序和降序性好不好,好则归并,否则快速;当待排序数量小于286的时候,如果待排序数据量大于47的时候采用快速排序,小于47的时候使用插入排序。

扩展

Arrays.sort对升序数组、降序数组和重复数组的排序效率有了很大的提升,这里面有几个重大的优化。

  • 1、对于小数组来说,插入排序效率更高,每次递归到小于47的大小时,用插入排序代替快排,明显提升了性能。
  • 2、双轴快排使用两个pivot,每轮把数组分成3段,在没有明显增加比较次数的情况下巧妙地减少了递归次数。
  • 3、pivot的选择上增加了随机性,却没有带来随机数的开销。
  • 4、对重复数据进行了优化处理,避免了不必要交换和递归。

双轴快排(DualPivotQuicksort),顾名思义有两个轴元素pivot1,pivot2,且pivot ≤ pivot2,将序列分成三段:x < pivot1、pivot1 ≤ x ≤ pivot2、x >pivot2,然后分别对三段进行递归。这个算法通常会比传统的快排效率更高,也因此被作为Arrays.java中给基本类型的数据排序的具体实现。

# 为何JDK选择快速排序

快排、堆排和归并都是O(nlog n)的算法,为何JDK选择快速排序?

参考答案

  • 1、快排的时间复杂度确实不稳定,极端情况是O(n^2),但是平摊下来是T(nlg(n)),而归并是严格的O(nlog(n))。
  • 2、快速排序比归并排序快。其原因有
    • 1)快排对内存的访问是顺序方式(包括逆序),只有两个目标而且是同一个数组,故cache的命中率不会比归并低。特别是数组空间接近于cache大小时,这一优势将更加明显。
    • 2)快排的内存写操作次数平摊下来是T(nlg(n)/2),而归并的内存写操作次数是严格的O(nlog(n)),由于内存写操作开销比较大,所以对于随机数据快排优于归并。

扩展

快速排序使用的是分治思想,将原问题分成若干个子问题进行递归解决。选择一个元素作为轴(pivot),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比轴元素小,另外一部分的所有数据都比轴元素大,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

# 为何JDK中的快速排序在元素个数小于47时要采用插入排序

参考答案

对于小数组来说,插入排序效率更高,每次递归到小于47的大小时,用插入排序代替快排,明显提升了性能。

# HashMap可能造成什么问题

参考答案

HashMap不是线程安全,并没有加锁控制,造成的问题就是在HashMap扩容的时候会有可能导致循环链表。

# HashTable是如何实现的

参考答案

HashTable是线程安全的,主要是基于HashMap的基础上加上了Synchronized关键字来进行同步控制。

# 常用并发包下的类有哪些

参考答案

  • ConcurrentHashMap:其实就是线程安全版本的hashMap,通过Synchronized来保证线程安全。
  • CopyOnWriteArrayList:其实就是线程安全版本的ArrayList,通过ReentrantLock来保证线程安全。
  • CountDownLatch:CountDownLatch的计数器只能使用一次。
  • CyclicBarrier: CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
  • Semaphore:信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
  • Exchanger:(交换者)是一个用于线程间协作的工具类。
  • 等等

# redis持久化方式为什么这么快

参考答案

  • RDB:在指定的时间间隔能对你的数据进行快照存储。
  • AOF:记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据。

redis之所以快,在于:

  • 1、基于内存储存的数据库。
  • 2、处理网络请求使用的是单线程,避免了不必要的上下文切换和锁的竞争维护。
  • 3、使用了I/O多路复用模型。

# 介绍自己的项目和项目中遇到的难点

参考答案

略。。。

# jvm类加载机制每一步做了什么工作 二面

参考答案

  • 加载:由类加载器将类的class文件读入到内存,并为之创建一个java.lang.Class对象
  • 连接:,连接阶段负责把类的二进制数据合并到JRE中,连接过程包含如下3个步骤。
    • 验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
    • 准备:类准备阶段负责为类的静态变量分配内存,并设置默认初始值。
    • 解析:将类的二进制数据中的符号引用替换成直接引用。
  • 初始化:初始化是为类的静态变量赋予正确的初始值。

扩展阅读

JVM的类加载机制主要有如下3种:

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制: 缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

# JVM运行时数据区包括哪些部分

JVM运行时数据区包括哪些部分,垃圾收集有哪些算法,各自的特点?如何确定被清除的对象?

参考答案

JVM运行时数据区

  • 程序计数器
  • Java虚拟机栈
  • 本地方法栈
  • 方法区
  • Java堆

垃圾收集的算法概述

  • 标记-清除算法(Mark-Sweep),算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
  • 复制算法(Copying),为了解决效率问题,有了“复制”的算法,他将可用内存分为大小相同两块。每次只用一块,当一块空间用完了,就将还存活的对象复制到另一块上。
  • 标记-整理(Mark-Compact),标记过程和“标记-清除”一样,但在清除已死对象的同时会对存活对象进行整理,这样可以减少碎片空间。

如何确定被清除的对象 通过判断GC roots是否可达来确定回收的对象,可以做为GC roots的对象有:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属于引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。
  • System Class(被boostrap 类加载器加载的系统类。如rt.jar)。

四种引用类型的回收机制

  • 强引用(Strong) ,就是我们平时使用的方式 A a = new A();强引用的对象是不会被回收的。
  • 软引用(Soft) ,在jvm要内存溢出(OOM)时,会回收软引用的对象,释放更多内存。
  • 弱引用(Weak) ,在下次GC时,弱引用的对象是一定会被回收的。
  • 虚引用(Phantom) ,对对象的存在时间没有任何影响,也无法引用对象实力,唯一的作用就是在该对象被回收时收到一个系统通知。

# JVM中的垃圾回收器有哪些,如何区别

参考答案

# Mysql索引类型和区别

参考答案

# 聚簇索引和非聚簇索引的区别

参考答案

# 如何理解事务的隔离级别

参考答案

# Bean创建过程中的用到了哪些设计模式

参考答案

  • 1.工厂模式,这个很明显,在各种BeanFactory以及ApplicationContext创建中都用到了;
  • 2.模版模式,这个也很明显,在各种BeanFactory以及ApplicationContext实现中也都用到了;
  • 3.代理模式,在Aop实现中用到了JDK的动态代理;
  • 4.单例模式,这个比如在创建bean的时候。
  • 5.装饰器模式,有些创建bean扩展方法由装饰器来实现。
  • 6.迭代器模式,Iterable接口和Iterator接口 这两个都是迭代相关的接口。

# 谈谈你对IOC和AOP是如何理解的

参考答案

  • IOC:将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。
  • AOP:能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

扩展阅读

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用Cglib ,这时候Spring AOP会使用 Cglib 生成一个被代理对象的子类来作为代理。

# spring中bean的生命周期

参考答案

简单来说,根据spring核心方法 refresh()中的调用顺序,可以将bean的声明周期概括如下4个环节:

  • 实例化(Instantiation): 实际就是在JVM的堆内存中分配好对象的存储空间,但是对象没有DI,执行实例化前后分别会用postProcessBeforeInstantiation、postProcessAfterInstantiation进行AOP拦截。
  • 属性赋值(Populate): DI过程,也就是依赖注入。
  • 初始化(Initialization): 对应源码的invokeInitMethods方法,进行initMethod方法调用,也会在执行实例化前后分别用postProcessBeforeInstantiation、postProcessAfterInstantiation进行AOP拦截,
  • 销毁(Destruction): 销毁操作主要是JVM来处理的,实际上就是GC来操控的。

# 描述一下SpringMvc的调用流程

参考答案

  • 用户发起请求到前端控制器(Controller),然后由DipatchServlet统一进行分发。
  • 前端控制器没有处理业务逻辑的能力,需要找到具体的模型对象处理(Handler),到处理器映射器(HandlerMapping)中查找Handler对象(Model)。
  • HandlerMapping返回执行链,包含了2部分内容: ① Handler对象、② 拦截器数组
  • 前端处理器通过处理器适配器包装后执行Handler对象。 处理业务逻辑。
  • Handler处理完业务逻辑,返回ModelAndView对象,其中view是视图名称,不是真正的视图对象。 将ModelAndView返回给前端控制器。 -视图解析器(ViewResolver)返回真正的视图对象(View)。
  • 返回渲染后的视图(html/json/xml)返回给用户响应。

# 谈谈线程池的参数列表和拒绝策略

参考答案

// 线程池的创建
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory, RejectedExecutionHandler handler)

  • corePoolSize: 线程池维护线程的最少数量(也叫核心线程池数量)
  • maximumPoolSize:线程池维护线程的最大数量
  • keepAliveTime: 线程池维护线程所允许的空闲时间
  • unit: 线程池维护线程所允许的空闲时间的单位
  • workQueue: 线程池所使用的缓冲队列
  • threadFactory:线程创建的工厂
  • handler: 线程池对拒绝任务的处理策略

ThreadPoolExecutor中已经包含四种处理策略

  • AbortPolicy: 策略是线程池默认的策略,使用策略时,如果线程池的线程数满了,如果再有一个的任务进来,那么线程池会丢掉这个任务并且抛出RejectedExecutionException异常。
  • DiscardPolicy: 这个策略和AbortPolicy的slient版本,如果线程池队列满了,再有新的任务进来,线程池则会直接丢掉这个任务并且不会有任何异常。
  • DiscardOldestPolicy: 这个策略从字面上也很好理解,丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。因为队列是队尾进,队头出,所以队头元素是最老的,因此每次都是移除对头元素后再尝试入队。
  • CallerRunsPolicy: 使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。

扩展阅读

# 线程池的工作流程

当一个任务通过execute(Runnable)方法欲添加到线程池时:

  • 如果此时线程池中的数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量等于 corePoolSize,但是缓冲队列 workQueue未满,那么任务被放入缓冲队列。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。也就是:处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,如果三者都满了,使用handler处理被拒绝的任务。
  • 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数。

# 请概述什么是AQS

参考答案

AbstractQuenedSynchronizer抽象的队列式同步器,AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS底层使用了模板方法模式

# 请概述下JDK1.8中的ConcurrentHashMap 三面

参考答案

HashMap线程不安全的问题完全都是出在对链表/红黑树的操作上,因此Java8的ConcurrentHashMap去掉了segment,完全就是HashMap进行加锁,实现线程安全。

# 为什么kafka这么快,如何理解零拷贝

参考答案

  • kafka基于磁盘的顺序读写
  • kafka把所有的消息都变成一个批量的文件,并且进行合理的批量压缩,减少网络IO损耗,通过mmap提高I/O速度,写入数据的时候由于单个Partion是末尾添加所以速度最优;
  • 读取数据的时候配合sendfile直接暴力输出。

零拷贝

传统模式下,当需要对一个文件进行传输的时候,其具体流程细节如下:

  • 调用read函数,文件数据被copy到内核缓冲区
  • read函数返回,文件数据从内核缓冲区copy到用户缓冲区
  • write函数调用,将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。
  • 数据从socket缓冲区copy到相关协议引擎。

因此,零拷贝的出现就是通过减少用户态和内核态的交互次数,减少了上下文切换的开销,达到了发送文件数据的高效方式。

# 算法问题

假设有16G 的可用内存,有个 72G 的文件(文本文件,每一行都是一个独立的字符串),请读取文件里的内容,并将字符串按行排序,然后将排序后的结果保存到另一个文件里?

参考答案

分而治之

# http和https协议区别,Https具体原理

参考答案

# 项目内存或者cpu占用率过高如何排查

参考答案

  • 使用top命令查看占用高资源的java项目的进程ID(pid): top
  • 查看该进程中的线程所占用资源的情况:top -Hp pid
  • 接下来通过jstack PID > xxx.log输出java应用当前堆栈信息到文件。
  • 打印并保存该进程中堆栈的使用信息日志:jstack -l 11095 >> jstack.log
  • 查看该线程对应的16进制:printf %x PID
  • 查看该进程中使用高资源的线程的具体信息日志:vim jstack.log

# mysql中死锁,怎么解决或者避免

参考答案

当多个事务同时持有和请求同一资源上的锁而产生循环依赖的时候就产生了死锁。死锁发生在事务试图以不同的顺序锁定资源。

解决死锁

  • 等待,直到超时(innodb_lock_wait_timeout=50s),自动回滚事务。
  • 发起死锁检测,主动回滚一条事务,让其他事务继续执行(innodb_deadlock_detect=on)。

避免死锁

  • 以固定的顺序访问表和行。比如两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;又比如,将两个事务的sql顺序调整为一致,也能避免死锁。
  • 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
  • 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
  • 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。

# 文档中的单词查找功能你会如何实现

参考答案

# 请设计一个高可用,高伸缩的缓存系统

参考答案

  • 高可用,从狭义来讲,单台机器宕机后用户完全无感知,容错性强。

  • 可伸缩,业务量增长的时候,通过横向增加物理机,即可完成系统容量增长。

  • 通过以上两个维度结合redis特性来分析即可。

# 谈谈zookeeper里的投票机制

参考答案

# dubbo协议为什么采用异步单一长连接

参考答案

# dubbo和springcloud的优缺点

对于dubbo和springcloud,给出二者的优缺点,描述服务降级与服务熔断,二者之间的区别以及使用的场合。

参考答案

# 手画自己项目的架构图,并且针对架构和中间件提问 四面

参考答案

略。。。