大家好,我是程序员小灰。
最近,小米 25 届秋招软件开发岗位的薪资已经开奖了。
不少同学看到开奖薪资之后都直言,小米手机有性价比,没想到工资开的也有性价比,直接泪崩了。
我从网上爆料同学的反馈,收集了小米今年软开的校招薪资开奖情况,小米研发二线城市主要是分布在武汉和南京,一线城市是分布在北京、深圳、上海,不同城市的薪资也有一些差距,毕竟一二线城市的生活开销还是不一样的。
- 武汉:11k x 15 = 16.5w、15k x 15 = 22.5w 、18k x 15 = 27w
- 南京:12k x 15 = 18w、15k x 15 = 22.5w
整体来看,年薪是在 16w~30w 之间,中位数是 20w+年薪,跟去年基本一样。
小米薪资相比互联网大厂差距还是比较明显的,比如美团今年的白菜档是 23k x 15.5,ssp offer 都达到 30k x 15.5了,实在是香。
针对小米公司的开发岗位,我目前还没有看到有人爆料超过 20k+的,不过听到一些消息说,小米开发岗位的 ssp offer 是 24k,去年的小米 ssp 薪资确实就是 24k,今年的话大概率也是的。
虽然相比互联网一线大厂是少了一些,但是也属于二线梯队的大厂了。
小米的面试压力确实比互联网大厂小了很多,互联网大厂一场面试都是持续 1 小时,问 20-30 个面试题,而小米一场面试面试问题一般就 10-15 个,当然也会有手撕算法环节,所以要冲中大厂的同学,算法是逃不了的,必须多练。
对了,有一些同学反馈,小米面试是在飞书上面进行的,写代码全都是ACM模式,力扣刷多的同学可以练一下ACM模式,不然到时候写链表都吭哧吭哧的。
这次,跟大家分享今年小米秋招 Java开发的面经,主要拷打了 8 个八股 + 2 个算法题,大家看看难度如何?
小米一面(50 分钟+)
Kafka和RocketMQ的区别?
Kafka的优缺点:
- 优点:首先,Kafka的最大优势就在于它的高吞吐量,在普通机器4CPU8G的配置下,一台机器可以抗住十几万的QPS,这一点还是相当优越的。Kafka支持集群部署,如果部分机器宕机不可用,则不影响Kafka的正常使用。
- 缺点:Kafka有可能会造成数据丢失,因为它在收到消息的时候,并不是直接写到物理磁盘的,而是先写入到磁盘缓冲区里面的。Kafka功能比较的单一主要的就是支持收发消息,高级功能基本没有,就会造成适用场景受限。
RocketMQ是阿里巴巴开源的消息中间件,优缺点:
- 优点:支持功能比较多,比如延迟队列、消息事务等等,吞吐量也高,单机吞吐量达到 10 万级,支持大规模集群部署,线性扩展方便,Java语言开发,满足了国内绝大部分公司技术栈
- 缺点:性能相比 kafka 是弱一点,因为 kafka 用到了 sendfile 的零拷贝技术,而 RocketMQ 主要是用 mmap+write 来实现零拷贝。
该怎么选择呢?
- 如果我们业务只是收发消息这种单一类型的需求,而且可以允许小部分数据丢失的可能性,但是又要求极高的吞吐量和高性能的话,就直接选Kafka就行了,就好比我们公司想要收集和传输用户行为日志以及其他相关日志的处理,就选用的Kafka中间件。
- 如果公司的需要通过 mq 来实现一些业务需求,比如延迟队列、消息事务等,公司技术栈主要是Java语言的话,就直接一步到位选择RocketMQ,这样会省很多事情。
反射是什么?有什么作用?
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类中的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 Java 语言的反射机制。
比如,获取一个Class
对象。Class.forName(完整类名)
。通过Class
对象获取类的构造方法,class.getConstructor
。根据Class
对象获取类的方法,getMethod
和getMethods
。使用Class
对象创建一个对象,class.newInstance
等。
反射具有以下特性:
- 运行时类信息访问:反射机制允许程序在运行时获取类的完整结构信息,包括类名、包名、父类、实现的接口、构造函数、方法和字段等。
- 动态对象创建:可以使用反射API动态地创建对象实例,即使在编译时不知道具体的类名。这是通过Class类的newInstance()方法或Constructor对象的newInstance()方法实现的。
- 动态方法调用:可以在运行时动态地调用对象的方法,包括私有方法。这通过Method类的invoke()方法实现,允许你传入对象实例和参数值来执行方法。
- 访问和修改字段值:反射还允许程序在运行时访问和修改对象的字段值,即使是私有的。这是通过Field类的get()和set()方法完成的。
反射的优点就是增加灵活性,可以在运行时动态获取对象实例。缺点是反射的效率很低,而且会破坏封装,通过反射可以访问类的私有方法,不安全。
类加载的具体过程,双亲委派机制
我们把 Java 的类加载过程分为三个主要步骤:加载、链接、初始化。
首先是加载阶段(Loading),它是 Java 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象),这里的数据源可能是各种各样的形态,如 jar 文件、class 文件,甚至是网络数据源等;如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError。
加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
第二阶段是链接(Linking),这是核心的步骤,简单说是把原始的类定义信息平滑地转化入 JVM 运行的过程中。这里可进一步细分为三个步骤:
- 验证(Verification),这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的,否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。
- 准备(Preparation),创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的 JVM 指令。
- 解析(Resolution),在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
最后是初始化阶段(initialization),这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
再来谈谈双亲委派模型,简单说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器去做。使用委派模型的目的是避免重复加载 Java 类型。
TCP四次挥手的过程?
具体过程:
- 客户端主动调用关闭连接的函数,于是就会发送 FIN 报文,这个 FIN 报文代表客户端不会再发送数据了,进入 FIN_WAIT_1 状态;
- 服务端收到了 FIN 报文,然后马上回复一个 ACK 确认报文,此时服务端进入 CLOSE_WAIT 状态。在收到 FIN 报文的时候,TCP 协议栈会为 FIN 包插入一个文件结束符 EOF 到接收缓冲区中,服务端应用程序可以通过 read 调用来感知这个 FIN 包,这个 EOF 会被放在已排队等候的其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
- 接着,当服务端在 read 数据的时候,最后自然就会读到 EOF,接着 read() 就会返回 0,这时服务端应用程序如果有数据要发送的话,就发完数据后才调用关闭连接的函数,如果服务端应用程序没有数据要发送的话,可以直接调用关闭连接的函数,这时服务端就会发一个 FIN 包,这个 FIN 报文代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
- 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状态;
- 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
- 客户端经过 2MSL 时间之后,也进入 CLOSE 状态;
多线程的优势,为什么要有多线程?
使用多线程的理由之一是和进程相比,它是一种非常花销小,切换快,更"节俭"的多任务操作方式。在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
- 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,可以避免这种尴尬的情况。
- 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
- 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
死锁的具体原因,怎么解决?
死锁只有同时满足以下四个条件才会发生:
- 互斥条件:互斥条件是指多个线程不能同时使用同一个资源。
- 持有并等待条件:持有并等待条件是指,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
- 不可剥夺条件:不可剥夺条件是指,当线程已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
- 环路等待条件:环路等待条件指的是,在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破环环路等待条件。
那什么是资源有序分配法呢?线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。
Java并发的工作原理?
Java并发编程是指在Java程序中同时执行多个线程,以提高程序的运行效率和响应能力,Java中的Thread
类和Runnable
接口是实现多线程的基础。每个线程都有自己独立的执行路径,通过调用Thread
类的start()
方法启动线程。
Java线程的生命周期包括:
- 就绪状态(Runnable):线程准备好并等待CPU分配时间片。
- 阻塞状态(Blocked):线程因某种原因(如等待锁)而被阻塞。
- 死亡状态(Terminated):线程已执行完毕。
要保证多线程的允许是安全,不要出现数据竞争造成的数据混乱的问题。Java的线程安全在三个方面体现:
- 原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,在Java中使用了atomic和synchronized这两个关键字来确保原子性;
- 可见性:一个线程对主内存的修改可以及时地被其他线程看到,在Java中使用了synchronized和volatile这两个关键字确保可见性;
- 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,在Java中使用了happens-before原则来确保有序性。
Java提供了多种锁机制,可以分为以下几类:
- 内置锁(synchronized):Java中的
synchronized
关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized
代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁。 - ReentrantLock:
java.util.concurrent.locks.ReentrantLock
是一个显式的锁类,提供了比synchronized
更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock
使用lock()
和unlock()
方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。 - 读写锁(ReadWriteLock):
java.util.concurrent.locks.ReadWriteLock
接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。 - 乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。
synchronized
和ReentrantLock
都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。 - 自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。
常用的git操作有哪些?
几个专用名词的译名如下:
工作流程
最基础的工作流程,首先执行 git pull
获取远程仓库的最新代码,进行代码的编写。
完成相应功能的开发后执行 git add .
将工作区代码的修改添加到暂存区,再执行 git commit -m 完成xx功能
将暂存区代码提交到本地仓库并添加相应的注释,最后执行 git push
命令推送到远程仓库。
撤回 git commit 操作
当执行了 git commit -m 注释内容
命令想要撤回,可以使用 git reset --soft HEAD^
把本地仓库回退到当前版本的上一个版本,也就是刚刚还没提交的时候,代码的改动会保留在暂存区和工作区。
也可以使用 git reset --mixed HEAD^
,这样不止回退了刚刚的 git commit
操作,还回退了 git add
操作,代码的改动只会保留在工作区。因为 --mixed
参数是 git reset
命令的默认选项,也就是可以写为 git reset HEAD^
。
撤回 git push 操作
当执行了 git push
命令想要撤回,可以使用 git reset HEAD^
将本地仓库回退到当前版本的上一个版本,代码的修改会保留在工作区,然后使用 git push origin xxx --force
将本地仓库当前版本的代码强制推送到远程仓库。
算法题
leetcode 3.无重复字符的最长子串(中等)
leetcode 88. 合并两个有序数组(简单)
< END >
对了,最近小灰新创建了一个程序员面试交流群,欢迎大家扫码添加小灰微信,备注“面试”,小灰会给大家拉到群里: