華為今年的校招薪資已經(jīng)開獎(jiǎng)了,有喜有悲!
今年華為的校招薪資波動(dòng)幅度還是蠻大的,算法類崗位總體來說很不錯(cuò),一些重要的前沿技術(shù)崗位甚至開出了 100w+ 的恐怖年薪,軟件開發(fā)類崗位相對(duì)少一些(薪資數(shù)據(jù)來源于 offershow+社區(qū)討論貼+讀者分享) :
算法:碩士 985,(base 27k + 績(jī)效 7k)*12+年終 10w,上海 通用軟件開發(fā):碩士 985,25k*16,上海 應(yīng)用軟件:碩士 211,22k*16,上海 鴻蒙開發(fā):碩士 985,22k*15,北京 數(shù)據(jù)存儲(chǔ):碩士 211,總包 45W,上海 華為技術(shù)面試對(duì)于求職者的技術(shù)要求相比較于互聯(lián)網(wǎng)大廠來說,還是低一些的,會(huì)看重學(xué)歷一些。 另外,華為面試的手撕算法可能會(huì)比較多,但難度相比較于大廠也要偏低一些。
面試華為的話,需要做好泡池子的準(zhǔn)備,這個(gè)池子還是“挺大 ”的。反正就是吊著你,可能要等很久才有下一步的進(jìn)度。
接下來,分享一位讀者的華為軟件開發(fā)的一二面的面經(jīng)。
簡(jiǎn)單介紹一下自己 基本每一面都會(huì)先讓你自我介紹,這個(gè)一定要提前好好準(zhǔn)備。
一個(gè)好的自我介紹應(yīng)該包含這幾點(diǎn)要素:
用簡(jiǎn)單的話說清楚自己主要的技術(shù)棧于擅長(zhǎng)的領(lǐng)域,例如 Java 后端開發(fā)、分布式系統(tǒng)開發(fā); 把重點(diǎn)放在自己的優(yōu)勢(shì)上,重點(diǎn)突出自己的能力比如自己的定位的 bug 的能力特別厲害; 避免避實(shí)就虛,適當(dāng)舉例體現(xiàn)自己的能力,例如過往的比賽經(jīng)歷、實(shí)習(xí)經(jīng)歷; 自我介紹的時(shí)間不宜過長(zhǎng),一般是 1~2 分鐘之間。 項(xiàng)目拷打 面試考察八股不多,幾乎都是在拷打項(xiàng)目。面試官對(duì)著項(xiàng)目經(jīng)歷上的工作內(nèi)容部分,一條接一條的拷打。
作為求職者,我們可以從這些方案去準(zhǔn)備項(xiàng)目經(jīng)歷的回答:
你對(duì)項(xiàng)目基本情況(比如項(xiàng)目背景、核心功能)以及整體設(shè)計(jì)(比如技術(shù)棧、系統(tǒng)架構(gòu))的了解(面試官可能會(huì)讓你畫系統(tǒng)的架構(gòu)圖、讓你講解某個(gè)模塊或功能的數(shù)據(jù)庫(kù)表設(shè)計(jì)) 你在這個(gè)項(xiàng)目中你擔(dān)任了什么角色?負(fù)責(zé)了什么?有什么貢獻(xiàn)?(具體說明你在項(xiàng)目中的職責(zé)和貢獻(xiàn)) 你在這個(gè)項(xiàng)目中是否解決過什么問題?怎么解決的?收獲了什么?(展現(xiàn)解決問題的能力) 你在這個(gè)項(xiàng)目用到了哪些技術(shù)?這些技術(shù)你吃透了沒有?(舉個(gè)例子,你的項(xiàng)目經(jīng)歷使用了 Seata 來做分布式事務(wù),那 Seata 相關(guān)的問題你要提前準(zhǔn)備一下吧,比如說 Seata 支持哪些配置中心、Seata 的事務(wù)分組是怎么做的、Seata 支持哪些事務(wù)模式,怎么選擇?) 你在這個(gè)項(xiàng)目中犯過的錯(cuò)誤,最后是怎么彌補(bǔ)的?(承認(rèn)不足并改進(jìn)才能走的更遠(yuǎn)) 從這個(gè)項(xiàng)目中你學(xué)會(huì)了那些東西?學(xué)會(huì)了那些新技術(shù)的使用?(總結(jié)你在這個(gè)項(xiàng)目中的收獲) 什么是進(jìn)程和線程?有什么區(qū)別? 進(jìn)程(Process) 是指計(jì)算機(jī)中正在運(yùn)行的一個(gè)程序?qū)嵗?。舉例:你打開的微信就是一個(gè)進(jìn)程。線程(Thread) 也被稱為輕量級(jí)進(jìn)程,更加輕量。多個(gè)線程可以在同一個(gè)進(jìn)程中同時(shí)執(zhí)行,并且共享進(jìn)程的資源比如內(nèi)存空間、文件句柄、網(wǎng)絡(luò)連接等。舉例:你打開的微信里就有一個(gè)線程專門用來拉取別人發(fā)你的最新的消息。一個(gè)進(jìn)程中可以有多個(gè)線程,多個(gè)線程共享進(jìn)程的堆和方法區(qū) (JDK1.8 之后的元空間)資源,但是每個(gè)線程有自己的程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧。
Java 運(yùn)行時(shí)數(shù)據(jù)區(qū)域(JDK1.8 之后) 總結(jié):
線程是進(jìn)程劃分成的更小的運(yùn)行單位,一個(gè)進(jìn)程在其執(zhí)行的過程中可以產(chǎn)生多個(gè)線程。 線程和進(jìn)程最大的不同在于基本上各進(jìn)程是獨(dú)立的,而各線程則不一定,因?yàn)橥贿M(jìn)程中的線程極有可能會(huì)相互影響。 線程執(zhí)行開銷小,但不利于資源的管理和保護(hù);而進(jìn)程正相反。 怎么創(chuàng)建線程? 一般來說,創(chuàng)建線程有很多種方式,例如繼承Thread
類、實(shí)現(xiàn)Runnable
接口、實(shí)現(xiàn)Callable
接口、使用線程池、使用CompletableFuture
類等等。
不過,這些方式其實(shí)并沒有真正創(chuàng)建出線程。準(zhǔn)確點(diǎn)來說,這些都屬于是在 Java 代碼中使用多線程的方法。
嚴(yán)格來說,Java 就只有一種方式可以創(chuàng)建線程,那就是通過new Thread().start()
創(chuàng)建。不管是哪種方式,最終還是依賴于new Thread().start()
。
關(guān)于這個(gè)問題的詳細(xì)分析可以查看這篇文章:大家都說 Java 有三種創(chuàng)建線程的方式!并發(fā)編程中的驚天騙局! 。
Java 線程的狀態(tài)有哪幾種? Java 線程在運(yùn)行的生命周期中的指定時(shí)刻只可能處于下面 6 種不同狀態(tài)的其中一個(gè)狀態(tài):
NEW: 初始狀態(tài),線程被創(chuàng)建出來但沒有被調(diào)用 start()
。 RUNNABLE: 運(yùn)行狀態(tài),線程被調(diào)用了 start()
等待運(yùn)行的狀態(tài)。 BLOCKED:阻塞狀態(tài),需要等待鎖釋放。 WAITING:等待狀態(tài),表示該線程需要等待其他線程做出一些特定動(dòng)作(通知或中斷)。 TIME_WAITING:超時(shí)等待狀態(tài),可以在指定的時(shí)間后自行返回而不是像 WAITING 那樣一直等待。 TERMINATED:終止?fàn)顟B(tài),表示該線程已經(jīng)運(yùn)行完畢。 線程在生命周期中并不是固定處于某一個(gè)狀態(tài)而是隨著代碼的執(zhí)行在不同狀態(tài)之間切換。
Java 線程狀態(tài)變遷圖(圖源:挑錯(cuò) |《Java 并發(fā)編程的藝術(shù)》中關(guān)于線程狀態(tài)的三處錯(cuò)誤 ):
Java 線程狀態(tài)變遷圖 由上圖可以看出:線程創(chuàng)建之后它將處于 NEW(新建) 狀態(tài),調(diào)用 start()
方法后開始運(yùn)行,線程這時(shí)候處于 READY(可運(yùn)行) 狀態(tài)。可運(yùn)行狀態(tài)的線程獲得了 CPU 時(shí)間片(timeslice)后就處于 RUNNING(運(yùn)行) 狀態(tài)。
在操作系統(tǒng)層面,線程有 READY 和 RUNNING 狀態(tài);而在 JVM 層面,只能看到 RUNNABLE 狀態(tài)(圖源:HowToDoInJava [1] :Java Thread Life Cycle and Thread States [2] ),所以 Java 系統(tǒng)一般將這兩個(gè)狀態(tài)統(tǒng)稱為 RUNNABLE(運(yùn)行中) 狀態(tài) 。
為什么 JVM 沒有區(qū)分這兩種狀態(tài)呢? (摘自:Java 線程運(yùn)行怎么有第六種狀態(tài)?- Dawell 的回答 [3] ) 現(xiàn)在的時(shí)分(time-sharing)多任務(wù)(multi-task)操作系統(tǒng)架構(gòu)通常都是用所謂的“時(shí)間分片(time quantum or time slice)”方式進(jìn)行搶占式(preemptive)輪轉(zhuǎn)調(diào)度(round-robin 式)。這個(gè)時(shí)間分片通常是很小的,一個(gè)線程一次最多只能在 CPU 上運(yùn)行比如 10-20ms 的時(shí)間(此時(shí)處于 running 狀態(tài)),也即大概只有 0.01 秒這一量級(jí),時(shí)間片用后就要被切換下來放入調(diào)度隊(duì)列的末尾等待再次調(diào)度。(也即回到 ready 狀態(tài))。線程切換的如此之快,區(qū)分這兩種狀態(tài)就沒什么意義了。
RUNNABLE-VS-RUNNING 當(dāng)線程執(zhí)行 wait()
方法之后,線程進(jìn)入 WAITING(等待) 狀態(tài)。進(jìn)入等待狀態(tài)的線程需要依靠其他線程的通知才能夠返回到運(yùn)行狀態(tài)。 TIMED_WAITING(超時(shí)等待) 狀態(tài)相當(dāng)于在等待狀態(tài)的基礎(chǔ)上增加了超時(shí)限制,比如通過 sleep(long millis)
方法或 wait(long millis)
方法可以將線程置于 TIMED_WAITING 狀態(tài)。當(dāng)超時(shí)時(shí)間結(jié)束后,線程將會(huì)返回到 RUNNABLE 狀態(tài)。當(dāng)線程進(jìn)入 synchronized
方法/塊或者調(diào)用 wait
后(被 notify
)重新進(jìn)入 synchronized
方法/塊,但是鎖被其它線程占有,這個(gè)時(shí)候線程就會(huì)進(jìn)入 BLOCKED(阻塞) 狀態(tài)。 線程在執(zhí)行完了 run()
方法之后將會(huì)進(jìn)入到 TERMINATED(終止) 狀態(tài)。 相關(guān)閱讀:線程的幾種狀態(tài)你真的了解么? 。
Java 集合的種類? Java 集合, 也叫作容器,主要是由兩大接口派生而來:一個(gè)是 Collection
接口,主要用于存放單一元素;另一個(gè)是 Map
接口,主要用于存放鍵值對(duì)。對(duì)于Collection
接口,下面又有三個(gè)主要的子接口:List
、Set
和 Queue
。
Java 集合框架如下圖所示:
Java 集合框架概覽 注:圖中只列舉了主要的繼承派生關(guān)系,并沒有列舉所有關(guān)系。比方省略了AbstractList
, NavigableSet
等抽象類以及其他的一些輔助類,如想深入了解,可自行查看源碼。
List
(對(duì)付順序的好幫手): 存儲(chǔ)的元素是有序的、可重復(fù)的。Set
(注重獨(dú)一無二的性質(zhì)): 存儲(chǔ)的元素不可重復(fù)的。Queue
(實(shí)現(xiàn)排隊(duì)功能的叫號(hào)機(jī)): 按特定的排隊(duì)規(guī)則來確定先后順序,存儲(chǔ)的元素是有序的、可重復(fù)的。Map
(用 key 來搜索的專家): 使用鍵值對(duì)(key-value)存儲(chǔ),類似于數(shù)學(xué)上的函數(shù) y=f(x),'x' 代表 key,'y' 代表 value,key 是無序的、不可重復(fù)的,value 是無序的、可重復(fù)的,每個(gè)鍵最多映射到一個(gè)值。HashMap 的底層實(shí)現(xiàn)是? JDK1.8 之前 HashMap
底層是 數(shù)組和鏈表 結(jié)合在一起使用也就是 鏈表散列 。HashMap 通過 key 的 hashcode
經(jīng)過擾動(dòng)函數(shù)處理過后得到 hash 值,然后通過 (n - 1) & hash
判斷當(dāng)前元素存放的位置(這里的 n 指的是數(shù)組的長(zhǎng)度),如果當(dāng)前位置存在元素的話,就判斷該元素與要存入的元素的 hash 值以及 key 是否相同,如果相同的話,直接覆蓋,不相同就通過拉鏈法解決沖突。
HashMap
中的擾動(dòng)函數(shù)(hash
方法)是用來優(yōu)化哈希值的分布。通過對(duì)原始的 hashCode()
進(jìn)行額外處理,擾動(dòng)函數(shù)可以減小由于糟糕的 hashCode()
實(shí)現(xiàn)導(dǎo)致的碰撞,從而提高數(shù)據(jù)的分布均勻性。
JDK 1.8 HashMap 的 hash 方法源碼:
JDK 1.8 的 hash 方法 相比于 JDK 1.7 hash 方法更加簡(jiǎn)化,但是原理不變。
static final int hash (Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^:按位異或 // >>>:無符號(hào)右移,忽略符號(hào)位,空位都以0補(bǔ)齊 return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
對(duì)比一下 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 方法的性能會(huì)稍差一點(diǎn)點(diǎn),因?yàn)楫吘箶_動(dòng)了 4 次。
所謂 “拉鏈法” 就是:將鏈表和數(shù)組相結(jié)合。也就是說創(chuàng)建一個(gè)鏈表數(shù)組,數(shù)組中每一格就是一個(gè)鏈表。若遇到哈希沖突,則將沖突的值加到鏈表中即可。
jdk1.8 之前的內(nèi)部結(jié)構(gòu)-HashMap JDK1.8 之后 相比于之前的版本, JDK1.8 之后在解決哈希沖突時(shí)有了較大的變化,當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)為 8)(將鏈表轉(zhuǎn)換成紅黑樹前會(huì)判斷,如果當(dāng)前數(shù)組的長(zhǎng)度小于 64,那么會(huì)選擇先進(jìn)行數(shù)組擴(kuò)容,而不是轉(zhuǎn)換為紅黑樹)時(shí),將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時(shí)間。
jdk1.8之后的內(nèi)部結(jié)構(gòu)-HashMap TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底層都用到了紅黑樹。紅黑樹就是為了解決二叉查找樹的缺陷,因?yàn)槎娌檎覙湓谀承┣闆r下會(huì)退化成一個(gè)線性結(jié)構(gòu)。
多線程交替打印 A、B、B 如何實(shí)現(xiàn)? 這個(gè)問題華為 OD 也考察過,我懷疑華為 OD 的面試也是正式員工負(fù)責(zé)的。對(duì)應(yīng)的文章地址:華為 OD 面試:三個(gè)線程交替打印 ABC 如何實(shí)現(xiàn)? 。
這里我們采用 ReentrantLock
+ Condition
實(shí)現(xiàn) 。
public class ABCPrinter { private final int max; // 用來指示當(dāng)前應(yīng)該打印的線程序號(hào),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 { // 等待直到輪到當(dāng)前線程打印 // turn 變量的值需要與線程要打印的字符相對(duì)應(yīng),例如,如果turn是0,且當(dāng)前線程應(yīng)該打印'A',則條件滿足。如果不滿足,當(dāng)前線程調(diào)用currentCondition.await()進(jìn)入等待狀態(tài)。 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); // 更新打印輪次,并喚醒下一個(gè)線程 turn = (turn + 1 ) % 3 ; nextCondition.signal(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } } } }
在上面的代碼中,三個(gè)線程的協(xié)調(diào)主要依賴:
ReentrantLock lock
: 用于線程同步的可重入鎖,確保同一時(shí)刻只有一個(gè)線程能修改共享資源。Condition conditionA/B/C
: 分別與'A'、'B'、'C'線程關(guān)聯(lián)的條件變量,用于線程間的協(xié)調(diào)通信。一個(gè)線程執(zhí)行完之后,通過調(diào)用nextCondition.signal()
喚醒下一個(gè)應(yīng)該打印的線程。手撕算法 給定數(shù)組,將連續(xù)的首尾用-連接輸出(例如,1-5),單個(gè)輸出單個(gè)數(shù)字 [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 線程運(yùn)行怎么有第六種狀態(tài)?- Dawell 的回答: https://www.zhihu.com/question/56494969/answer/154053599