上一次因为文章篇幅和个人精力有限的原因,只分享了淘天的前 6 道题及其答案(点击访问上一篇open in new window)。接下来,咱们把其他几道题面试题及答案也分享给大家。

1.公司简介

淘天集团就是“淘宝”+“天猫”的结合,其集团拥有淘宝、天猫、1688、闲鱼等商业品牌,并通过天猫国际、淘宝直播、天猫超市、淘宝买菜、阿里妈妈等业务,提供进口、直播、超市、买菜、数字营销等服务。

2.面试背景

面试岗位:Java 开发工程师

面试时间:2023.10.28

3.面试问题

  1. 为什么要用 Redis?有预估 QPS 的提升幅度吗?
  2. Redis 内存不够用怎么办?
  3. 是否定义、设计过业务模型?
  4. 百万级用户规模服务上线的话需要做什么?
  5. JVM 怎么创建一个对象?
  6. 有哪些场景会触发类的加载?
  7. 如果不使用双亲委派会有什么问题?
  8. 一个线程包含哪些线程状态?
  9. 线程池执行任务的过程?
  10. 线程同步有哪些策略和类?有没有实测过关键字的性能?
  11. SpringBoot 搭建的 Web 服务处理过程?
  12. 有没有看过开源框架的源码,举一个例子讲讲?

PS:前 6 道问题上篇已经解答过,这里就不再赘述了,大家可以自行查看:https://www.javacn.site/company/alibaba.htmlopen in new window

4.答案解析

如果不使用双亲委派会有什么问题?

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

自 JDK 1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构器,如下图所示:

其中:

  • 启动类加载器:加载 JDK 中 lib 目录中 Java 的核心类库,即$JAVA_HOME/lib目录。 扩展类加载器。加载 lib/ext 目录下的类;
  • 应用程序类加载器:加载我们写的应用程序;
  • 自定义类加载器:根据自己的需求定制类加载器。

如果不使用双亲委派模型,可能存在以下问题:

  1. 安全性问题:双亲委派模型可以通过从上层类加载器向下层加载器委派类加载请求来提高安全性。如果没有双亲委派机制,那些由上层类加载器加载的核心类可能会被替换成恶意代码,从而导致安全漏洞。
  2. 资源浪费问题:没有双亲委派机制,每个类加载器都有自己的类加载搜索路径和加载规则。这可能导致同一个类被不同的类加载器重复加载,造成资源的浪费。
  3. 类冲突问题:在没有双亲委派机制的情况下,不同的类加载器可以独立加载相同的类。这可能导致类的冲突和不一致性,因为同一个类在不同的类加载器中会有多个版本存在,最终导致类的不一致问题。

双亲委派模型是保证 Java 应用程序的稳定性和安全性的重要机制,使用双亲委派模型能够避免类的冲突、提高安全性、节省资源,并保证类的一致性。

线程中包含哪些状态?

答:在 Java 中,线程状态总共有以下 6 种:

  1. NEW(初始化状态):线程刚被创建时是初始状态,线程对象被创建,但还未调用 start() 方法启动线程。
  2. RUNNABLE(可运行状态):线程正在 Java 虚拟机中执行,调用 start() 方法后,线程开始执行,变为此状态。
  3. BLOCKED(阻塞状态):线程被阻塞,等待获取锁资源。当线程执行 synchronized 关键字标识的代码块时,如果无法获取到锁资源,则线程进入阻塞状态。当其他线程释放锁资源后,该阻塞线程进入就绪状态,等待竞争锁资源。
  4. WAITING(无时限等待状态):线程通过调用 Object.wait() 方法进入等待状态,直到被其他线程通过 Object.notify() 或 Object.notifyAll() 来唤醒。
  5. TIMED_WAITING(有时限等待状态):线程通过调用 Thread.sleep(long millis) 方法或 Object.wait(long timeout) 方法进入计时等待状态。在指定的时间段内,线程会一直保持计时等待状态,直到到达指定时间或被其他线程唤醒。
  6. TERMINATED(终止状态):线程执行完成或者异常终止,即线程生命周期结束,线程进入终止状态后不可再次转换为其他状态。

线程状态的转换流程如下图所示:

线程池执行任务的过程?

答:线程池的执行流程如下(当任务来了之后):

  1. 先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;
  2. 如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行;
  3. 如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务;
  4. 如果结果为 true,则将执行线程池的拒绝策略。

执行流程如下图所示:

线程同步有哪些策略和类?有没有实测过关键字的性能?

答:线程同步是为了保证多线程环境下数据的一致性和协调线程之间的执行顺序。 在 Java 中,有多种线程同步的策略和类有以下这些:

  1. synchronized 关键字:通过在代码块或方法上加上 synchronized 关键字,可以实现对代码块或方法的同步访问。当一个线程获取到了对象的锁资源,其他线程就无法进入该代码块或方法,只能等待锁资源的释放。
  2. ReentrantLock 类:它是显示锁的一种实现,提供了可重入的锁机制,与 synchronized 关键字相比,ReentrantLock 提供了更高的灵活性和额外的功能,例如设置等待时间、中断等待、公平性等。
  3. Condition 类:与 ReentrantLock 类一起使用,通过创建多个 Condition 对象,可以实现更加精细化的线程等待和唤醒机制。
  4. Semaphore 类:通过设置信号量的数量,可以控制同时访问某个资源的线程数量。
  5. CountDownLatch 类:通过设置计数器的值,可以控制某个任务等待其他一组任务完成后再执行。
  6. CyclicBarrier 类:通过设置参与线程数量,当所有线程都达到栅栏点后,所有线程会被释放,并继续执行。

然而,这些线程同步类的性能是和具体使用场景有关的,不同的业务场景其性能是不同的,synchronized 在早期的版本(JDK 1.6 之前)使用的是重量级锁,所以性能不是很好。但在 JDK 1.6 时经过了锁升级的优化(无锁、偏向锁、轻量级锁、重量级锁),因此绝大部分场景使用更易操作的 synchronized 就足够了,但如果需要创建公平锁或有多个任务需要协调一起执行时可以考虑其他的同步关键字。

Semaphore 类介绍和使用可以看我之前写的文章:https://juejin.cn/post/6953417207191699464open in new window

CountDownLatch 类介绍和使用可以看我之前写的这篇文章:https://juejin.cn/post/6945655452952854536open in new window

CyclicBarrier 类介绍和使用可以看我之前写的这篇文章:https://www.yuque.com/vipstone/tp0yhd/owd178open in new window

SpringBoot 搭建的 Web 服务处理过程?

答:Spring Boot 内部使用 Servlet 容器(如 Tomcat、Jetty 等)来处理 Web(HTTP)请求和响应。 它的执行流程可以分为以下几个关键步骤:

  1. 客户端发起请求:客户端通过 HTTP 协议向 Spring Boot 应用程序发送请求。请求可以包括 HTTP 方法(GET、POST等)、URL 路径、请求头、请求参数等信息。
  2. 路由匹配:Spring Boot 应用程序根据请求的 URL 路径,通过路由匹配将请求分发到对应的处理器。
  3. 处理器处理请求:匹配到的处理器(Controller)会执行相应的方法来处理请求。在 Spring Boot 中,Controller 会被注解标识,Spring Boot 会根据注解配置自动将请求分发给对应的 Controller。Controller 方法可以接收请求参数、处理业务逻辑,并返回响应结果。
  4. 调用服务层:Controller 可以调用业务逻辑处理层(Service)来进行具体的业务处理。Service 层通常负责处理复杂的业务逻辑,如数据库读写、事务管理等。
  5. 返回响应结果:处理器处理完请求后,将处理结果封装成 HTTP 响应返回给客户端。响应可以包括 HTTP 状态码、响应头、响应体等信息。
  6. 客户端接收响应:客户端收到响应后,根据响应的内容进行相应的处理,如解析 JSON 数据、渲染页面等。
  7. 结束请求生命周期:请求处理完成后,会结束请求的生命周期,释放相关资源。

有没有看过开源框架的源码,举一个例子讲讲?

答:这个问题没有固定的答案了,个人可根据自己的情况来说,这个给大家提供两个比较典型的案例。

Spring Boot 请求执行源码

你可以说你看过 Spring Boot 的源码,其中记忆比较深刻的就是请求进入 Spring Boot 中的执行流程,他的执行流程是这样的,所有请求先进入 DispatcherServlet(前端控制器),调用其父类 FrameworkServlet service 方法,核心源码如下:

/**
 * Override the parent class implementation in order to intercept PATCH requests.
 */
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
    if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
        processRequest(request, response);
    } else {
        super.service(request, response);
    }
}

继续往下看,processRequest 实现源码如下:

protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 // 省略一堆初始化配置
   
   try {
       // 真正执行逻辑的方法
       doService(request, response);
   }
   catch (ServletException | IOException ex) {
       ...
   }
}

doService 实现源码如下:

protected abstract void doService(HttpServletRequest request, HttpServletResponse response) throws Exception;

doService 是抽象方法,由 DispatcherServlet 重写实现了,源码如下:

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 省略初始化过程...
    try {
        doDispatch(request, response);
    }
    finally {
		// 省略其他...
    }
}

此时就进入到了 DispatcherServlet 中的 doDispatch 源码了:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // 获取原生请求
    HttpServletRequest processedRequest = request;
    // 获取Handler执行链
    HandlerExecutionChain mappedHandler = null;
    // 是否为文件上传请求, 默认为false
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            // 检查是否为文件上传请求
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            // Determine handler for the current request.
            // 获取能处理此请求的Handler
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }
            // Determine handler adapter for the current request.
            // 获取适配器
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
            // 执行拦截器(链)的前置处理
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }
            // 真正的执行对应方法
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }
            applyDefaultViewName(processedRequest, mv);
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        // 忽略其他...
}

通过上述的源码我们可以看到,请求的核心代码都在 doDispatch 中,他里面包含的主要执行流程有以下这些:

  1. 调用 HandlerExecutionChain 获取处理器:DispatcherServlet 首先调用 getHandler 方法,通过 HandlerMapping 获取请求对应的 HandlerExecutionChain 对象,包含了处理器方法和拦截器列表。
  2. 调用 HandlerAdapter 执行处理器方法:DispatcherServlet 使用 HandlerAdapter 来执行处理器方法。根据 HandlerExecutionChain 中的处理器方法类型不同,选择对应的 HandlerAdapter 进行处理。常用的适配器有 RequestMappingHandlerAdapter 和 HttpRequestHandlerAdapter。
  3. 解析请求参数:DispatcherServlet 调用 HandlerAdapter 的 handle 方法,解析请求参数,并将解析后的参数传递给处理器方法执行。
  4. 调用处理器方法:DispatcherServlet 通过反射机制调用处理器方法,执行业务逻辑。
  5. 处理拦截器:在调用处理器方法前后,DispatcherServlet 会调用拦截器的 preHandle 和 postHandle方法进行相应的处理。
  6. 渲染视图:处理器方法执行完成后,DispatcherServlet 会通过 ViewResolver 解析视图名称,找到对应的 View 对象,并将模型数据传递给 View 进行渲染。
  7. 生成响应:View 会将渲染后的视图内容生成响应数据。

Spring Cloud LoadBalancer 负载均衡源码

当然,除了 Spring Boot 外,你还可以讲一下 Spring cloud 微服务的源码,比如业务代码比较简单的 Spring Cloud LoadBalancer 的源码,这样既能展现自己会微服务,而且掌握的还不错。因为微服务在企业中应用广泛,所以熟练掌握微服务是一个很大的加分项。

Spring Cloud LoadBalancer 中内置了两种负载均衡策略:

  1. 轮询负载均衡策略
  2. 随机负载均衡策略

轮询负载均衡策略的核心实现源码如下:

// ++i 去负数,得到一个正数值
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
// 正数值和服务实例个数取余 -> 实现轮询
ServiceInstance instance = (ServiceInstance)instances.get(pos % instances.size());
// 将实例返回给调用者
return new DefaultResponse(instance);

随机负载均衡策略的核心实现源码如下:

// 通过 ThreadLocalRandom 获取一个随机数,最大值为服务实例的个数
int index = ThreadLocalRandom.current().nextInt(instances.size());
// 得到实例
ServiceInstance instance = (ServiceInstance)instances.get(index);
// 返回
return new DefaultResponse(instance);

小结

淘天集团一个标准的大厂,其薪资是比较高的,校招也能给到 30W 以上,社招薪资也不会太低,但其实看了他的面试题也可以发现,他的面试题其实不难,所以好好准备面试,也是有很大的几率进大厂的哦。


以上内容来自我的 《Java 面试突击训练营》,这门课程是 有着 14 年工作经验(前 360 开发工程师),9 年面试官经验的我,花费 4 年时间打磨完成的一门视频面试课

整个课程从 Java 基础到微服务 Spring Cloud、从实际开发问题到场景题应有尽有,如下图所示:

全程通过视频直播 + 录播的方式,把 Java 常见的面试题系统的过一遍,遇到一个问题,把这个问题相关的内容都给大家讲明白,并且视频支持永久更新和观看。

上完训练营的课程之后,基本可以应对目前市面上绝大部分公司的面试了,想要了解详情,加我微信:vipStone【备注:训练营】