牛客上刷到一条帮选 offer 的帖子,上来就是“阿里 80 万年包,边缘业务”,于是很有诚意的点进去,想帮牛友分担一点选择上的负担,结果看得汗流浃背。
截图来自牛客
直到“洒水车司机,月薪 3000”的出现,又突然释怀的笑了(😂)~
你别说,我小学有个同学真在郑州开洒水车(真事),薪资比 3000 高一些,上次我表弟结婚的时候他也回村了,我们聊的还挺开心。我俩同届,从育红班开始一起上学到初中一年级他辍学,7 年时间。
话说回来,阿里 80 万年包,和洒水车司机月薪 3000元,我估计 99.9% 的同学都会毫不犹豫的选择阿里。
互联网高速发展到现在,很卷,阿里也是各种大厂病,最近几年一直在治。从下面这幅图大家应该能感受出来阿里的变化,从巅峰时期的 25 万员工减少到现在的 20 多万员工。
截图来自市界
但即便如此,如果有阿里的 offer 摆在同学们的面前,我相信大家依然会毫不犹豫地冲,毕竟下山的神仍然是神。我也希望国内的所有互联网公司都能走出低谷,重现辉煌,也只有这样,大家的就业选择才会多起来。
这次我们就以《Java 面试指南-阿里面经》同学 1 的后端面试为例,来看看阿里面试官都喜欢问哪些八股,好背的滚瓜烂熟,了然于胸,25 届的同学加把劲。
让天下所有的面渣都能逆袭 😁
题目不多,主要还是围绕着二哥强调的 Java 后端四大件为主,所以大家在准备的时候一定要有的放矢,知道哪些是重点。
Spring Bean 的默认作用域是单例(Singleton),这意味着 Spring 容器中只会存在一个 Bean 实例,并且该实例会被多个线程共享。
如果单例 Bean 是无状态的,也就是没有成员变量,那么这个单例 Bean 是线程安全的。比如 Spring MVC 中的 Controller、Service、Dao 等,基本上都是无状态的。
但如果 Bean 的内部状态是可变的,且没有进行适当的同步处理,就可能出现线程安全问题。
三分恶面渣逆袭:Spring单例Bean线程安全问题
第一,使用局部变量。局部变量是线程安全的,因为每个线程都有自己的局部变量副本。尽量使用局部变量而不是共享的成员变量。
public class MyService {
public void process() {
int localVar = 0;
// 使用局部变量进行操作
}
}
第二,尽量使用无状态的 Bean,即不在 Bean 中保存任何可变的状态信息。
public class MyStatelessService {
public void process() {
// 无状态处理
}
}
第三,同步访问。如果 Bean 中确实需要保存可变状态,可以通过 synchronized 关键字或者 Lock 接口来保证线程安全。
public class MyService {
private int sharedVar;
public synchronized void increment() {
sharedVar++;
}
}
或者将 Bean 中的成员变量保存到 ThreadLocal 中,ThreadLocal 可以保证多线程环境下变量的隔离。
public class MyService {
private ThreadLocal<Integer> localVar = ThreadLocal.withInitial(() -> 0);
public void process() {
localVar.set(localVar.get() + 1);
}
}
再或者使用线程安全的工具类,比如说 AtomicInteger、ConcurrentHashMap、CopyOnWriteArrayList 等。
public class MyService {
private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
public void putValue(String key, String value) {
map.put(key, value);
}
}
第四,将 Bean 定义为原型作用域(Prototype)。原型作用域的 Bean 每次请求都会创建一个新的实例,因此不存在线程安全问题。
@Component
@Scope("prototype")
public class MyService {
// 实例变量
}
B+ 树相比较 B 树,有这些优势:
①、更高的查询效率
B+树的所有值(数据记录或指向数据记录的指针)都存在于叶子节点,并且叶子节点之间通过指针连接,形成一个有序链表。
极客时间:B+树
这种结构使得 B+树非常适合进行范围查询——一旦到达了范围的开始位置,接下来的元素可以通过遍历叶子节点的链表顺序访问,而不需要回到树的上层。如 SQL 中的 ORDER BY 和 BETWEEN 查询。
极客时间:B 树
而 B 树的数据分布在整个树中,进行范围查询时可能需要遍历树的多个层级。
②、更高的空间利用率
在 B+树中,非叶子节点不存储数据,只存储键值,这意味着非叶子节点可以拥有更多的键,从而有更多的分叉。
这导致树的高度更低,进一步降低了查询时磁盘 I/O 的次数,因为每一次从一个节点到另一个节点的跳转都可能涉及到磁盘 I/O 操作。
③、查询效率更稳定
B+树中所有叶子节点深度相同,所有数据查询路径长度相等,保证了每次搜索的性能稳定性。而在 B 树中,数据可以存储在内部节点,不同的查询可能需要不同深度的搜索。
三分恶面渣逆袭:CPU飙高
首先,使用 top 命令查看 CPU 占用情况,找到占用 CPU 较高的进程 ID。
top
haikuotiankongdong:top 命令结果
接着,使用 jstack 命令查看对应进程的线程堆栈信息。
jstack -l <pid> > thread-dump.txt
上面👆🏻这个命令会将所有线程的堆栈信息输出到 thread-dump.txt 文件中。
然后再使用 top 命令查看进程中线程的占用情况,找到占用 CPU 较高的线程 ID。
top -H -p <pid>
haikuotiankongdong:Java 进程中的线程情况
注意,top 命令显示的线程 ID 是十进制的,而 jstack 输出的是十六进制的,所以需要将线程 ID 转换为十六进制。
printf "%x\n" PID
在 jstack 的输出中搜索这个十六进制的线程 ID,找到对应的堆栈信息。
"Thread-5" #21 prio=5 os_prio=0 tid=0x00007f812c018800 nid=0x1a85 runnable [0x00007f811c000000]
java.lang.Thread.State: RUNNABLE
at com.example.MyClass.myMethod(MyClass.java:123)
at ...
最后,根据堆栈信息定位到具体的业务方法,查看是否有死循环、频繁的垃圾回收(GC)、资源竞争(如锁竞争)导致的上下文频繁切换等问题。
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
悲观锁的代表有 synchronized 关键字和 Lock 接口。
乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。
由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。
CAS 存在三个经典问题,ABA 是其中一个。
三分恶面渣逆袭:CAS三大问题
如果一个位置的值原来是 A,后来被改为 B,再后来又被改回 A,那么进行 CAS 操作的线程将无法知晓该位置的值在此期间已经被修改过。
可以使用版本号/时间戳的方式来解决 ABA 问题。
比如说,每次变量更新时,不仅更新变量的值,还更新一个版本号。CAS 操作时不仅要求值匹配,还要求版本号匹配。
Java 的 AtomicStampedReference 类就实现了这种机制,它会同时检查引用值和 stamp 是否都相等。
二哥的 Java 进阶之路:AtomicStampedReference
顾名思义,慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。
定位慢 SQL 主要通过两种手段:
也可以使用 show processlist;
查看当前正在执行的 SQL 语句,找出执行时间较长的 SQL。
找到对应的慢 SQL 后,使用 EXPLAIN 命令查看 MySQL 是如何执行 SQL 语句的,这会帮助我们找到问题的根源。
EXPLAIN SELECT * FROM your_table WHERE conditions;
第一,使用消息队列,如 RabbitMQ、Kafka、RocketMQ 等,将任务放到消息队列中,然后由消费者异步处理这些任务。
①、在订单创建时,将订单超时检查任务放入消息队列,并设置延迟时间(即订单超时时间)。
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void createOrder(Order order) {
// 创建订单逻辑
// ...
// 发送延迟消息
rabbitTemplate.convertAndSend("orderExchange", "orderTimeoutQueue", order, message -> {
message.getMessageProperties().setExpiration("600000"); // 设置延迟时间(10分钟)
return message;
});
}
}
②、使用消费者从队列中消费消息,当消费到超时任务时,执行订单超时处理逻辑。
@Service
public class OrderTimeoutConsumer {
@RabbitListener(queues = "orderTimeoutQueue")
public void handleOrderTimeout(Order order) {
// 处理订单超时逻辑
// ...
}
}
第二,使用数据库调度器(如 Quartz)。
①、创建一个 Quartz 任务类,处理订单超时逻辑。
public class OrderTimeoutJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
// 获取订单信息
Order order = (Order) context.getJobDetail().getJobDataMap().get("order");
// 处理订单超时逻辑
// ...
}
}
②、在订单创建时,调度一个 Quartz 任务,设置任务的触发时间为订单超时时间。
@Service
public class OrderService {
@Autowired
private Scheduler scheduler;
public void createOrder(Order order) {
// 创建订单逻辑
// ...
// 调度 Quartz 任务
JobDetail jobDetail = JobBuilder.newJob(OrderTimeoutJob.class)
.usingJobData("order", order)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.startAt(new Date(System.currentTimeMillis() + 600000)) // 设置触发时间(10分钟后)
.build();
try {
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
ThreadLocal 是 Java 中提供的一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己的独立副本,从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。
三分恶面渣逆袭:ThreadLocal线程副本
在 Web 应用中,可以使用 ThreadLocal 存储用户会话信息,这样每个线程在处理用户请求时都能方便地访问当前用户的会话信息。
在数据库操作中,可以使用 ThreadLocal 存储数据库连接对象,每个线程有自己独立的数据库连接,从而避免了多线程竞争同一数据库连接的问题。
在格式化操作中,例如日期格式化,可以使用 ThreadLocal 存储 SimpleDateFormat 实例,避免多线程共享同一实例导致的线程安全问题。
线程之间传递信息有多种方式,每种方式适用于不同的场景。比如说使用共享对象、wait()
和 notify()
、Exchanger 和 CompletableFuture。
我简单说一下 CompletableFuture 吧,它是 Java 8 引入的一个类,支持异步编程,允许线程在完成计算后将结果传递给其他线程。
public class Main {
public static void main(String[] args) {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 模拟长时间计算
return "Message from CompletableFuture";
});
future.thenAccept(message -> {
System.out.println("Received: " + message);
});
}
}