偷偷盘点一下京东研发岗薪资

共 10792字,需浏览 22分钟

 ·

2024-05-23 14:04

大家好,我是二哥呀。

京东这几天的热度真的非常高,据说零售部门开始严查考勤,并且调整了午休时间,整整缩短了一个小时,从原来的 11:30-13:30 调整为 12:00-13:00。

更关键的是,晚上 6 点下班的员工要掂量掂量自己的工作饱和度。

我只能说,东哥这是不打算和兄弟们做兄弟了吗?(😂)

山雨欲来风满楼,这种举措也就意味着新的风暴即将来袭,这段时间京东的小伙伴们就要注意了,简历该更新要记得更新,八股、算法、项目该复盘的注意复盘,为下一步做好充足的准备。

另外,我也统计了一波京东 24 届的校招薪资,主要是后端开发、前端、测开和产品经理,25 届打算冲京东的小伙伴可以拿来作为一个参考。

数据来源于牛客和 offershow

能看得出,Java 后端开发主要集中在 23k*16 这个段位上,包括硕士 211、硕士 985、211 本。只能说,大厂香是香,卷也是真的卷~

这次我们就以《Java 面试指南-京东面经》同学 2 的后端面试为例, 来看看京东的面试官都喜欢问哪些八股,好背的滚瓜烂熟,了然于胸~

让天下所有的面渣都能逆袭 😁

题目不少,火箭造的飞起。主要还是围绕着二哥强调的 Java 后端四大件为主,所以大家在准备的时候一定要有的放矢,知道哪些是重点。

京东面经(狠狠拷打)

如何保证redis和数据库一致性?(答:延时双删)

在技术派实战项目中,我采用的是先写 MySQL,再删除 Redis 的方式来保证缓存和数据库的数据一致性。

技术派教程

对于第一次查询,请求 B 查询到的缓存数据是 10,但 MySQL 被请求 A 更新为了 11,此时数据库和缓存不一致。

但也只存在这一次不一致的情况,对于不是强一致性的业务,可以容忍。

当请求 B 第二次查询时,因为请求 A 更新完数据库把缓存删除了,所以请求 B 这次不会命中缓存,会重新查一次 MySQL,然后回写到 Redis。

缓存和数据库又一致了。

那假如对一致性要求很高,该怎么办呢?

延时双删防止脏数据

简单说,就是在第一次删除缓存之后,过一段时间之后,再次删除缓存。

主要针对缓存不存在,但写入了脏数据的情况。在先删缓存,再写数据库的更新策略下发生的比较多。

三分恶面渣逆袭:延时双删

这种方式的延时时间需要仔细考量和测试。

说说mq原理,怎么保证消息接受顺序?

消息队列(Message Queue, MQ)是一种非常重要的中间件技术,广泛应用于分布式系统中,以提高系统的可用性、解耦能力和异步通信效率。

生产者将消息放入队列,消费者从队列中取出消息,这样一来,生产者和消费者之间就不需要直接通信,生产者只管生产消息,消费者只管消费消息,这样就实现了解耦。

三分恶面渣逆袭:消息队列解耦

与此同时,系统可以将那些耗时的任务放在消息队列中异步处理,从而快速响应用户的请求。

三分恶面渣逆袭:消息队列异步

RocketMQ 实现顺序消息的关键在于保证消息生产和消费过程中严格的顺序控制,即确保同一业务的消息按顺序发送到同一个队列中,并由同一个消费者线程按顺序消费。

三分恶面渣逆袭:顺序消息

局部顺序消息如何实现?

局部顺序消息保证在某个逻辑分区或业务逻辑下的消息顺序,例如同一个订单或用户的消息按顺序消费,而不同订单或用户之间的顺序不做保证。

三分恶面渣逆袭:部分顺序消息

全局顺序消息如何实现?

全局顺序消息保证消息在整个系统范围内的严格顺序,即消息按照生产的顺序被消费。

可以将所有消息发送到一个单独的队列中,确保所有消息按生产顺序发送和消费。

三分恶面渣逆袭:全局顺序消息

项目压测了嘛?(Jmeter)

我会使用 Jmeter 对项目进行压测,通过合理配置线程组、HTTP 请求和监听器,可以模拟真实的用户负载,并分析项目在高负载下的表现。

arraylist,linkedlist,hashset区别和使用场景,线程安全?

多数情况下,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>, Cloneablejava.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套娃

hashset为什么是随机的?其他两个是按存入顺序的?

  • ArrayList 是基于动态数组实现的,HashSet 是基于 HashMap 实现的。
  • LinkedList 是基于链表实现的,本身也可以拿来作为队列。
  • ArrayList 允许重复元素和 null 值,可以有多个相同的元素;HashSet 保证每个元素唯一,不允许重复元素,基于元素的 hashCode 和 equals 方法来确定元素的唯一性。
  • ArrayList 保持元素的插入顺序,可以通过索引访问元素;HashSet 不保证元素的顺序,元素的存储顺序依赖于哈希算法,并且可能随着元素的添加或删除而改变。

说说类加载过程(5步)

类加载过程有:载入、验证、准备、解析、初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段会发生在初始化阶段之后。

载入过程中,JVM 需要做三件事情:

三分恶面渣逆袭:载入
  • 1)通过一个类的全限定名来获取定义此类的二进制字节流。
  • 2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

载入阶段结束后,JVM 外部的二进制字节流就按照虚拟机所设定的格式存储在方法区(逻辑概念)中了,方法区中的数据存储格式完全由虚拟机自行实现。

JVM 会在验证阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。

JVM 会在准备阶段对类变量(也称为静态变量,static 关键字修饰的变量)分配内存并初始化,初始化为数据类型的默认值,如 0、0L、null、false 等。

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、成员方法等。

初始化阶段是类加载过程的最后一步。在准备阶段,类变量已经被赋过默认初始值了,而在初始化阶段,类变量将被赋值为代码期望赋的值。

换句话说,初始化阶段是执行类的构造方法(javap 中看到的 <clinit>() 方法)的过程。

双亲委派模型好处?

可以为 Java 应用程序的运行提供一致性和安全性的保障。

①、保证 Java 核心类库的类型安全

如果自定义类加载器优先加载一个类,比如说自定义的 Object,那在 Java 运行时环境中就存在多个版本的 java.lang.Object,双亲委派模型确保了 Java 核心类库的类加载工作由启动类加载器统一完成,从而保证了 Java 应用程序都是使用的同一份核心类库。

②、避免类的重复加载

在双亲委派模型中,类加载器会先委托给父加载器尝试加载类,这样同一个类不会被加载多次。如果没有这种模型,可能会导致同一个类被不同的类加载器重复加载到内存中,造成浪费和冲突。

new子类的时候,子类和父类静态代码块,构造方法的执行顺序

在 Java 中,当创建一个子类对象时,子类和父类的静态代码块、构造方法的执行顺序遵循一定的规则。这些规则主要包括以下几个步骤:

  1. 首先执行父类的静态代码块(仅在类第一次加载时执行)。
  2. 接着执行子类的静态代码块(仅在类第一次加载时执行)。
  3. 再执行父类的构造方法。
  4. 最后执行子类的构造方法。

下面是一个详细的代码示例:

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区别原理

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。

  • ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制。
  • ReentrantLock 可以指定是公平锁还是非公平锁。
  • ReentrantReadWriteLock 读写锁,读锁是共享锁,写锁是独占锁,读锁可以同时被多个线程持有,写锁只能被一个线程持有。这种锁的设计可以提高性能,特别是在读操作的数量远远超过写操作的情况下。

Lock 还提供了newCondition()方法来创建等待通知条件Condition,比 synchronized 与 wait()notify()/notifyAll()方法的组合更强大。

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

tcp三次握手四次挥手工作在哪一层?

三次握手和四次挥手都是工作在传输层。传输层(Transport Layer)是 OSI 模型的第四层,负责提供端到端的通信服务,包括数据传输的建立、维护和终止。

TCP 作为一种面向连接的协议,通过三次握手建立连接,通过四次挥手终止连接,确保数据传输的可靠性和完整性。

用过序列化和反序列化吗?

序列化(Serialization)是指将对象转换为字节流的过程,以便能够将该对象保存到文件、数据库,或者进行网络传输。

反序列化(Deserialization)就是将字节流转换回对象的过程,以便构建原始对象。

三分恶面渣逆袭:序列化和反序列化

Serializable 接口有什么用?

Serializable接口用于标记一个类可以被序列化。

public class Person implements Serializable {
    private String name;
    private int age;
    // 省略 getter 和 setter 方法
}

serialVersionUID 有什么用?

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 就会发生变化,导致反序列化失败。

内容来源

  • 星球嘉宾三分恶的面渣逆袭:https://javabetter.cn/sidebar/sanfene/nixi.html
  • 二哥的 Java 进阶之路(GitHub 已有 12000+star):https://javabetter.cn

ending

一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 5300 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。

两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远

欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

浏览 1896
2点赞
评论
收藏
分享

手机扫一扫分享

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

手机扫一扫分享

分享
举报