美团后端日常实习面试,轻松拿捏了!

共 12134字,需浏览 25分钟

 ·

2024-07-24 15:48

这是一位读者分享的美团日常实习面经,我对其中提到的面试问题添加了参加答案供大家复习参考。总体难度不大,但对于实习面试来说已经算是高难度了!

合集地址:《Java 后端面经精选(附详细参考答案)》 。

面试情况

总时长 45 min 左右,日常实习,已经拿到 offer 了!

个人情况

  • 末位 211,26 届,学习 Java 一年多一点。
  • 简历上有一个魔改的培训班项目和一个参加学校比赛的项目,编码能力一般,技术八股能力中等。

面试题

1、自我介绍。

2、拷打项目 30min ,对着项目提问架构设计以及一些功能的开发思路。

星球里有一篇帖子分享面试官一般如何考察项目经历以及我们应该如何准备项目经历的回答:https://t.zsxq.com/136ObGpk4

3、看你项目用到了 CompletableFuture 进行任务编排,怎么做的?为什么要自己创建一个线程池?如果有一个任务失败了,你如何处理异常?

参考答案:从 5s 到 0.5s!看看人家的 CompletableFuture 异步任务优化技巧,确实优雅!

4、 Java 线程池参数有哪些?线程数设置多少合适?线程池参数固定配置有什么问题吗?动态线程池是什么?

参考答案:

    /**
     * 用给定的初始参数创建一个新的ThreadPoolExecutor。
     */

    public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
                              int maximumPoolSize,//线程池的最大线程数
                              long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
                              ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
                              RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
                               )
 
{
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

下面提到的公示也只是参考,实际项目不太可能直接按照公式来设置线程池参数,毕竟不同的业务场景对应的需求不同,具体还是要根据项目实际线上运行情况来动态调整。

线程数设置有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

线程数更严谨的计算的方法应该是:最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间)),其中 WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)

线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。

我们可以通过 JDK 自带的工具 VisualVM 来查看 WT/ST 比例。

  • CPU 密集型任务的 WT/ST 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。
  • IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。

传统线程池的参数(如核心线程数、最大线程数和任务队列大小)在创建时被固定。随着业务负载的变化,这种固定配置难以适应,需要调整时必须重启服务,过程繁琐且耗时。

动态线程池是一种能够在应用程序运行过程中,无需重启服务即可实时调整其核心配置参数(如核心线程数、最大线程数等)的线程池机制。通常情况下,动态线程池不仅支持参数的动态变更,还内置了监控和告警功能,例如在发生线程池任务堆积时通知相应的开发人员。

5、谈谈你知道的 Java 同步机制。

参考答案:

  • synchronized:Java 中的一个关键字,翻译成中文是同步的意思,主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
  • ReentrantLock: 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
  • ReentrantReadWriteLock :实现了 ReadWriteLock ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。
  • StampedLock:JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 Condition。不同于一般的 Lock 类,StampedLock 并不是直接实现 LockReadWriteLock接口,而是基于 CLH 锁 独立实现的(AQS 也是基于这玩意)。
  • SemaphoresynchronizedReentrantLock 都是一次只允许一个线程访问某个资源,而Semaphore(信号量)可以用来控制同时访问特定资源的线程数量。
  • CountDownLatch :允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
  • ......

6、你的项目这里为什么用分布式锁?怎么实现的?

参考答案:为什么需要分布式锁?如何基于 Redis 实现分布式锁?

7、项目用到了 Redis 哪些数据类型?

参考答案:Redis 八种常用数据类型常用命令和应用场景

8、MySQL 怎么解决慢查询问题?

# 开启慢查询日志功能
SET GLOBAL slow_query_log = 'ON';
# 慢查询日志存放位置
SET GLOBAL slow_query_log_file = '/var/lib/mysql/ranking-list-slow.log';
# 无论是否超时,未被索引的记录也会记录下来。
SET GLOBAL log_queries_not_using_indexes = 'ON';
# 慢查询阈值(秒),SQL 执行超过这个阈值将被记录在日志中。
SET SESSION long_query_time = 1;
# 慢查询仅记录扫描行数大于此参数的 SQL
SET SESSION min_examined_row_limit = 100;

设置成功之后,使用 show variables like 'slow%'; 命令进行查看。

| Variable_name       | Value                                |
+---------------------+--------------------------------------+
| slow_launch_time    | 2                                    |
| slow_query_log      | ON                                   |
| slow_query_log_file | /var/lib/mysql/ranking-list-slow.log |
+---------------------+--------------------------------------+
3 rows in set (0.01 sec)

我们故意在百万数据量的表(未使用索引)中执行一条排序的语句:

SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;

确保自己有对应目录的访问权限:

chmod 755 /var/lib/mysql/

查看对应的慢查询日志:

 cat /var/lib/mysql/ranking-list-slow.log

我们刚刚故意执行的 SQL 语句已经被慢查询日志记录了下来:

# Time: 2022-10-09T08:55:37.486797Z
# User@Host: root[root] @ [172.17.0.1] Id: 14
# Query_time: 0.978054 Lock_time: 0.000164 Rows_sent: 999999 Rows_examined: 1999998
SET timestamp=1665305736;
SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;

这里对日志中的一些信息进行说明:

  • Time :被日志记录的代码在服务器上的运行时间。
  • User@Host:谁执行的这段代码。
  • Query_time:这段代码运行时长。
  • Lock_time:执行这段代码时,锁定了多久。
  • Rows_sent:慢查询返回的记录。
  • Rows_examined:慢查询扫描过的行数。

实际项目中,慢查询日志通常会比较复杂,我们需要借助一些工具对其进行分析。像 MySQL 内置的 mysqldumpslow 工具就可以把相同的 SQL 归为一类,并统计出归类项的执行次数和每次执行的耗时等一系列对应的情况。

找到了慢 SQL 之后,我们可以通过 EXPLAIN 命令分析对应的 SELECT 语句:

mysql> EXPLAIN SELECT `score`,`name` FROM `cus_order` ORDER BY `score` DESC;
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
| id | select_type | table     | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra          |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
|  1 | SIMPLE      | cus_order | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 997572 |   100.00 | Using filesort |
+----+-------------+-----------+------------+------+---------------+------+---------+------+--------+----------+----------------+
1 row in set1 warning (0.00 sec)

比较重要的字段说明:

  • select_type :查询的类型,常用的取值有 SIMPLE(普通查询,即没有联合查询、子查询)、PRIMARY(主查询)、UNION(UNION 中后面的查询)、SUBQUERY(子查询)等。
  • table :表示查询涉及的表或衍生表。
  • type :执行方式,判断查询是否高效的重要参考指标,结果值从差到好依次是:ALL < index < range ~ index_merge < ref < eq_ref < const < system。
  • rows : SQL 要查找到结果集需要扫描读取的数据行数,原则上 rows 越少越好。
  • ......

关于 Explain 的详细介绍,请看这篇文章:MySQL 执行计划分析[1]。另外,再推荐一下阿里的这篇文章:慢 SQL 治理经验总结,总结的挺不错。

9、MySQL 的隔离级别是基于锁实现的吗?

MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。

SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。

10、MySQL 的默认隔离级别是什么?可以解决幻读问题问题吗?

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过SELECT @@tx_isolation;命令来查看,MySQL 8.0 该命令改为SELECT @@transaction_isolation;

mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+

标准的 SQL 隔离级别定义里,REPEATABLE-READ(可重复读)是不可以防止幻读的。

但是!InnoDB 实现的 REPEATABLE-READ 隔离级别其实是可以解决幻读问题发生的,主要有下面两种情况:

  • 快照读:由 MVCC 机制来保证不出现幻读。
  • 当前读:使用 Next-Key Lock 进行加锁来保证不出现幻读,Next-Key Lock 是行锁(Record Lock)和间隙锁(Gap Lock)的结合,行锁只能锁住已经存在的行,为了避免插入新行,需要依赖间隙锁。

因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED ,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ 并不会有任何性能损失。

11、算法:买卖股票的最佳时机 II

Leetcode 原题:122.买卖股票的最佳时机 II[2]

参考资料
[1]

MySQL 执行计划分析: https://javaguide.cn/database/mysql/mysql-query-execution-plan.html

[2]

122.买卖股票的最佳时机 II: https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

欢迎准备面试的同学加入我的知识星球 。

星球部分面试资料介绍

2024 最新版原创 PDF 面试资料来啦!涵盖 Java 核心、数据库、缓存、分布式、设计模式、智力题等内容,非常全面!

浏览 800
3点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
3点赞
评论
收藏
分享

手机扫一扫分享

分享
举报