圖解學(xué)習(xí)網(wǎng)站:https://xiaolincoding.com
大家好,我是小林。
現(xiàn)在新能源汽車的競爭已經(jīng)開始進入下半場了,各家公司都開始進行組織架構(gòu)調(diào)整,決戰(zhàn)決賽圈,如果不能在決賽圈勝出,可能企業(yè)很難存活下來,市場就是這么殘酷,贏家通吃,輸家出局。
之前發(fā)過理想、蔚來、比亞迪、極越等車企公司的薪資情況和面試真題,這次來補上小鵬汽車的!
小鵬汽車 2025 年計劃招聘 6000 人,即使之前從小鵬離職過的,也可以重新加入小鵬。
之所以今年大力開始招聘,也是因為小鵬汽車穿過了暴風雨,在 2024 年度銷量有突破性的增長,現(xiàn)在小鵬汽車月銷 4w+臺是常態(tài),而且應(yīng)該會很快上升到 5w+臺,銷量上去了,自然要擴大員工,在新的一年,將繼續(xù)推出更多新型號的汽車。
這次來看看 25 屆小鵬汽車校招的薪資,目前還沒看到 Java 崗的薪資,所以列了一些其他崗位的薪資情況,也可以做個參考:
- 軟件測試:18.5k x 15 = 27.7w,辦公地點廣州,同學(xué)背景碩士 985嵌入式開發(fā):20k * 15 = 30w,辦公地點上海,同學(xué)背景碩士211產(chǎn)品經(jīng)理:18.5 x 15 = 27.7w,辦公地點廣州,同學(xué)背景碩士其他影像開發(fā):25k x 15 = 37.5w,辦公地點廣州,同學(xué)背景碩士 985
小鵬汽車的薪資對比互聯(lián)網(wǎng)公司大廠的話,是會少一些,大概是互聯(lián)網(wǎng)中廠的薪資水平,如果同學(xué)手上有互聯(lián)網(wǎng)公司選擇的話,小鵬汽車的競爭力就會弱一些了。新能源汽車里開的薪資能對標互聯(lián)網(wǎng)大廠的,目前來看是理想汽車了。
肯定也有同學(xué)好奇,小鵬汽車的面試難度如何?
這次來看看小鵬汽車Java崗位的校招面經(jīng),主要考察計算機基、Java、JVM、網(wǎng)絡(luò)、算法這些內(nèi)容,難度的話,算中等,算法還是會考察,大部分新能源汽車都會考察,不管面互聯(lián)網(wǎng)公司,還是新能源車企,算法大家都需要準備。
計算機基礎(chǔ)
ARM有了解嘛?
了解不過,大概知道是一個處理器,嵌入式領(lǐng)域用的多。
二進制怎么轉(zhuǎn)16機制?
可以采用分組轉(zhuǎn)換法。
分組轉(zhuǎn)換法基于二進制和十六進制之間的位權(quán)關(guān)系。因為2^4=16,這意味著 4 位二進制數(shù)能夠表示的狀態(tài)數(shù)剛好與十六進制的一位所能表示的狀態(tài)數(shù)相同。所以可以將二進制數(shù)按每 4 位一組進行劃分,每一組都能唯一地對應(yīng)一個十六進制數(shù)字。
下面我給出了二進制的數(shù),我們將它轉(zhuǎn)換為十六進制,例如:0101101,我們將這個數(shù)按4個一組來劃分,變成 0010 1101(這里本來是010 1101前面不夠4位我們就湊一個0),可以得到 0010 =2、 1101=D
所以轉(zhuǎn)換成十六進制就是2D。
byte類型的-1怎么表示?
byte
類型是有符號的 8 位整數(shù),取值范圍是-128
到127
。-1
在byte
類型中的二進制表示是補碼形式,正數(shù)的補碼與原碼相同,負數(shù)的補碼是在反碼的基礎(chǔ)上加 1,這是因為計算機中采用補碼來進行減法運算,可以將減法轉(zhuǎn)換為加法,方便硬件實現(xiàn),計算過程如下:
先寫出1的原碼:00000001
。然后得到-1
的原碼:10000001
。接著求-1
的反碼:11111110
。最后求-1
的補碼:11111111
。
所以,在 Java 的byte
類型中,-1
用二進制補碼表示為11111111
。當進行運算或存儲時,計算機使用這個補碼來處理-1
相關(guān)的操作。例如,在進行加法運算時,-1 + 1
的計算過程如下:
- 1
的補碼是11111111
,1
的補碼是00000001
。相加得到:11111111 + 00000001 = 100000000
(9 位,超出byte
范圍)。由于byte
類型是 8 位,會發(fā)生截斷,得到00000000
,也就是0
,這符合數(shù)學(xué)運算結(jié)果。
Java
兩個方法都被synchronized修飾,其中一個調(diào)用另一個可以成功嘛?synchronized修飾方法鎖的那一部分?
如果兩個方法都被synchronized
修飾,一個方法內(nèi)部調(diào)用另一個方法是可以成功的。這是因為synchronized
方法默認是對當前對象(this
)加鎖。當一個線程進入了一個synchronized
方法,它已經(jīng)獲得了該對象的鎖,在這個方法內(nèi)部調(diào)用另一個synchronized
方法時,由于是同一個對象的鎖,所以線程可以繼續(xù)執(zhí)行被調(diào)用的synchronized
方法,不會出現(xiàn)鎖競爭導(dǎo)致無法調(diào)用的情況。
例如下面的代碼,method1
調(diào)用method2
時,因為它們都是同一個對象example
的synchronized
方法,所以可以正常執(zhí)行。
public?class?SynchronizedExample?{
????public?synchronized?void?method1()?{
????????System.out.println("Method?1?started");
????????method2();
????????System.out.println("Method?1?ended");
????}
????public?synchronized?void?method2()?{
????????System.out.println("Method?2?is?running");
????}
????public?static?void?main(String[]?args)?{
????????SynchronizedExample?example?=?new?SynchronizedExample();
????????example.method1();
????}
}
synchronized 修飾方法鎖的對象:
對于非靜態(tài)方法:當synchronized
修飾一個非靜態(tài)方法時,鎖的是當前對象(this
)。這意味著同一時刻,對于同一個對象實例,只有一個線程能夠執(zhí)行這個對象的synchronized
非靜態(tài)方法。不同的對象實例之間的synchronized
非靜態(tài)方法可以被不同的線程同時執(zhí)行,因為它們的鎖對象(this
)是不同的。
對于靜態(tài)方法:當synchronized
修飾一個靜態(tài)方法時,鎖的是這個類的Class
對象。因為靜態(tài)方法是屬于類的,而不是屬于某個具體的對象實例。所以同一時刻,對于一個類的所有實例,只有一個線程能夠執(zhí)行這個類的synchronized
靜態(tài)方法。例如,下面的例子,staticMethod1
和staticMethod2
都是靜態(tài)的synchronized
方法,它們共享同一個類的Class
對象作為鎖。所以當thread1
和thread2
同時啟動時,其中一個方法會先獲得類的Class
對象鎖,另一個方法需要等待鎖釋放后才能執(zhí)行。
public?class?SynchronizedStaticExample?{
????public?static?synchronized?void?staticMethod1()?{
????????System.out.println("Static?Method?1?started");
????}
????public?static?synchronized?void?staticMethod2()?{
????????System.out.println("Static?Method?2?started");
????}
????public?static?void?main(String[]?args)?{
????????Thread?thread1?=?new?Thread(()?->?{
????????????SynchronizedStaticExample.staticMethod1();
????????});
????????Thread?thread2?=?new?Thread(()?->?{
????????????SynchronizedStaticExample.staticMethod2();
????????});
????????thread1.start();
????????thread2.start();
????}
}
靜態(tài)內(nèi)部類和匿名內(nèi)部類有什么區(qū)別嗎?
靜態(tài)內(nèi)部類是定義在另一個類內(nèi)部的類,并且使用static
關(guān)鍵字修飾。它就像是類的一個靜態(tài)成員,不依賴于外部類的實例,就像下面的例子中,StaticInnerClass
可以直接訪問OuterClass
的outerStaticVar
靜態(tài)變量。
class?OuterClass?{
????private?static?int?outerStaticVar?=?10;
????static?class?StaticInnerClass?{
????????public?void?printOuterStaticVar()?{
????????????System.out.println(outerStaticVar);
????????}
????}
}
靜態(tài)內(nèi)部類不能直接訪問外部類的非靜態(tài)成員,因為非靜態(tài)成員是依賴于外部類的實例存在的。如果要訪問外部類的非靜態(tài)成員,需要通過外部類的實例來訪問。
靜態(tài)內(nèi)部類的生命周期與外部類的靜態(tài)成員相似。它在外部類加載時不會自動加載,只有在第一次被使用(例如,通過new
關(guān)鍵字創(chuàng)建實例或者訪問靜態(tài)成員)時才會加載。加載后,只要類加載器沒有卸載這個類,它就一直存在于內(nèi)存中。
實例化靜態(tài)內(nèi)部類時,不需要外部類的實例??梢灾苯油ㄟ^外部類名.靜態(tài)內(nèi)部類名
的方式來創(chuàng)建實例,例如OuterClass.StaticInnerClass innerObj = new OuterClass.StaticInnerClass();
。
當一個類只與另一個類有比較緊密的關(guān)聯(lián),并且主要是為了輔助外部類完成某些功能,同時又不依賴于外部類的實例時,適合使用靜態(tài)內(nèi)部類。例如,一個工具類中的一些工具方法可以組織成靜態(tài)內(nèi)部類,這些方法可能會共享一些外部類的靜態(tài)資源。靜態(tài)內(nèi)部類還可以用于實現(xiàn)單例模式。通過將單例對象的實例化放在靜態(tài)內(nèi)部類中,可以保證在第一次訪問單例對象時才進行實例化,并且保證了線程安全。
匿名內(nèi)部類是一種沒有名字的內(nèi)部類。它是在創(chuàng)建對象的同時定義類的一種方式,通常用于只需要使用一次的類,并且是作為某個接口或者抽象類的實現(xiàn)(或者某個類的子類)出現(xiàn)。例如,在下面實現(xiàn)接口的例子中,匿名內(nèi)部類是在main
方法內(nèi)部定義的,它的行為可能會受到main
方法中的其他變量或者外部類的狀態(tài)的影響。
interface?MyInterface?{
????void?myMethod();
}
class?Main?{
????public?static?void?main(String[]?args)?{
????????MyInterface?anonymousClass?=?new?MyInterface()?{
????????????@Override
????????????public?void?myMethod()?{
????????????????System.out.println("This?is?an?anonymous?class?implementing?MyInterface");
????????????}
????????};
????????anonymousClass.myMethod();
????}
}
匿名內(nèi)部類可以訪問外部類的成員變量和方法,包括靜態(tài)和非靜態(tài)的。如果訪問外部類的局部變量,這些局部變量必須是final
(在 Java 8 之后,實際上是隱式final
)的,這是為了保證在匿名內(nèi)部類的生命周期內(nèi),這些變量的值不會被改變。
匿名內(nèi)部類的生命周期取決于它的使用場景。如果它是在一個方法內(nèi)部定義的,那么當方法執(zhí)行結(jié)束后,只要沒有其他引用指向這個匿名內(nèi)部類的對象,它就會被垃圾回收。如果它是作為一個類的成員變量定義的,那么它的生命周期會和這個類的對象生命周期相關(guān)。匿名內(nèi)部類在定義的同時就會被實例化,并且只能創(chuàng)建一個實例。因為它沒有類名,所以不能像普通類一樣通過new
關(guān)鍵字在其他地方再次創(chuàng)建實例。
當只需要臨時實現(xiàn)一個接口或者繼承一個抽象類來提供特定的功能,并且這個實現(xiàn)類只使用一次時,匿名內(nèi)部類是一個很好的選擇。它避免了為一個簡單的功能定義一個完整的類,從而簡化了代碼結(jié)構(gòu)。
匿名內(nèi)部內(nèi)可以使用外部類的引用嗎?靜態(tài)的呢?
HashMap和HashTable區(qū)別?
- HashMap線程不安全,效率高一點,可以存儲null的key和value,null的key只能有一個,null的value可以有多個。默認初始容量為16,每次擴充變?yōu)樵瓉?倍。創(chuàng)建時如果給定了初始容量,則擴充為2的冪次方大小。底層數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表,插入元素后如果鏈表長度大于閾值(默認為8),先判斷數(shù)組長度是否小于64,如果小于,則擴充數(shù)組,反之將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間。HashTable線程安全,效率低一點,其內(nèi)部方法基本都經(jīng)過synchronized修飾,不可以有null的key和value。默認初始容量為11,每次擴容變?yōu)樵瓉淼?n+1。創(chuàng)建時給定了初始容量,會直接用給定的大小。底層數(shù)據(jù)結(jié)構(gòu)為數(shù)組+鏈表。它基本被淘汰了,要保證線程安全可以用ConcurrentHashMap。ConcurrentHashMap是Java中的一個線程安全的哈希表實現(xiàn),它可以在多線程環(huán)境下并發(fā)地進行讀寫操作,而不需要像傳統(tǒng)的HashTable那樣在讀寫時加鎖。ConcurrentHashMap的實現(xiàn)原理主要基于分段鎖和CAS操作。它將整個哈希表分成了多Segment(段),每個Segment都類似于一個小的HashMap,它擁有自己的數(shù)組和一個獨立的鎖。在ConcurrentHashMap中,讀操作不需要鎖,可以直接對Segment進行讀取,而寫操作則只需要鎖定對應(yīng)的Segment,而不是整個哈希表,這樣可以大大提高并發(fā)性能。
講一下ConcurrentHashMap?
JDK 1.7 ConcurrentHashMap
在 JDK 1.7 中它使用的是數(shù)組加鏈表的形式實現(xiàn)的,而數(shù)組又分為:大數(shù)組 Segment 和小數(shù)組 HashEntry。Segment 是一種可重入鎖(ReentrantLock),在 ConcurrentHashMap 里扮演鎖的角色;HashEntry 則用于存儲鍵值對數(shù)據(jù)。一個 ConcurrentHashMap 里包含一個 Segment 數(shù)組,一個 Segment 里包含一個 HashEntry 數(shù)組,每個 HashEntry 是一個鏈表結(jié)構(gòu)的元素。
JDK 1.7 ConcurrentHashMap 分段鎖技術(shù)將數(shù)據(jù)分成一段一段的存儲,然后給每一段數(shù)據(jù)配一把鎖,當一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候,其他段的數(shù)據(jù)也能被其他線程訪問,能夠?qū)崿F(xiàn)真正的并發(fā)訪問。
JDK 1.8 ConcurrentHashMap
在 JDK 1.7 中,ConcurrentHashMap 雖然是線程安全的,但因為它的底層實現(xiàn)是數(shù)組 + 鏈表的形式,所以在數(shù)據(jù)比較多的情況下訪問是很慢的,因為要遍歷整個鏈表,而 JDK 1.8 則使用了數(shù)組 + 鏈表/紅黑樹的方式優(yōu)化了 ConcurrentHashMap 的實現(xiàn),具體實現(xiàn)結(jié)構(gòu)如下:
JDK 1.8 ConcurrentHashMap JDK 1.8 ConcurrentHashMap 主要通過 volatile + CAS 或者 synchronized 來實現(xiàn)的線程安全的。添加元素時首先會判斷容器是否為空:
如果為空則使用 ?volatile ?加 ?CAS ?來初始化
如果容器不為空,則根據(jù)存儲的元素計算該位置是否為空。
如果根據(jù)存儲的元素計算結(jié)果為空,則利用 ?CAS ?設(shè)置該節(jié)點;
如果根據(jù)存儲的元素計算結(jié)果不為空,則使用 synchronized ?,然后,遍歷桶中的數(shù)據(jù),并替換或新增節(jié)點到桶中,最后再判斷是否需要轉(zhuǎn)為紅黑樹,這樣就能保證并發(fā)訪問時的線程安全了。
如果把上面的執(zhí)行用一句話歸納的話,就相當于是ConcurrentHashMap通過對頭結(jié)點加鎖來保證線程安全的,鎖的粒度相比 Segment 來說更小了,發(fā)生沖突和加鎖的頻率降低了,并發(fā)操作的性能就提高了。
而且 JDK 1.8 使用的是紅黑樹優(yōu)化了之前的固定鏈表,那么當數(shù)據(jù)量比較大的時候,查詢性能也得到了很大的提升,從之前的 O(n) 優(yōu)化到了 O(logn) 的時間復(fù)雜度。
JVM
類加載過程?
類從被加載到虛擬機內(nèi)存開始,到卸載出內(nèi)存為止,它的整個生命周期包括以下 7 個階段:
加載:通過類的全限定名(包名 + 類名),獲取到該類的.class文件的二進制字節(jié)流,將二進制字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu),轉(zhuǎn)化為方法區(qū)運行時的數(shù)據(jù)結(jié)構(gòu),在內(nèi)存中生成一個代表該類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
連接:驗證、準備、解析 3 個階段統(tǒng)稱為連接。
驗證:確保class文件中的字節(jié)流包含的信息,符合當前虛擬機的要求,保證這個被加載的class類的正確性,不會危害到虛擬機的安全。驗證階段大致會完成以下四個階段的檢驗動作:文件格式校驗、元數(shù)據(jù)驗證、字節(jié)碼驗證、符號引用驗證
準備:為類中的靜態(tài)字段分配內(nèi)存,并設(shè)置默認的初始值,比如int類型初始值是0。被final修飾的static字段不會設(shè)置,因為final在編譯的時候就分配了
解析:解析階段是虛擬機將常量池的「符號引用」直接替換為「直接引用」的過程。符號引用是以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用的時候可以無歧義地定位到目標即可。直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄,直接引用是和虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的。如果有了直接引用, 那引用的目標必定已經(jīng)存在在內(nèi)存中了。
初始化:初始化是整個類加載過程的最后一個階段,初始化階段簡單來說就是執(zhí)行類的構(gòu)造器方法,要注意的是這里的構(gòu)造器方法()并不是開發(fā)者寫的,而是編譯器自動生成的。
使用:使用類或者創(chuàng)建對象
卸載:如果有下面的情況,類就會被卸載:1. 該類所有的實例都已經(jīng)被回收,也就是java堆中不存在該類的任何實例。2. 加載該類的ClassLoader已經(jīng)被回收。3. 類對應(yīng)的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。
雙親委派機制為什么叫雙親?有什么好處?
雙親委派模型,簡單說就是當類加載器(Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應(yīng)類型,否則盡量將這個任務(wù)代理給當前加載器的父加載器去做。使用委派模型的目的是避免重復(fù)加載 Java 類型。
“雙親” 并不是指有兩個父母一樣的角色。實際上,這里的 “雙親” 是一種形象的比喻,它是指除了最頂層的啟動類加載器(Bootstrap ClassLoader)外,每個類加載器都有一個父類加載器。當一個類加載器需要加載類時,它會先委托給它的父類加載器去嘗試加載,這個過程就好像孩子(子加載器)先請求父母(父加載器)幫忙做事一樣,所以稱為 “雙親委派”。
雙親委派機制好處主要是:
防止核心 API 被篡改:Java 的核心類庫(如java.lang包中的類)是由啟動類加載器(Bootstrap ClassLoader)加載的。因為雙親委派機制的存在,自定義的類加載器在加載類時,首先會將加載請求委托給父加載器。這就保證了像java.lang.Object
這樣的核心類不會被自定義的同名類隨意替換。例如,如果沒有雙親委派機制,惡意代碼可能會定義一個自己的java.lang.Object
類,并且通過自定義的類加載器加載,從而破壞 Java 程序的基本運行規(guī)則。
避免類的重復(fù)加載:由于類加載請求是由下向上委托,然后再從上向下嘗試加載。如果父加載器已經(jīng)成功加載了某個類,子加載器就不會再重復(fù)加載該類,從而避免了因多次加載同一類而可能導(dǎo)致的類型不一致等問題。例如,系統(tǒng)中有多個不同的類加載器都可能需要加載java.util.ArrayList
類,通過雙親委派機制,只有啟動類加載器會加載這個類,其他類加載器會直接使用已經(jīng)加載好的類。
保證類的一致性:
- 在 Java 的運行環(huán)境中,對于同樣全限定名的類,應(yīng)該只有一份字節(jié)碼被加載并使用。雙親委派機制確保了在整個類加載體系中,類的加載是有層次和順序的。例如,在一個復(fù)雜的 Java 應(yīng)用系統(tǒng)中,可能存在多個模塊都依賴于同一個第三方庫中的類。通過雙親委派機制,這些模塊所使用的該類是由同一個類加載器加載的,保證了在整個系統(tǒng)中該類的一致性,使得不同模塊之間可以正確地交互和共享對象。
class文件和字節(jié)碼文件的區(qū)別?
概念上的區(qū)別:
Class 文件:在 Java 中,.class
文件是 Java 編譯器(javac
)將.java
源文件編譯后生成的文件格式。它是一種二進制文件,存儲了 Java 程序的字節(jié)碼指令、常量池、訪問標志、類名、方法名、字段名等各種信息。可以把.class
文件看作是字節(jié)碼的一種物理存儲形式,是字節(jié)碼的載體。
字節(jié)碼(Byte - code):字節(jié)碼是一種中間形式的機器語言,它是 Java 程序經(jīng)過編譯后產(chǎn)生的指令集。字節(jié)碼是一種高度抽象的、與具體機器硬件無關(guān)的指令代碼,它可以在任何安裝了 Java 虛擬機(JVM)的平臺上執(zhí)行。字節(jié)碼指令是 JVM 能夠理解和執(zhí)行的基本單位,這些指令類似于匯編語言指令,但更加抽象和高級。
Class 文件用途
存儲和分發(fā):.class
文件是 Java 程序的一種可存儲和可分發(fā)的形式。當開發(fā)一個 Java 項目時,編譯器會生成一系列的.class
文件,這些文件可以被打包成.jar
文件或者部署到服務(wù)器等環(huán)境中,供其他程序使用或者在運行時被加載。
跨平臺基礎(chǔ):.class
文件的存在是 Java 實現(xiàn) “一次編寫,到處運行” 特性的基礎(chǔ)之一。因為不同的操作系統(tǒng)有不同的機器指令集,Java 編譯器將.java
源文件編譯成與平臺無關(guān)的.class
文件,然后由各個平臺上的 JVM 對.class
文件進行解釋執(zhí)行或者編譯成機器碼執(zhí)行。
字節(jié)碼用途
JVM 執(zhí)行的指令集:字節(jié)碼是 JVM 執(zhí)行 Java 程序的實際指令。當 JVM 加載.class
文件時,它會解析.class
文件中的字節(jié)碼指令,并按照字節(jié)碼指令的順序執(zhí)行操作。例如,當調(diào)用一個 Java 方法時,JVM 會讀取方法表中的字節(jié)碼指令,逐條執(zhí)行這些指令來完成方法的功能。
動態(tài)加載和執(zhí)行:字節(jié)碼的動態(tài)特性使得 Java 可以實現(xiàn)一些高級的功能,如動態(tài)代理、字節(jié)碼增強等。通過在運行時動態(tài)生成字節(jié)碼或者修改已有的字節(jié)碼,可以實現(xiàn)諸如 AOP等編程技術(shù),為 Java 程序提供了更大的靈活性。
弱引用和軟引用的區(qū)別?
- 軟引用是一種相對較強的引用類型。它所引用的對象在內(nèi)存足夠的情況下,不會被垃圾回收器回收;只有在內(nèi)存不足時,才會被回收。這使得軟引用適合用來緩存一些可能會被頻繁使用,但又不是必須一直存在的數(shù)據(jù),例如緩存圖片等資源。弱引用是一種比較弱的引用類型。被弱引用關(guān)聯(lián)的對象,只要垃圾回收器運行,無論當前內(nèi)存是否充足,都會被回收。它主要用于解決一些對象的生命周期管理問題,例如在哈希表中,如果鍵是弱引用,當對象沒有其他強引用時,就可以自動被回收,避免內(nèi)存泄漏。
網(wǎng)絡(luò)
計網(wǎng)分層結(jié)構(gòu)說一下?
OSI七層模型
為了使得多種設(shè)備能通過網(wǎng)絡(luò)相互通信,和為了解決各種不同設(shè)備在網(wǎng)絡(luò)互聯(lián)中的兼容性問題,國際標準化組織制定了開放式系統(tǒng)互聯(lián)通信參考模型(Open System Interconnection Reference Model),也就是 OSI 網(wǎng)絡(luò)模型,該模型主要有 7 層,分別是應(yīng)用層、表示層、會話層、傳輸層、網(wǎng)絡(luò)層、數(shù)據(jù)鏈路層以及物理層。
每一層負責的職能都不同,如下:
- 應(yīng)用層,負責給應(yīng)用程序提供統(tǒng)一的接口;表示層,負責把數(shù)據(jù)轉(zhuǎn)換成兼容另一個系統(tǒng)能識別的格式;會話層,負責建立、管理和終止表示層實體之間的通信會話;傳輸層,負責端到端的數(shù)據(jù)傳輸;網(wǎng)絡(luò)層,負責數(shù)據(jù)的路由、轉(zhuǎn)發(fā)、分片;數(shù)據(jù)鏈路層,負責數(shù)據(jù)的封幀和差錯檢測,以及 MAC 尋址;物理層,負責在物理網(wǎng)絡(luò)中傳輸數(shù)據(jù)幀;
由于 OSI 模型實在太復(fù)雜,提出的也只是概念理論上的分層,并沒有提供具體的實現(xiàn)方案。
事實上,我們比較常見,也比較實用的是四層模型,即 TCP/IP 網(wǎng)絡(luò)模型,Linux 系統(tǒng)正是按照這套網(wǎng)絡(luò)模型來實現(xiàn)網(wǎng)絡(luò)協(xié)議棧的。
TCP/IP模型
TCP/IP協(xié)議被組織成四個概念層,其中有三層對應(yīng)于ISO參考模型中的相應(yīng)層。ICP/IP協(xié)議族并不包含物理層和數(shù)據(jù)鏈路層,因此它不能獨立完成整個計算機網(wǎng)絡(luò)系統(tǒng)的功能,必須與許多其他的協(xié)議協(xié)同工作。TCP/IP 網(wǎng)絡(luò)通常是由上到下分成 4 層,分別是應(yīng)用層,傳輸層,網(wǎng)絡(luò)層和網(wǎng)絡(luò)接口層。
- 應(yīng)用層 支持 HTTP、SMTP 等最終用戶進程傳輸層 處理主機到主機的通信(TCP、UDP)網(wǎng)絡(luò)層 尋址和路由數(shù)據(jù)包(IP 協(xié)議)鏈路層 通過網(wǎng)絡(luò)的物理電線、電纜或無線信道移動比特
TCP為什么要三次握手?
三次握手的原因:
- 三次握手才可以阻止重復(fù)歷史連接的初始化(主要原因)三次握手才可以同步雙方的初始序列號三次握手才可以避免資源浪費
原因一:避免歷史連接
我們來看看 RFC 793 指出的 TCP 連接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
簡單來說,三次握手的首要原因是為了防止舊的重復(fù)連接初始化造成混亂。
我們考慮一個場景,客戶端先發(fā)送了 SYN(seq = 90)報文,然后客戶端宕機了,而且這個 SYN 報文還被網(wǎng)絡(luò)阻塞了,服務(wù)并沒有收到,接著客戶端重啟后,又重新向服務(wù)端建立連接,發(fā)送了 SYN(seq = 100)報文(注意!不是重傳 SYN,重傳的 SYN 的序列號是一樣的)。
看看三次握手是如何阻止歷史連接的:
客戶端連續(xù)發(fā)送多次 SYN(都是同一個四元組)建立連接的報文,在網(wǎng)絡(luò)擁堵情況下:
- 一個「舊 SYN 報文」比「最新的 SYN」 報文早到達了服務(wù)端,那么此時服務(wù)端就會回一個 SYN + ACK 報文給客戶端,此報文中的確認號是 91(90+1)。客戶端收到后,發(fā)現(xiàn)自己期望收到的確認號應(yīng)該是 100 + 1,而不是 90 + 1,于是就會回 RST 報文。服務(wù)端收到 RST 報文后,就會釋放連接。后續(xù)最新的 SYN 抵達了服務(wù)端后,客戶端與服務(wù)端就可以正常的完成三次握手了。
上述中的「舊 SYN 報文」稱為歷史連接,TCP 使用三次握手建立連接的最主要原因就是防止「歷史連接」初始化了連接。
如果是兩次握手連接,就無法阻止歷史連接,那為什么 TCP 兩次握手為什么無法阻止歷史連接呢?
我先直接說結(jié)論,主要是因為在兩次握手的情況下,服務(wù)端沒有中間狀態(tài)給客戶端來阻止歷史連接,導(dǎo)致服務(wù)端可能建立一個歷史連接,造成資源浪費。
你想想,在兩次握手的情況下,服務(wù)端在收到 SYN 報文后,就進入 ESTABLISHED 狀態(tài),意味著這時可以給對方發(fā)送數(shù)據(jù),但是客戶端此時還沒有進入 ESTABLISHED 狀態(tài),假設(shè)這次是歷史連接,客戶端判斷到此次連接為歷史連接,那么就會回 RST 報文來斷開連接,而服務(wù)端在第一次握手的時候就進入 ESTABLISHED 狀態(tài),所以它可以發(fā)送數(shù)據(jù)的,但是它并不知道這個是歷史連接,它只有在收到 RST 報文后,才會斷開連接。
可以看到,如果采用兩次握手建立 TCP 連接的場景下,服務(wù)端在向客戶端發(fā)送數(shù)據(jù)前,并沒有阻止掉歷史連接,導(dǎo)致服務(wù)端建立了一個歷史連接,又白白發(fā)送了數(shù)據(jù),妥妥地浪費了服務(wù)端的資源。
因此,要解決這種現(xiàn)象,最好就是在服務(wù)端發(fā)送數(shù)據(jù)前,也就是建立連接之前,要阻止掉歷史連接,這樣就不會造成資源浪費,而要實現(xiàn)這個功能,就需要三次握手。
所以,TCP 使用三次握手建立連接的最主要原因是防止「歷史連接」初始化了連接。
原因二:同步雙方初始序列號
TCP 協(xié)議的通信雙方, 都必須維護一個「序列號」, 序列號是可靠傳輸?shù)囊粋€關(guān)鍵因素,它的作用:
- 接收方可以去除重復(fù)的數(shù)據(jù);接收方可以根據(jù)數(shù)據(jù)包的序列號按序接收;可以標識發(fā)送出去的數(shù)據(jù)包中, 哪些是已經(jīng)被對方收到的(通過 ACK 報文中的序列號知道);
可見,序列號在 TCP 連接中占據(jù)著非常重要的作用,所以當客戶端發(fā)送攜帶「初始序列號」的 SYN 報文的時候,需要服務(wù)端回一個 ACK 應(yīng)答報文,表示客戶端的 SYN 報文已被服務(wù)端成功接收,那當服務(wù)端發(fā)送「初始序列號」給客戶端的時候,依然也要得到客戶端的應(yīng)答回應(yīng),這樣一來一回,才能確保雙方的初始序列號能被可靠的同步。
四次握手其實也能夠可靠的同步雙方的初始化序號,但由于第二步和第三步可以優(yōu)化成一步,所以就成了「三次握手」。
而兩次握手只保證了一方的初始序列號能被對方成功接收,沒辦法保證雙方的初始序列號都能被確認接收。
原因三:避免資源浪費
如果只有「兩次握手」,當客戶端發(fā)生的 SYN 報文在網(wǎng)絡(luò)中阻塞,客戶端沒有接收到 ACK 報文,就會重新發(fā)送 SYN ,由于沒有第三次握手,服務(wù)端不清楚客戶端是否收到了自己回復(fù)的 ACK 報文,所以服務(wù)端每收到一個 SYN 就只能先主動建立一個連接,這會造成什么情況呢?
如果客戶端發(fā)送的 SYN 報文在網(wǎng)絡(luò)中阻塞了,重復(fù)發(fā)送多次 SYN 報文,那么服務(wù)端在收到請求后就會建立多個冗余的無效鏈接,造成不必要的資源浪費。
即兩次握手會造成消息滯留情況下,服務(wù)端重復(fù)接受無用的連接請求 SYN 報文,而造成重復(fù)分配資源
算法
反轉(zhuǎn)鏈表
通過迭代遍歷鏈表,在遍歷過程中改變鏈表節(jié)點指針的指向,將當前節(jié)點的next
指針指向前一個節(jié)點,從而實現(xiàn)鏈表的反轉(zhuǎn)。需要使用三個指針來輔助操作,分別指向當前節(jié)點、前一個節(jié)點和后一個節(jié)點。
class?ListNode?{
????int?val;
????ListNode?next;
????ListNode(int?val)?{
????????this.val?=?val;
????}
}
public?class?ReverseLinkedList?{
????public?static?ListNode?reverseList(ListNode?head)?{
????????ListNode?prev?=?null;
????????ListNode?curr?=?head;
????????while?(curr!=?null)?{
????????????ListNode?nextTemp?=?curr.next;
????????????curr.next?=?prev;
????????????prev?=?curr;
????????????curr?=?nextTemp;
????????}
????????return?prev;
????}
}
在上述代碼中:
prev
初始化為null
,代表反轉(zhuǎn)后鏈表的末尾(也就是原鏈表的頭節(jié)點反轉(zhuǎn)后的前一個節(jié)點)。
curr
初始化為原鏈表的頭節(jié)點head
,然后在循環(huán)中,先保存當前節(jié)點的下一個節(jié)點到nextTemp
,接著將當前節(jié)點的next
指針指向前一個節(jié)點prev
,再更新prev
和curr
的值,繼續(xù)下一輪循環(huán),直到遍歷完整個鏈表,最后返回prev
,它就是反轉(zhuǎn)后鏈表的頭節(jié)點。