大家好,我是二哥呀。
京东这几天的热度真的非常高,据说零售部门开始严查考勤,并且调整了午休时间,整整缩短了一个小时,从原来的 11:30-13:30 调整为 12:00-13:00。
更关键的是,晚上 6 点下班的员工要掂量掂量自己的工作饱和度。
我只能说,东哥这是不打算和兄弟们做兄弟了吗?(😂)
山雨欲来风满楼,这种举措也就意味着新的风暴即将来袭,这段时间京东的小伙伴们就要注意了,简历该更新要记得更新,八股、算法、项目该复盘的注意复盘,为下一步做好充足的准备。
另外,我也统计了一波京东 24 届的校招薪资,主要是后端开发、前端、测开和产品经理,25 届打算冲京东的小伙伴可以拿来作为一个参考。
数据来源于牛客和 offershow
能看得出,Java 后端开发主要集中在 23k*16 这个段位上,包括硕士 211、硕士 985、211 本。只能说,大厂香是香,卷也是真的卷~
这次我们就以《Java 面试指南-京东面经》同学 2 的后端面试为例, 来看看京东的面试官都喜欢问哪些八股,好背的滚瓜烂熟,了然于胸~
让天下所有的面渣都能逆袭 😁
题目不少,火箭造的飞起。主要还是围绕着二哥强调的 Java 后端四大件为主,所以大家在准备的时候一定要有的放矢,知道哪些是重点。
在技术派实战项目中,我采用的是先写 MySQL,再删除 Redis 的方式来保证缓存和数据库的数据一致性。
技术派教程
对于第一次查询,请求 B 查询到的缓存数据是 10,但 MySQL 被请求 A 更新为了 11,此时数据库和缓存不一致。
但也只存在这一次不一致的情况,对于不是强一致性的业务,可以容忍。
当请求 B 第二次查询时,因为请求 A 更新完数据库把缓存删除了,所以请求 B 这次不会命中缓存,会重新查一次 MySQL,然后回写到 Redis。
缓存和数据库又一致了。
延时双删防止脏数据
简单说,就是在第一次删除缓存之后,过一段时间之后,再次删除缓存。
主要针对缓存不存在,但写入了脏数据的情况。在先删缓存,再写数据库的更新策略下发生的比较多。
三分恶面渣逆袭:延时双删
这种方式的延时时间需要仔细考量和测试。
消息队列(Message Queue, MQ)是一种非常重要的中间件技术,广泛应用于分布式系统中,以提高系统的可用性、解耦能力和异步通信效率。
生产者将消息放入队列,消费者从队列中取出消息,这样一来,生产者和消费者之间就不需要直接通信,生产者只管生产消息,消费者只管消费消息,这样就实现了解耦。
三分恶面渣逆袭:消息队列解耦
与此同时,系统可以将那些耗时的任务放在消息队列中异步处理,从而快速响应用户的请求。
三分恶面渣逆袭:消息队列异步
RocketMQ 实现顺序消息的关键在于保证消息生产和消费过程中严格的顺序控制,即确保同一业务的消息按顺序发送到同一个队列中,并由同一个消费者线程按顺序消费。
三分恶面渣逆袭:顺序消息
局部顺序消息保证在某个逻辑分区或业务逻辑下的消息顺序,例如同一个订单或用户的消息按顺序消费,而不同订单或用户之间的顺序不做保证。
三分恶面渣逆袭:部分顺序消息
全局顺序消息保证消息在整个系统范围内的严格顺序,即消息按照生产的顺序被消费。
可以将所有消息发送到一个单独的队列中,确保所有消息按生产顺序发送和消费。
三分恶面渣逆袭:全局顺序消息
我会使用 Jmeter 对项目进行压测,通过合理配置线程组、HTTP 请求和监听器,可以模拟真实的用户负载,并分析项目在高负载下的表现。
多数情况下,ArrayList 更利于查找,LinkedList 更利于增删
①、由于 ArrayList 是基于数组实现的,所以 get(int index)
可以直接通过数组下标获取,时间复杂度是 O(1);LinkedList 是基于链表实现的,get(int index)
需要遍历链表,时间复杂度是 O(n)。
当然,get(E element)
这种查找,两种集合都需要遍历通过 equals 比较获取元素,所以时间复杂度都是 O(n)。
②、ArrayList 如果增删的是数组的尾部,直接插入或者删除就可以了,时间复杂度是 O(1);如果 add 的时候涉及到扩容,时间复杂度会提升到 O(n)。
但如果插入的是中间的位置,就需要把插入位置后的元素向前或者向后移动,甚至还有可能触发扩容,效率就会低很多,O(n)。
LinkedList 因为是链表结构,插入和删除只需要改变前置节点、后置节点和插入节点的引用就行了,不需要移动元素。
如果是在链表的头部插入或者删除,时间复杂度是 O(1);如果是在链表的中间插入或者删除,时间复杂度是 O(n),因为需要遍历链表找到插入位置;如果是在链表的尾部插入或者删除,时间复杂度是 O(1)。
三分恶面渣逆袭:ArrayList和LinkedList中间插入
三分恶面渣逆袭:ArrayList和LinkedList中间删除
注意,这里有个陷阱,LinkedList 更利于增删不是体现在时间复杂度上,因为二者增删的时间复杂度都是 O(n),都需要遍历列表;而是体现在增删的效率上,因为 LinkedList 的增删只需要改变引用,而 ArrayList 的增删可能需要移动元素。
HashSet 其实是由 HashMap 实现的,只不过值由一个固定的 Object 对象填充,而键用于操作。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
// ……
}
实际开发中,HashSet 并不常用,比如,如果我们需要按照顺序存储一组元素,那么 ArrayList 和 LinkedList 可能更适合;如果我们需要存储键值对并根据键进行查找,那么 HashMap 可能更适合。
HashSet 主要用于去重,比如,我们需要统计一篇文章中有多少个不重复的单词,就可以使用 HashSet 来实现。
// 创建一个 HashSet 对象
HashSet<String> set = new HashSet<>();
// 添加元素
set.add("沉默");
set.add("王二");
set.add("陈清扬");
set.add("沉默");
// 输出 HashSet 的元素个数
System.out.println("HashSet size: " + set.size()); // output: 3
// 遍历 HashSet
for (String s : set) {
System.out.println(s);
}
HashSet 会自动去重,因为它是用 HashMap 实现的,HashMap 的键是唯一的(哈希值),相同键的值会覆盖掉原来的值,于是第二次 set.add("沉默") 的时候就覆盖了第一次的 set.add("沉默")。
三分恶面渣逆袭:HashSet套娃
类加载过程有:载入、验证、准备、解析、初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段会发生在初始化阶段之后。
载入过程中,JVM 需要做三件事情:
三分恶面渣逆袭:载入
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。载入阶段结束后,JVM 外部的二进制字节流就按照虚拟机所设定的格式存储在方法区(逻辑概念)中了,方法区中的数据存储格式完全由虚拟机自行实现。
JVM 会在验证阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。
JVM 会在准备阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化,初始化为数据类型的默认值,如 0、0L、null、false 等。
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、成员方法等。
初始化阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值了,而在初始化阶段,类变量将被赋值为代码期望赋的值。
换句话说,初始化阶段是执行类的构造方法(javap 中看到的 <clinit>()
方法)的过程。
可以为 Java 应用程序的运行提供一致性和安全性的保障。
①、保证 Java 核心类库的类型安全
如果自定义类加载器优先加载一个类,比如说自定义的 Object,那在 Java 运行时环境中就存在多个版本的 java.lang.Object,双亲委派模型确保了 Java 核心类库的类加载工作由启动类加载器统一完成,从而保证了 Java 应用程序都是使用的同一份核心类库。
②、避免类的重复加载
在双亲委派模型中,类加载器会先委托给父加载器尝试加载类,这样同一个类不会被加载多次。如果没有这种模型,可能会导致同一个类被不同的类加载器重复加载到内存中,造成浪费和冲突。
在 Java 中,当创建一个子类对象时,子类和父类的静态代码块、构造方法的执行顺序遵循一定的规则。这些规则主要包括以下几个步骤:
下面是一个详细的代码示例:
class Parent {
// 父类静态代码块
static {
System.out.println("父类静态代码块");
}
// 父类构造方法
public Parent() {
System.out.println("父类构造方法");
}
}
class Child extends Parent {
// 子类静态代码块
static {
System.out.println("子类静态代码块");
}
// 子类构造方法
public Child() {
System.out.println("子类构造方法");
}
}
public class Main {
public static void main(String[] args) {
new Child();
}
}
执行上述代码时,输出结果如下:
父类静态代码块
子类静态代码块
父类构造方法
子类构造方法
synchronized 是一个关键字,而 Lock 属于一个接口,其实现类主要有 ReentrantLock、ReentrantReadWriteLock。
三分恶面渣逆袭:synchronized和ReentrantLock的区别
synchronized 可以直接在方法上加锁,也可以在代码块上加锁(无需手动释放锁,锁会自动释放),而 ReentrantLock 必须手动声明来加锁和释放锁。
// synchronized 修饰方法
public synchronized void method() {
// 业务代码
}
// synchronized 修饰代码块
synchronized (this) {
// 业务代码
}
// ReentrantLock 加锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 业务代码
} finally {
lock.unlock();
}
随着 JDK 版本的升级,synchronized 的性能已经可以媲美 ReentrantLock 了,加入了偏向锁、轻量级锁和重量级锁的自适应优化等,所以可以大胆地用。
如果需要更细粒度的控制(如可中断的锁操作、尝试非阻塞获取锁、超时获取锁或者使用公平锁等),可以使用 Lock。
lock.lockInterruptibly()
来实现这个机制。Lock 还提供了newCondition()
方法来创建等待通知条件Condition,比 synchronized 与 wait()
、 notify()/notifyAll()
方法的组合更强大。
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
三次握手和四次挥手都是工作在传输层。传输层(Transport Layer)是 OSI 模型的第四层,负责提供端到端的通信服务,包括数据传输的建立、维护和终止。
TCP 作为一种面向连接的协议,通过三次握手建立连接,通过四次挥手终止连接,确保数据传输的可靠性和完整性。
序列化(Serialization)是指将对象转换为字节流的过程,以便能够将该对象保存到文件、数据库,或者进行网络传输。
反序列化(Deserialization)就是将字节流转换回对象的过程,以便构建原始对象。
三分恶面渣逆袭:序列化和反序列化
Serializable
接口用于标记一个类可以被序列化。
public class Person implements Serializable {
private String name;
private int age;
// 省略 getter 和 setter 方法
}
serialVersionUID 是 Java 序列化机制中用于标识类版本的唯一标识符。它的作用是确保在序列化和反序列化过程中,类的版本是兼容的。
import java.io.Serializable;
public class MyClass implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
// getters and setters
}
serialVersionUID 被设置为 1L 是一种比较省事的做法,也可以使用 Intellij IDEA 进行自动生成。
但只要 serialVersionUID 在序列化和反序列化过程中保持一致,就不会出现问题。
如果不显式声明 serialVersionUID,Java 运行时会根据类的详细信息自动生成一个 serialVersionUID。那么当类的结构发生变化时,自动生成的 serialVersionUID 就会发生变化,导致反序列化失败。