接下来,继续给大家分享一个《Java 面试指南-农行面经同学 3》的 面试原题,来看看农行面试官都喜欢问哪些问题,好做到知彼知己百战不殆。
内容较长,建议正在冲刺 24 届春招和 25 届暑期实习、秋招的同学先收藏起来,面试的时候大概率会碰到,我会尽量用通俗易懂+手绘图的方式,让天下所有的面渣都能逆袭 😁
在技术派实战项目中,很多地方都用到了 Redis,比如说用户活跃排行榜、作者白名单、常用热点数据(文章标签、文章分类)、计数统计(文章点赞收藏评论数粉丝数)等等。
技术派专栏
像用户活跃榜,主要是基于 Redis 的 Zset 实现的,可以根据 score(分值)进行排序,实时展示用户的活跃度。
技术派阅读活跃榜
当然了,这块也可以使用 Redis 的zrevrange,直接倒序展示前 8 名用户。
秒杀主要是指大量用户集中在短时间内对服务器进行访问,从而导致服务器负载剧增,可能出现系统响应缓慢甚至崩溃的情况。
针对秒杀的场景来说,最终抢到商品的用户是固定的,也就是说 100 个人和 10000 个人来抢一个商品,最终都只能有 100 个人抢到。
但是对于秒杀活动的初心来说,肯定是希望参与的用户越多越好,但真正开始下单时,最好能把请求控制在服务器能够承受的范围之内(😂)。
许令波-秒杀系统的设计
解决这一问题的关键就在于错峰削峰和限流。当然了,前端页面的静态化、按钮防抖也能够有效的减轻服务器的压力。
针对车流量的晚高峰和早高峰,最强有力的办法就是限行,但限行不是无损的,毕竟限行的牌号无法出行。
无损的方式就是有的车辆早出发,有的车辆晚出发,这样就能够实现错峰出行。
在秒杀场景下,可以通过以下几种方式实现错峰削峰:
①、预热缓存:提前将热点数据加载到 Redis 缓存中,减少对数据库的访问压力。
②、消息队列:引入消息队列,将请求异步处理,减少瞬时请求压力。消息队列就像一个水库,可以削减上游的洪峰流量。
许令波-排队
③、多阶段多时间窗口:将秒杀活动分为多个阶段,每个阶段设置不同的时间窗口,让用户在不同的时间段内参与秒杀活动。
④、插入答题系统:在秒杀活动中加入答题环节,只有答对题目的用户才能参与秒杀活动,这样可以减少无效请求。
许令波-答题
采用令牌桶算法,它就像在帝都买车,摇到号才有资格,没摇到就只能等下一次(😁)。
在实际开发中,我们需要维护一个容器,按照固定的速率往容器中放令牌(token),当请求到来时,从容器中取出一个令牌,如果容器中没有令牌,则拒绝请求。
李子捌:令牌桶
在 Redis 中,设置键值对的命令是 set。set 命令有几个常用的参数:
①、可以通过 EX 或 PX 为键设置过期时间(秒或毫秒)
redis-cli SET session_id "xyz" EX 3600 # 设置键 session_id,值为 "xyz",过期时间为 3600 秒
②、NX 选项表示只有键不存在时才设置
redis-cli SET lock_key "locked" NX
③、XX 选项表示只有键存在时才设置
redis-cli SET config "new_config" XX
Java 中创建线程主要有三种方式,分别为继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。
第一种,继承 Thread 类,重写 run()
方法,调用 start()
方法启动线程。
class ThreadTask extends Thread {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
task.start();
}
}
这种方法的缺点是,由于 Java 不支持多重继承,所以如果类已经继承了另一个类,就不能使用这种方法了。
第二种,实现 Runnable 接口,重写 run()
方法,然后创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 对象,调用 start()
方法启动线程。
class RunnableTask implements Runnable {
public void run() {
System.out.println("看完二哥的 Java 进阶之路,上岸了!");
}
public static void main(String[] args) {
RunnableTask task = new RunnableTask();
Thread thread = new Thread(task);
thread.start();
}
}
这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。
第三种,实现 Callable 接口,重写 call()
方法,然后创建 FutureTask 对象,参数为 Callable 对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,调用 start()
方法启动线程。
class CallableTask implements Callable<String> {
public String call() {
return "看完二哥的 Java 进阶之路,上岸了!";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableTask task = new CallableTask();
FutureTask<String> futureTask = new FutureTask<>(task);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
这种方法的优点是可以获取线程的执行结果。
线程池有 7 个参数,需要重点关注corePoolSize
、maximumPoolSize
、workQueue
、handler
这四个。
三分恶面渣逆袭:线程池参数
我一一说一下:
①、corePoolSize
定义了线程池中的核心线程数量。即使这些线程处于空闲状态,它们也不会被回收。这是线程池保持在等待状态下的线程数。
②、maximumPoolSize
线程池允许的最大线程数量。当工作队列满了之后,线程池会创建新线程来处理任务,直到线程数达到这个最大值。
③、keepAliveTime
非核心线程的空闲存活时间。如果线程池中的线程数量超过了 corePoolSize,那么这些多余的线程在空闲时间超过 keepAliveTime 时会被终止。
④、unit
keepAliveTime 参数的时间单位。
⑤、workQueue
用于存放待处理任务的阻塞队列。当所有核心线程都忙时,新任务会被放在这个队列里等待执行。
⑥、threadFactory
一个创建新线程的工厂。它用于创建线程池中的线程。可以通过自定义 ThreadFactory 来给线程池中的线程设置有意义的名字,或设置优先级等。
⑦、handler
拒绝策略 RejectedExecutionHandler,定义了当线程池和工作队列都满了之后对新提交的任务的处理策略。常见的拒绝策略包括抛出异常、直接丢弃、丢弃队列中最老的任务、由提交任务的线程来直接执行任务等。