华为今年的校招薪资已经开奖了,社交平台上看了一圈,还是有喜有悲!
有的审批失败:接到华子降温电话了,但我的offer也已经全都失效了!,all in 华为导致面临 0 offer的尴尬境地;有的审批通过,顺利开出高薪offer,一些重要的前沿技术岗位甚至开出了 100w+ 的恐怖年薪。
目前来看还是一线城市优先开,二线城市次之,北京、上海已经开出了不少了,其中岗位主要集中在技术岗,比如开发、算法等!
今年华为的校招薪资波动幅度还是蛮大的,算法类岗位总体来说依旧很不错,软件开发类岗位相对少一些(薪资数据来源于 offershow+社区讨论贴+读者分享):
算法:硕士 985,(base 27k + 绩效 7k)*12+年终 10w,上海 通用软件开发:硕士 985,25k*16,上海 应用软件:硕士 211,22k*16,上海 鸿蒙开发:硕士 985,22k*15,北京 AIGC 算法:博士 985,总包 108W AI 工程师:本科 211,25k*14,上海 数据存储:硕士 211,总包 45W,上海
华为技术面试对于求职者的技术要求相比较于互联网大厂来说,还是低一些的,会看重学历一些。
另外,华为面试的手撕算法可能会比较多,但难度相比较于大厂也要偏低一些。
面试华为的话,需要做好泡池子的准备,这个池子还是“挺大”的。反正就是吊着你,可能要等很久才有下一步的进度。
接下来,分享一位读者的华为软件开发的一二面的面经。
简单介绍一下自己
基本每一面都会先让你自我介绍,这个一定要提前好好准备。
一个好的自我介绍应该包含这几点要素:
用简单的话说清楚自己主要的技术栈于擅长的领域,例如 Java 后端开发、分布式系统开发; 把重点放在自己的优势上,重点突出自己的能力比如自己的定位的 bug 的能力特别厉害; 避免避实就虚,适当举例体现自己的能力,例如过往的比赛经历、实习经历; 自我介绍的时间不宜过长,一般是 1~2 分钟之间。
项目拷打
面试考察八股不多,几乎都是在拷打项目。面试官对着项目经历上的工作内容部分,一条接一条的拷打。
作为求职者,我们可以从这些方案去准备项目经历的回答:
你对项目基本情况(比如项目背景、核心功能)以及整体设计(比如技术栈、系统架构)的了解(面试官可能会让你画系统的架构图、让你讲解某个模块或功能的数据库表设计) 你在这个项目中你担任了什么角色?负责了什么?有什么贡献?(具体说明你在项目中的职责和贡献) 你在这个项目中是否解决过什么问题?怎么解决的?收获了什么?(展现解决问题的能力) 你在这个项目用到了哪些技术?这些技术你吃透了没有?(举个例子,你的项目经历使用了 Seata 来做分布式事务,那 Seata 相关的问题你要提前准备一下吧,比如说 Seata 支持哪些配置中心、Seata 的事务分组是怎么做的、Seata 支持哪些事务模式,怎么选择?) 你在这个项目中犯过的错误,最后是怎么弥补的?(承认不足并改进才能走的更远) 从这个项目中你学会了那些东西?学会了那些新技术的使用?(总结你在这个项目中的收获)
什么是进程和线程?有什么区别?
进程(Process) 是指计算机中正在运行的一个程序实例。举例:你打开的微信就是一个进程。 线程(Thread) 也被称为轻量级进程,更加轻量。多个线程可以在同一个进程中同时执行,并且共享进程的资源比如内存空间、文件句柄、网络连接等。举例:你打开的微信里就有一个线程专门用来拉取别人发你的最新的消息。
一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。
总结:
线程是进程划分成的更小的运行单位,一个进程在其执行的过程中可以产生多个线程。 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。 线程执行开销小,但不利于资源的管理和保护;而进程正相反。
怎么创建线程?
一般来说,创建线程有很多种方式,例如继承Thread
类、实现Runnable
接口、实现Callable
接口、使用线程池、使用CompletableFuture
类等等。
不过,这些方式其实并没有真正创建出线程。准确点来说,这些都属于是在 Java 代码中使用多线程的方法。
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()
创建。不管是哪种方式,最终还是依赖于new Thread().start()
。
关于这个问题的详细分析可以查看这篇文章:大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!。
Java 线程的状态有哪几种?
Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态:
NEW: 初始状态,线程被创建出来但没有被调用 start()
。RUNNABLE: 运行状态,线程被调用了 start()
等待运行的状态。BLOCKED:阻塞状态,需要等待锁释放。 WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。 TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。 TERMINATED:终止状态,表示该线程已经运行完毕。
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。
Java 线程状态变迁图(图源:挑错 |《Java 并发编程的艺术》中关于线程状态的三处错误):
由上图可以看出:线程创建之后它将处于 NEW(新建) 状态,调用 start()
方法后开始运行,线程这时候处于 READY(可运行) 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 RUNNING(运行) 状态。
在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态(图源:HowToDoInJava[1]:Java Thread Life Cycle and Thread States[2]),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
为什么 JVM 没有区分这两种状态呢? (摘自:Java 线程运行怎么有第六种状态?- Dawell 的回答[3] )
现在的时分(time-sharing)多任务(multi-task)操作系统架构通常都是用所谓的“时间分片(time quantum or time slice)”方式进行抢占式(preemptive)轮转调度(round-robin 式)。
这个时间分片通常是很小的,一个线程一次最多只能在 CPU 上运行比如 10-20ms 的时间(此时处于 running 状态),也即大概只有 0.01 秒这一量级,时间片用后就要被切换下来放入调度队列的末尾等待再次调度。(也即回到 ready 状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
当线程执行 wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或wait(long millis)
方法可以将线程置于 TIMED_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。当线程进入 synchronized
方法/块或者调用wait
后(被notify
)重新进入synchronized
方法/块,但是锁被其它线程占有,这个时候线程就会进入 BLOCKED(阻塞) 状态。线程在执行完了 run()
方法之后将会进入到 TERMINATED(终止) 状态。
相关阅读:线程的几种状态你真的了解么? 。
Java 集合的种类?
Java 集合, 也叫作容器,主要是由两大接口派生而来:一个是 Collection
接口,主要用于存放单一元素;另一个是 Map
接口,主要用于存放键值对。对于Collection
接口,下面又有三个主要的子接口:List
、Set
和 Queue
。
Java 集合框架如下图所示:
注:图中只列举了主要的继承派生关系,并没有列举所有关系。比方省略了AbstractList
, NavigableSet
等抽象类以及其他的一些辅助类,如想深入了解,可自行查看源码。
List
(对付顺序的好帮手): 存储的元素是有序的、可重复的。Set
(注重独一无二的性质): 存储的元素不可重复的。Queue
(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。Map
(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),"x" 代表 key,"y" 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。
HashMap 的底层实现是?
JDK1.8 之前
HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashcode
经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash
判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
HashMap
中的扰动函数(hash
方法)是用来优化哈希值的分布。通过对原始的 hashCode()
进行额外处理,扰动函数可以减小由于糟糕的 hashCode()
实现导致的碰撞,从而提高数据的分布均匀性。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加简化,但是原理不变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^:按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK1.7 的 HashMap 的 hash 方法源码.
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
多线程交替打印 A、B、B 如何实现?
这个问题华为 OD 也考察过,我怀疑华为 OD 的面试也是正式员工负责的。对应的文章地址:华为 OD 面试:三个线程交替打印 ABC 如何实现?。
这里我们采用 ReentrantLock
+ Condition
实现。
public class ABCPrinter {
private final int max;
// 用来指示当前应该打印的线程序号,0-A, 1-B, 2-C
private int turn = 0;
private final ReentrantLock lock = new ReentrantLock();
private final Condition conditionA = lock.newCondition();
private final Condition conditionB = lock.newCondition();
private final Condition conditionC = lock.newCondition();
public ABCPrinter(int max) {
this.max = max;
}
public void printA() {
print("A", conditionA, conditionB);
}
public void printB() {
print("B", conditionB, conditionC);
}
public void printC() {
print("C", conditionC, conditionA);
}
private void print(String name, Condition currentCondition, Condition nextCondition) {
for (int i = 0; i < max; i++) {
lock.lock();
try {
// 等待直到轮到当前线程打印
// turn 变量的值需要与线程要打印的字符相对应,例如,如果turn是0,且当前线程应该打印"A",则条件满足。如果不满足,当前线程调用currentCondition.await()进入等待状态。
while (!((turn == 0 && name.charAt(0) == 'A') || (turn == 1 && name.charAt(0) == 'B') || (turn == 2 && name.charAt(0) == 'C'))) {
currentCondition.await();
}
System.out.println(Thread.currentThread().getName() + " : " + name);
// 更新打印轮次,并唤醒下一个线程
turn = (turn + 1) % 3;
nextCondition.signal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}
}
在上面的代码中,三个线程的协调主要依赖:
ReentrantLock lock
: 用于线程同步的可重入锁,确保同一时刻只有一个线程能修改共享资源。Condition conditionA/B/C
: 分别与"A"、"B"、"C"线程关联的条件变量,用于线程间的协调通信。一个线程执行完之后,通过调用nextCondition.signal()
唤醒下一个应该打印的线程。
手撕算法
给定数组,将连续的首尾用-连接输出(例如,1-5),单个输出单个数字 二叉树最大深度 有效括号 单词拆分 II
参考资料
[1]HowToDoInJava: https://howtodoinJava.com/
[2]Java Thread Life Cycle and Thread States: https://howtodoinJava.com/Java/multi-threading/Java-thread-life-cycle-and-thread-states/
[3]Java 线程运行怎么有第六种状态?- Dawell 的回答: https://www.zhihu.com/question/56494969/answer/154053599
好文推荐
你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,这是我在校期间的编程学习之路,详细记录了我是如何自学技术以应对第二年的校招秋招的。
毕业后我先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责的项目已经顺利盈利300w+。在研三那年就组建了一个阿秀的学习圈,一直持续分享校招/社招跳槽找工作的经验,都是自己一路走过来的经验,目前已经累计服务超过 4000 +人,欢迎点此了解一二。