抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

在上一篇 我們中,我們分享了幾大互聯網公司面試的題目,本文就來

詳細分析面試題答案以及複習參考和整理的面試資料

首先是面試題答案公佈,在講解時我們主要分成如下幾塊:語言的基礎知識、中間件、操作系統、計算機網絡、手寫算法、開放題和項目經歷。對面試題和涉及的知識點進行整理,這樣更容易讓各位同學理解。不會按照提問的順序進行講解,還請見諒。

其次是 Java 複習參考和整理的面試資料。由於內容比較多,學習有 道 非常重要,我們介紹一下其中的要點和目錄,完整文件可以參見筆者提供的 pdf 資料。

面試題解析

Java 的語言基礎

Future 的缺陷?

Future 在異步編程中經常用到,Future 表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。

然而 Future 接口調用 get()方法取得處理的結果值時是 阻塞性 的,如果調用 Future 對象的 get()方法時,如果這個線程還沒執行完成,就一直主線程main阻塞到此線程完成為止,就算和它同時進行的其它線程已經執行完了,也要等待這個耗時線程執行完才能獲取結果,大大影響運行效率。那麼使用多線程就沒什麼意義了。

CompletionService 在依賴任務之間是如何實現的?

接上一個問你題,鑑於 Future 的缺陷,JDK 1.8 併發包也提供了CompletionService接口可以解決這個問題,它的take()方法哪個線程先完成就先獲取誰的 Futrue 對象。

volatile 怎麼搞

出現 volatile,是因為多線程的場景下存在髒讀。Java內存模型規定所有的變量都是存在主存當中,每個線程都有自己的工作內存。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。變量的值何時從線程的工作內存寫回主存,無法確定。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

volatile關鍵字的作用:保證了變量的可見性(visibility)。被volatile關鍵字修飾的變量,如果值發生了變更,其他線程立馬可見,避免出現髒讀的現象。如以下代碼片段,isShutDown被置為true後,doWork方法仍有執行。如用volatile修飾isShutDown變量,可避免此問題。

volatile只能保證變量的可見性,不能保證對volatile變量操作的原子性。

類加載機制?

JVM將class文件字節碼文件加載到內存中, 並將這些靜態數據轉換成方法區中的運行時數據結構,在堆(並不一定在堆中,HotSpot在方法區中)中生成一個代表這個類的java.lang.Class 對象,作為方法區類數據的訪問入口。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

JVM類加載機制分為五個部分:加載,驗證,準備,解析,初始化,下面我們就分別來看一下這五個過程。其中加載、檢驗、準備、初始化和卸載這個五個階段的順序是固定的,而解析則未必。為了支持動態綁定,解析這個過程可以發生在初始化階段之後。

  • 加載:加載過程主要完成三件事情,通過類的全限定名來獲取定義此類的二進制字節流;將這個類字節流代表的靜態存儲結構轉為方法區的運行時數據結構;在堆中生成一個代表此類的java.lang.Class對象,作為訪問方法區這些數據結構的入口。
  • 驗證:此階段主要確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機的自身安全。文件格式驗證:基於字節流驗證;元數據驗證:基於方法區的存儲結構驗證;字節碼驗證:基於方法區的存儲結構驗證;
    符號引用驗證:基於方法區的存儲結構驗證。
  • 準備:為類變量分配內存,並將其初始化為默認值。(此時為默認值,在初始化的時候才會給變量賦值)即在方法區中分配這些變量所使用的內存空間。例如:
<code>public static int value = 123;
/<code>

此時在準備階段過後的初始值為0而不是123。

  • 解析:把類型中的符號引用轉換為直接引用。符號引用與虛擬機實現的佈局無關,引用的目標並不一定要已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中;直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。
  • 初始化:初始化階段是執行類構造器 方法的過程。 方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合併而成的。虛擬機會保證 方法執行之前,父類的 方法已經執行完畢。如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不為這個類生成()方法。java中,對於初始化階段,有且只有以下五種情況才會對要求類立刻“初始化”(加載,驗證,準備,自然需要在此之前開始):使用new關鍵字實例化對象、訪問或者設置一個類的靜態字段(被final修飾、編譯器優化時已經放入常量池的例外)、調用類方法,都會初始化該靜態字段或者靜態方法所在的類。初始化類的時候,如果其父類沒有被初始化過,則要先觸發其父類初始化。使用java.lang.reflect包的方法進行反射調用的時候,如果類沒有被初始化,則要先初始化。虛擬機啟動時,用戶會先初始化要執行的主類(含有main)jdk 1.7後,如果java.lang.invoke.MethodHandle的實例最後對應的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,並且這個方法所在類沒有初始化,則先初始化。

類加載器?

把類加載階段的“通過一個類的全限定名來獲取描述此類的二進制字節流”這個動作交給虛擬機之外的類加載器來完成。這樣的好處在於,我們可以自行實現類加載器來加載其他格式的類,只要是二進制字節流就行,這就大大增強了加載器靈活性。系統自帶的類加載器分為三種:

  • 啟動類加載器。
  • 擴展類加載器。
  • 應用程序類加載器。
抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

雙親委派機制工作過程:

如果一個類加載器收到了類加載器的請求.它首先不會自己去嘗試加載這個類.而是把這個請求委派給父加載器去完成.每個層次的類加載器都是如此.因此所有的加載請求最終都會傳送到Bootstrap類加載器(啟動類加載器)中.只有父類加載反饋自己無法加載這個請求(它的搜索範圍中沒有找到所需的類)時.子加載器才會嘗試自己去加載。

雙親委派模型的優點:java類隨著它的加載器一起具備了一種帶有優先級的層次關係.

例如類java.lang.Object,它存放在rt.jart之中.無論哪一個類加載器都要加載這個類.最終都是雙親委派模型最頂端的Bootstrap類加載器去加載.因此Object類在程序的各種類加載器環境中都是同一個類.相反.如果沒有使用雙親委派模型.由各個類加載器自行去加載的話.如果用戶編寫了一個稱為“java.lang.Object”的類.並存放在程序的ClassPath中.那系統中將會出現多個不同的Object類.java類型體系中最基礎的行為也就無法保證.應用程序也將會一片混亂。

JDBC 加載機制?SPI 與雙親委派?

JDBC 加載機制:SPI ,全稱為(Service Provider Interface) ,是JDK內置的一種服務提供發現機制;主要被框架的開發人員使用,比如java.sql.Driver接口,數據庫廠商實現此接口即可,當然要想讓系統知道具體實現類的存在,還需要使用固定的存放規則,需要在classpath下的META-INF/services/目錄裡創建一個以服務接口命名的文件,這個文件裡的內容就是這個接口的具體的實現類。

SPI 服務機制破壞了雙親委派模型。可以看出雙親委派機制是一種至下而上的加載方式,那麼SPI是如何打破這種關係?

以JDBC加載驅動為例:在JDBC4.0之後支持SPI方式加載java.sql.Driver的實現類。SPI實現方式為,通過ServiceLoader.load(Driver.class)方法,去各自實現Driver接口的lib的META-INF/services/java.sql.Driver文件裡找到實現類的名字,通過Thread.currentThread().getContextClassLoader()類加載器加載實現類並返回實例。

驅動加載的過程大致如上,那麼是在什麼地方打破了雙親委派模型呢?

先看下如果不用Thread.currentThread().getContextClassLoader()加載器加載,整個流程會怎麼樣。

  • 從META-INF/services/java.sql.Driver文件得到實現類名字DriverA
    Class.forName(“xx.xx.DriverA”)來加載實現類
  • Class.forName()方法默認使用當前類的ClassLoader,JDBC是在D riverManager類裡調用Driver的,當前類也就是DriverManager,它的加載器是BootstrapClassLoader。
  • 用BootstrapClassLoader去加載非rt.jar包裡的類xx.xx.DriverA,就會找不到。
  • 要加載xx.xx.DriverA需要用到AppClassLoader或其他自定義ClassLoader

最終矛盾出現在,要在BootstrapClassLoader加載的類裡,調用AppClassLoader去加載實現類。

因此

在父加載器加載的類中去調用子加載器去加載類

  • jdk提供了兩種方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他們能加載classpath中的類
  • SPI 則用 Thread.currentThread().getContextClassLoader() 來加載實現類,實現在核心包裡的基礎類調用用戶代碼

面向對象的原則

  • 單一原則。一個類應該有且只有一個變化的原因。單一職責原則將不同的職責分離到單獨的類,每一個職責都是一個變化的中心。需求變化時,將通過更改職責相關的類來體現。如果一個類擁有多於一個的職責,則多個職責耦合在一起,會有多於一個原因來導致這個類發生變化。一個職責的變化可能會影響到其他的職責,另外,把多個職責耦合在一起,影響複用性。
  • 里氏替換原則,就是要求繼承是嚴格的is-a關係。所有引用基類的地方必須能透明地使用其子類的對象。在軟件中將一個基類對象替換成它的子類對象,程序將不會產生任何錯誤和異常,反過來則不成立,如果一個軟件實體使用的是一個子類對象的話,那麼它不一定能夠使用基類對象。例如:我喜歡動物,那我一定喜歡狗,因為狗是動物的子類;但是我喜歡狗,不能據此斷定我喜歡動物,因為我並不喜歡老鼠,雖然它也是動物。
  • 依賴倒置原則。依賴倒置原則的核心就是要我們面向接口編程,理解了面向接口編程,也就理解了依賴倒置。低層模塊儘量都要有抽象類或接口,或者兩者都有。變量的聲明類型儘量是抽象類或接口。
  • 接口分離原則。一個類對另一個類的依賴應該建立在最小的接口上,通俗的講就是需要什麼就提供什麼,不需要的就不要提供。接口中的方法應該儘量少,不要使接口過於臃腫,不要有很多不相關的邏輯方法。
  • 多用組合(has-a),少用繼承(is-a)。如果新對象的某些功能在別的已經創建好的對象裡面已經實現,那麼應當儘量使用別的對象提供的功能,使之成為新對象的一部分,而不要再重新創建。可以降低類與類之間的耦合程度。
  • 開閉原則。對修改關閉,對擴展開放。在軟件的生命週期內,因為變化,升級和維護等原因需要對軟件原有代碼進行修改,可能會給舊代碼引入錯誤,也有可能會使我們不得不對整個功能進行重構,並且需要原有代碼經過重新測試。解決方案:當軟件需要變化時,儘量通過擴展軟件實體的行為來實現變化,而不是通過修改已有的代碼來實現。不過這要求,我們要對需求的變更有前瞻性和預見性。其實只要遵循前面5種設計模式,設計出來的軟件就是符合開閉原則的。
    對象創建過程一個對象在可以被使用之前必須要被正確地實例化。在Java代碼中,有很多行為可以引起對象的創建,最為直觀的一種就是使用new關鍵字來調用一個類的構造函數顯式地創建對象,這種方式在Java規範中被稱為:由執行類實例創建表達式而引起的對象創建。除此之外,我們還可以使用反射機制(Class類的newInstance方法、使用Constructor類的newInstance方法)、使用Clone方法、使用反序列化等方式創建對象。

當一個對象被創建時,虛擬機就會為其分配內存來存放對象自己的實例變量及其從父類繼承過來的實例變量(即使這些從超類繼承過來的實例變量有可能被隱藏也會被分配空間)。在為這些實例變量分配內存的同時,這些實例變量也會被賦予默認值(零值)。在內存分配完成之後,Java虛擬機就會開始對新創建的對象按照開發人員的意志進行初始化。

<code>類加載檢查-->分配內存-->初始化零值-->設置對象頭-->執行init方法
/<code>
抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

策略模式 不同策略怎麼轉化?

策略模式是一種比較簡單的模式,他的定義是:定義一組算法,將每個算法都封裝起來,並且使他們之間可以互換。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

Context封裝角色,也叫做上下文角色,起承上啟下封裝作用,屏蔽高層模塊對策略、算法的直接訪問,封裝可能存在的變化。

Spring AOP 如何實現及應用?

基於代理思想,對原來目標對象,創建代理對象,在不修改原對象代碼情況下,通過代理對象,調用增強功能的代碼,從而對原有業務方法進行增強 !Spring中AOP的有兩種實現方式:JDK動態代理以及Cglib動態代理。

使用場景: 記錄日誌、監控方法運行時間 (監控性能)、權限控制、緩存優化 (第一次調用查詢數據庫,將查詢結果放入內存對象, 第二次調用, 直接從內存對象返回,不需要查詢數據庫 )、事務管理 (調用方法前開啟事務, 調用方法後提交關閉事務 )

你項目中如何捕獲業務異常以及記錄日誌的?

AOP 思想,Spring 統一異常處理有 3 種方式,分別為:

  • 使用 @ ExceptionHandler 註解
  • 實現 HandlerExceptionResolver 接口
  • 使用 @controlleradvice 註解

編譯時異常和運行時異常RuntimeException,前者通過捕獲異常從而獲取異常信息,後者主要通過規範代碼開發、測試通過手段減少運行時異常的發生。在開發中,不管是dao層、service層還是controller層,都有可能拋出異常,在springmvc中,能將所有類型的異常處理從各處理過程解耦出來,既保證了相關處理過程的功能較單一,也實現了異常信息的統一處理和維護。

java 枚舉類型是否可以繼承 (final)?

enum類是無法被繼承的,編譯器會自動把枚舉用繼承enum類來表示,但這一過程是由編譯器完成的,枚舉也不過是個語法糖。

如果一個類的實例是有限且確定的,那麼可以使用枚舉類。比如:季節類,只有春夏秋冬四個實例。

enum類默認被final修飾的情況下,是無法有子類的。enum本身不存在final、abstract的說法。就是不能被繼承。運行時生成的class才有final、abstract的說法。

註解是否可以繼承?

我們知道在編寫自定義註解時,可以通過指定@Inherited註解,指明自定義註解是否可以被繼承。但實現情況又可細分為多種。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

@Inherited 只可控制 對類名上註解是否可以被繼承。不能控制方法上的註解是否可以被繼承。

java 內存結構

JAVA內存結構:堆、棧、方法區;堆:存放所有 new出來的東西(堆空間是所有線程共享,虛擬機氣動的時候建立);棧:存放局部變量(線程創建的時候 被創建);方法區:被虛擬機加載的類信息、常量、靜態常量等。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

程序計數器

程序計數器可以看作是當前線程所執行的字節碼的行號指示器,是一塊線程隔離的內存空間。在虛擬機的概念模型中,字節碼解釋器通過改變程序計數器的值來選取下一條執行的字節碼命令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要計數器完成。每個線程都有獨立的程序計數器內存空間,它們之間相互隔離、互不影響。當線程上下文進行切換時,線程獨佔的程序計數器也會被加載。

當線程在執行Java方法時,計數器中記錄的是正在執行的虛擬機字節碼指令的地址;如果執行的是Native方法,計數器的值為空。程序計數器在Java虛擬機規範中沒有規定如何的OutOfMemoryError情況的區域。

Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等。每一個方法從調用到執行完成的過程,對應著一個棧幀從虛擬機棧中入棧到出棧的過程。

局部變量表中存放了編譯期可知的各種基本的數據類型,對象引用和returnAddress類型(指向了一條字節碼指令的地址)。局部變量表所需的內存空間在編譯期間完成分配,方法運行期間不會改變局部變量表的大小。

當線程請求的棧深度大於虛擬機允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,如果擴展時無法申請到足夠的內存,將會拋出OutOfMemoryError異常。

本地方法棧

本地方法棧描述虛擬機使用到的Native方法執行的內存模型,其作用與Java虛擬機棧類似,同樣可能拋出StackOverflowError和OutOfMemoryError異常。

Java堆

Java堆是所有線程共享的一塊內存區域,在虛擬機啟動時創建。Java堆作為運行時數據區域,存放著所有的類實例和數組,這是Java虛擬機規範中的規定。但是JIT編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換等優化技術使得所有對象都在堆上分配變得不那麼絕對。

Java堆是垃圾收集器管理的主要區域。從內存回收的角度來講,現在的收集器基本都是採取分代收集算法,所以Java堆可以細分為新生代和老年代,再細緻一點新生代中有Eden空間、From Survivor空間、 To Survivor空間等。從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。

進一步劃分的是為了更好地回收內存或者更快地分配內存。

如果在Java堆中沒有內存完成實例分配,並且堆無法再擴展,將會拋出OutOfMemoryError異常。

方法區

方法區作為所有線程共享的內存區域,存儲了被虛擬機加載的類信息、常量、靜態變量、及時編譯器編譯後的代碼等數據。

在HotSpot 1.8之前,HotSpot通過永久代的方式實現了方法區,GC分代收集的方式擴展到了方法區,減少了專門管理方法區內存管理代碼的編寫。在HotSpot 1.8中,方法區通過元數據區實現,永久代被廢棄,在1.7時字符串常量池已經被遷移到堆空間中。

方法區中的內存回收目標主要是針對常量池的回收和對類型的卸載。

當方法區無法滿足內存分配需求時,將會拋出OutOfMemoryError異常。

運行時常量池

運行時常量池作為方法區的一部分存在。Class文件中的常量池用於存放編譯期間生成的各種字面量和符號引用,在類加載後進入方法區的運行時常量池中存放。

運行時常量池相對於Class文件常量池的另一個重要特徵是具備動態性,即在運行時也可以將新的常量放入池中(String#intern方法)。

直接內存

直接內存並不是虛擬機運行時數據區的一部分。在JDK 1.4中新加入的NIO類,引入了一種基於通道與緩衝區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內存的引用進行操作,這樣能夠在一些場景中避免Java堆和Native堆中來回複製數據,提高性能。

一般來講,本機直接內存的分配不會收到Java堆大小的限制,但是總會受到本機物理內存以及尋址空間的限制。如果各個內存區域的總和大於物理內存限制,容易導致動態擴展時出現OutOfMemoryError異常。

注意不要回答成內存模型!

jvm參數 為什麼要配置?

堆空間主要組成部分:

  • 新生代(new generation),新生代又劃分為3部分:edenFrom Survivor(s0區域)To Survivor(s1區域)
    其中s0和s1區域大小相等
  • 老年代(tenured generation)

new出來的對象都會存放在堆內存中。新生代和老年代的存在主要用於垃圾回收機制,其中主要針對的是新生代,因為對象首先分配在eden區,在新生代回收後,如果對象還存活,則進入s0或s1區,之後每經過一次新生代回收,如果對象存活則它的年齡就加1,對象達到一定的年齡後,則進入老年代。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

在JVM啟動參數中,可以設置跟內存、垃圾回收相關的一些參數設置,默認情況不做任何設置JVM會工作的很好,但對一些配置很好的Server和具體的應用必須仔細調優才能獲得最佳性能。通過設置我們希望達到一些目標:

  • GC的時間足夠的小
  • GC的次數足夠的少
  • 發生Full GC(新生代和老年代)的週期足夠的長

要想GC時間小必須要一個更小的堆,要保證GC次數足夠少,必須保證一個更大的堆,我們只能取其平衡。

  • 針對JVM堆的設置,一般可以通過-Xms -Xmx限定其最小、最大值,為了防止垃圾收集器在最小、最大之間收縮堆而產生額外的時間,我們通常把最大、最小設置為相同的值
  • 年輕代和年老代將根據默認的比例(1:2)分配堆內存,可以通過調節二者的比例為1:3或者1:4,當然也可以設置新生代的大小,原則上為堆空間的1/3或者1/4。

8G內存的機器 java進程最大配置多少?

增大堆內存(-Xms,-Xmx)會減少可創建的線程數量,增大線程棧內存(-Xss,32位系統中此參數值最小為60K)也會減少可創建的線程數量。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

操作系統限制,系統最大可開線程數,主要受以下幾個參數影響:

  • /proc/sys/kernel/thread-max:系統可以生成的最大線程數量
  • /proc/sys/kernel/pid_max:並不是threads-max,就能創建越多的線程,發現線程數量在達到一定數量以後不再增長。創建的線程數還受到系統可創建的最大pid數影響,默認值為32768。
  • max_user_process: 64位Linux系統,這個參數還是會限制線程數量,可通過ulimit –a查看。
  • /proc/sys/vm/max_map_count:包含限制一個進程可以擁有的VMA(虛擬內存區域)的數量。

平衡樹的種類

平衡樹是一顆二叉搜索樹,並且它的深度保持相對穩定,也就是不會退化成鏈的樹。平衡樹可以說是區間操作的數據結構中效率高的一種,它最大的用處自然是維護區間了。細分為:splay、有旋/無旋treap、AVL樹、替罪羊樹、二叉查找樹(SBT)樹等。

跳錶和平衡樹區別

Redis sortedset 使用的是跳錶。跳錶是一種可以替代平衡樹的數據結構。跳錶追求的是概率性平衡,而不是嚴格平衡。因此,跟平衡二叉樹相比,跳錶的插入和刪除操作要簡單得多,執行也更快。

二叉樹可以用來實現字典和有序表等抽象數據結構。在元素隨機插入的場景,二叉樹可以很好應對。然而,在有序插入的情況下,二叉樹就退化了(鏈表),性能非常差。如果有辦法對待插入元素進行隨機排列,二叉樹大概率可以運行良好。大部分情況下,插入是在線進行的,因此隨機排列並不具有可行性。平衡樹在操作時對樹結構進行調整以滿足平衡條件,因此獲得理想性能。

跳錶是一種概率性可行的平衡二叉樹替代數據結構。跳錶通過一個隨機數生成器實現平衡。雖然跳錶最壞情況下(worst-case)性能也很差,但是沒有任何輸入序列必然會導致最壞情況發生(這點類似劃分元素(pivot point)隨機選定的快排)。跳錶極度不平衡發生的概率非常低(一個包含250個元素的字典,一次查找需要花3倍期望時間的概率小於百萬分之一)。跳錶平衡概率跟隨機插入的二叉樹差不多,好處是插入順序不要求隨機。

實現概率性平衡比嚴格控制平衡要簡單得多。對很多應用來說,跳錶用起來比平衡樹更自然,而且算法更簡單。跳錶算法簡單性意味著更容易實現,而且與平衡樹和自適應樹相比有常數倍數的性能提升。跳錶在空間上也比較高效。平均每個元素只需要額外耗費個2指針(甚至可以配置得更低),並不需要在每個節點上都存與平衡和優先級相關的數據。

volatile,內存重排序到底怎麼避免的?

Volatile 變量具有 synchronized 的可見性特性,但是不具備原子特性。

一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。

在單線程程序中,對存在控制依賴的操作重排序,不會改變執行結果;但在多線程程序中,對存在控制依賴的操作重排序,可能會改變程序的執行結果。這是就需要內存屏障來保證可見性了。

內存屏障分為兩種:Load Barrier 和 Store Barrier即讀屏障和寫屏障。

  • 對於Load Barrier來說,在指令前插入Load Barrier,可以讓高速緩存中的數據失效,強制從新從主內存加載數據;
  • 對於Store Barrier來說,在指令後插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見。

volatile的內存屏障策略非常嚴格保守,非常悲觀且毫無安全感的心態:

  • 在每個volatile寫操作前插入StoreStore屏障,在寫操作後插入StoreLoad屏障;
  • 在每個volatile讀操作前插入LoadLoad屏障,在讀操作後插入LoadStore屏障;

由於內存屏障的作用,避免了volatile變量和其它指令重排序、線程之間實現了通信,使得volatile表現出了鎖的特性。

一個線程在內存中如何存儲

從線程和進程的角度來說,進程是資源分配的最小單位,線程是獨立調度的最小單位。

同一個進程中的多個線程之間可以併發執行,他們共享進程資源。

線程不擁有資源,線程可以訪問隸屬進程的資源,進程有自己的獨立空間地址,線程沒有自己的獨立空間地址,但是線程有自己的堆棧和局部變量。

線程的棧、程序計數器、本地方法區也是存放在進程的地址空間上,只是這些棧、程序計數器、本地方法區都只能有某個特定的線程去訪問、其他的線程訪問不到。如果使用C/C++語言的話,數組越界後,很容易就訪問到其他線程的棧了,以致有可能導致其他線程的異常。

線程池,堵塞隊列為什麼要用堵塞?

多線程環境中,通過隊列可以很容易實現數據共享,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的數據共享。假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把準備好的數據共享給消費者線程,利用隊列的方式來傳遞數據,就可以很方便地解決他們之間的數據共享問題。但如果生產者和消費者在某個時間段內,萬一發生數據處理速度不匹配的情況呢?理想情況下,如果生產者產出數據的速度大於消費者消費的速度,並且當生產出來的數據累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的數據處理完畢,反之亦然。然而,在concurrent包發佈以前,在多線程環境下,我們每個程序員都必須去自己控制這些細節,尤其還要兼顧效率和線程安全,而這會給我們的程序帶來不小的複雜度。好在此時,強大的concurrent包橫空出世了,而他也給我們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)。

中間件

Tomcat框架的 selevet

Tomcat是目前市場上主流Web服務器之一,是用Java語言開發的項目。Tomcat支持Servlet和JSP的規範,它由一組嵌套的層次和組件組成。所有組件都實現lifecycle生命週期方法,裡面包含了init、start、stop、destroy等方法,用來控制生命週期。

Servlet是用Java編寫的Server端程序,它與協議和平臺無關。Servlet運行於Java-enabled Web Server中。Java Servlet可以動態地擴展Server的能力,並採用請求-響應模式提供Web服務。最早支持Servlet技術的是JavaSoft的Java Web Server。此後,一些其它的基於Java的Web Server開始支持標準的Servlet API。Servlet的主要功能在於交互式地瀏覽和修改數據,生成動態Web內容。

抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

Tomcat服務器本質是通過ServerSocket與客戶端進行通信,要進行通信首先就要進行TCP連接,Tomcat有兩個核心組件,Connecter和Container,Connecter將在某個指定的端口上偵聽客戶請求,接收瀏覽器的發過來的 tcp 連接請求,創建一個 Request 和 Response 對象分別用於和請求端交換數據,Request包含了用戶的請求信息,Response負責記錄了服務器的答覆內容。然後會產生一個線程來處理這個請求並把產生的 Request 和 Response 對象傳給Container處理。

Connector 最重要的功能就是接收連接請求然後分配線程讓 Container 來處理這個請求,所以這必然是多線程的,多線程的處理是 Connector 設計的核心。

當Connector處理完後會調用Container的invoke()方法,你可以想象Container容器裡有一條管道,管道上有很多閥門,每個閥門都會根據request進行一些操作,request和response請求會依次經過這些閥門,而Servlet就是該管道的最後一道閥門,之前的閥門就是filter。

Tomcat容器也分有上下層級關係如下圖,Tomcat的四層容器不都是必須的,一般簡單的容器只有Context和Wrapper兩層,Contenxt負責管理多個Wrapper,負責將映射轉發到對應Wrapper,當然期間還要經過filter過濾。Wrapper是最低層的容器,它只包裹著一個Servlet,Wrapper負責加載並管理調用Servlet服務。

mysql B+ B- B區別?

B樹,即二叉搜索樹,有如下特點:

  • 所有非葉子節點至多擁有兩個兒子(Leaf和Right)
  • 左右結點存儲一個關鍵字
  • 非葉子節點的左指針指向小於其關鍵字的子樹,右指針指向大於其關鍵字的子樹

B樹的搜索,從根結點開始,如果查詢的關鍵字與結點的關鍵字相等,那麼就命中;否則,如果查詢關鍵字比結點關鍵字小,就進入左兒子;如果比結點關鍵字大,就進入右兒子;如果左兒子或右兒子的指針為空,則報告找不到相應的關鍵字;如果B樹的所有非葉子結點的左右子樹的結點數目均保持差不多(平衡),那麼B樹的搜索性能逼近二分查找;但它比連續內存空間的二分查找的優點是,改變B樹結構(插入與刪除結點)不需要移動大段的內存數據,甚至通常是常數開銷。

B-樹,是一種多路搜索樹(並不是二叉的):

B樹和B-樹是同一種樹,只不過英語中B-tree被中國人翻譯成了B-樹,讓人以為B樹和B-樹是兩種樹,實際上,兩者就是同一種樹。此處單從算法的角度進行了劃分,區別於 B+ 樹,可以參見:

https://en.wikipedia.org/wiki/Binary_search_tree。

  • 關鍵字集合分佈在整顆樹中
  • 任何一個關鍵字出現且只出現在一個結點中
  • 搜素有可能在非葉子節點結束
  • 其搜索性能等價於在關鍵字全集內做一次二分查找
  • 自動層次控制

由於限制了除根結點以外的非葉子結點,至少含有M/2個兒子,確保了結點的至少利用率。所以B-樹的性能總是等價於二分查找(與M值無關),也就沒有B樹平衡的問題,由於 M/2的限制,在插入結點時,如果結點已滿,需要將結點分裂為兩個各佔 M/2 的結點,刪除結點時,需將兩個不足M/2的兄弟節點合併。B-樹的搜索,從根結點開始,對結點內的關鍵字(有序)序列進行二分查找,如果

命中則結束,否則進入查詢關鍵字所屬範圍的兒子結點;重複,直到所對應的兒子指針為空,或已經是葉子結點。

B+樹,B+樹是B-樹的變體,也是一種多路搜索樹,其定義基本與B-樹同,除了:

  • 非葉子結點的子樹指針與關鍵字個數相同;
  • 非葉子結點的子樹指針P[i],指向關鍵字值屬於[K[i], K[i+1])的子樹(B-樹是開區間);
  • 為所有葉子結點增加一個鏈指針;
  • 所有關鍵字都在葉子結點出現

B+的搜索與B-樹也基本相同,區別是B+樹只有達到葉子結點才命中(B-樹可以在非葉子結點命中),其性能也等價於在關鍵字全集做一次二分查找;非葉子結點相當於是葉子結點的索引(稀疏索引),葉子結點相當於是存儲(關鍵字)數據的數據層;更適合文件索引系統。

mysql 隔離級別?

數據庫事務是數據庫管理系統執行過程中的一個邏輯單位,由一個有限的數據庫操作序列構成。事務擁有四個重要的特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)、持久性(Durability),人們習慣稱之為 ACID 特性。

SQL 標準定義的四種隔離級別:

  • READ UNCOMMITTED(讀未提交):該隔離級別的事務會讀到其它未提交事務的數據,此現象也稱之為髒讀。
  • READ COMMITTED(讀提交):
    一個事務可以讀取另一個已提交的事務,多次讀取會造成不一樣的結果,此現象稱為不可重複讀問題,Oracle 和 SQL Server 的默認隔離級別。
  • REPEATABLE READ(可重複讀):
    該隔離級別是 MySQL 默認的隔離級別,在同一個事務裡,select 的結果是事務開始時時間點的狀態,因此,同樣的 select 操作讀到的結果會是一致的,但是,會有幻讀現象。MySQL 的 InnoDB 引擎可以通過 next-key locks 機制來避免幻讀。
  • SERIALIZABLE(序列化):
    在該隔離級別下事務都是串行順序執行的,MySQL 數據庫的 InnoDB 引擎會給讀操作隱式加一把讀共享鎖,從而避免了髒讀、不可重讀復讀和幻讀問題。
抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試

MVCC如何保證可重複讀?

MySQL的innodb引擎是如何實現MVCC的。innodb會為每一行添加兩個字段,分別表示該行創建的版本和刪除的版本,填入的是事務的版本號,這個版本號隨著事務的創建不斷遞增。在repeated read的隔離級別下,具體各種數據庫操作的實現:

事務開始,第一次不加鎖SELECT時,InnoDB從全局事務鏈表中,篩選所有活動事務(事務trx_id嚴格遞增),生成當前一致性視圖。

根據當前一致性視圖高低水位,計算事務可見性。

根據可見事務redo log,逆向算出歷史版本。SELECT快照讀,讀之前版本數據。SELECT FOR UPDATE 或 UPDATE 當前讀,加行鎖讀當前值,不會創建一致性視圖,有其它事務更新時,等待其它事務提交。(2PL更新時加寫鎖,事務提交時才會釋放)

間隙鎖怎麼使用?

InnoDB 行級鎖是通過給索引上的索引項加鎖來實現的,InnoDB行級鎖只有通過索引條件檢索數據,才使用行級鎖;否則,InnoDB使用表鎖。

在不通過索引(主鍵)條件查詢的時候,InnoDB是表鎖而不是行鎖。也就是說,在沒有使用索引的情況下,使用的就是表鎖。

間隙鎖可以理解為是對於一定範圍內的數據進行鎖定,如果說這個區間沒有這條數據的話也是會鎖住的;主要是解決幻讀的問題,如果沒有添加間隙鎖。如果其他事務中添加 id 在 1 到 100 之間的某條記錄,此時會發生幻讀;另一方面,視為了滿足其恢復和賦值的需求(幻讀的概念在上面有提到)。

默認情況下,innodb_locks_unsafe_for_binlog是0(禁用),這意味著啟用了間隙鎖定:InnoDB使用下一個鍵鎖進行搜索和索引掃描。若要啟用該變量,請將其設置為1。這將導致禁用間隙鎖定:InnoDB只使用索引記錄鎖進行搜索和索引掃描。

innodb自動使用間隙鎖的條件:

  • 必須在RR級別下
  • 檢索條件必須有索引(沒有索引的話,mysql會全表掃描,那樣會鎖定整張表所有的記錄,包括不存在的記錄,此時其他事務不能修改不能刪除不能添加)

間隙鎖的目的是為了防止幻讀,其主要通過兩個方面實現這個目的:

  • 防止間隙內有新數據被插入
  • 防止已存在的數據,更新成間隙內的數據(例如防止 number=3 的記錄通過update變成 number=5)

mysql hash索引使用場景?

hash索引的特點:

  • hash索引是基於hash表實現的,只有查詢條件精確匹配hash索引中的所有列的時候,才能用到hash索引。
  • 對於hash索引中的所有列,存儲引擎都會為每一行計算一個hash碼,hash索引中存儲的就是hash碼。
  • hash索引包括鍵值、hash碼和指針 。

在MySQL的存儲引擎中,MyISAM 不支持哈希索引,而 InnoDB 中的hash索引是存儲引擎根據B-Tree索引自建的。因為hash索引本身只需要存儲對應的hash值,所以索引的結構十分緊湊,這也讓hash索引查找的速度非常快。然而,哈希索引也有限制,如下:

  • 哈希索引只包含哈希值和行指針,而不存儲字段值,所以不能使用索引中的值來避免讀取行(即不能使用哈希索引來做覆蓋索引掃描),不過,訪問內存中的行的速度很快(因為memory引擎的數據都保存在內存裡),所以大部分情況下這一點對性能的影響並不明顯。
  • 哈希索引數據並不是按照索引列的值順序存儲的,所以也就無法用於排序
    哈希索引也不支持部分索引列匹配查找,因為哈希索引始終是使用索引的全部列值內容來計算哈希值的。如:數據列(a,b)上建立哈希索引,如果只查詢數據列a,則無法使用該索引。
  • 哈希索引只支持等值比較查詢,如:=,in(),<=>(注意,<>和<=>是不同的操作),不支持任何範圍查詢(必須給定具體的where條件值來計算hash值,所以不支持範圍查詢)。
  • 訪問哈希索引的數據非常快,除非有很多哈希衝突,當出現哈希衝突的時候,存儲引擎必須遍歷鏈表中所有的行指針,逐行進行比較,直到找到所有符合條件的行。
  • 如果哈希衝突很多的話,一些索引維護操作的代價也很高,如:如果在某個選擇性很低的列上建立哈希索引(即很多重複值的列),那麼當從表中刪除一行時,存儲引擎需要遍歷對應哈希值的鏈表中的每一行,找到並刪除對應的引用,衝突越多,代價越大。

redis 為什麼快?

完全基於內存,絕大多數請求是純粹的內存操作,非常快速。數據存儲在內存中,類似於HashMap,具備較快的查找和操作的時間複雜度O(1)。

數據結構簡單,對數據操作也簡單,Redis中的數據結構是專門進行設計的。

採用單線程,避免了不必要的上下文切換和競爭條件,也不存在多進程或者多線程導致的切換而消耗CPU,不用考慮各種鎖的問題,不存在加鎖釋放鎖(上下文的切換),沒有因為可能出現死鎖而導致的性能消耗。

使用多路I/O複用模型,非阻塞IO。

使用底層模型不同,它們之間底層實現方式以及與客戶端之間的通信的應用協議不一樣,Redis直接自己構建了VM機制,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求(用戶態和內核態之間的切換)。

未完待續

本來本文的標題是 抖音、騰訊、阿里、美團春招服務端開發崗位硬核面試(下) ,然題目比較多,限於篇幅,只能改成 二 ,下篇我們繼續。

另外幫忙插播一個內推崗位,有興趣的同學可以投簡歷或者後臺和我私聊。

阿里雲智能事業群-ECS資深Java工程師/專家-北京/杭州

大量HC,歡迎來聊。

  • 團隊組合:一群有情有義有夢想的工程師和雲計算行業技術大牛
  • 產品是啥:全球第三中國第一的公有云服務平臺,負責阿里雲 ECS 的資源管理、售賣、資源調度、資源供給服務,構建全球計算力的基礎設施

崗位職責:

  1. 參與建設大規模的資源調度系統,承載每天百萬次的 ECS 調度決策,為每臺 ECS 選擇最佳資源供給
  2. 運用數據挖掘、數據分析和智能算法,構建用戶畫像與資源畫像,預測未來各個區域不同產品的購買行為和趨勢,為ECS資源供給提供最佳決策,打造雲計算的彈性能力與性價比,實現成本與售賣的雙贏
  3. 構建資源管理系統,從宏觀的物理機管理,到微觀的虛擬資源管理,讓每一份資源物盡其用
  4. 參與基於 ECS 的產品研發,打造後端能力的新玩法,實現技術能力的變現,物盡其用,讓技術產生更多社會紅利價值

職位要求:

  1. 至少熟悉Java/Python語言的一種或多種,理解該語言涉及的基礎框架,對您使用過的框架能夠了解到它的原理和機制
  2. 熟悉linux操作系統、常用工具和命令,熟悉mysql數據庫
  3. 熟練掌握多線程等高併發系統編程和優化技能;熟悉分佈式系統的設計和應用,熟悉分佈式、緩存、消息等機制;能對分佈式常用技術進行合理應用,解決問題
  4. 具備快速學習能力,較強的團隊溝通和協作能力,較強的自我驅動能力
  5. 熟悉OpenStack/Kubernetes/Mesos/Borg等平臺經驗者優先,有大規模調度系統、資源管理系統的實際建設經驗者優先


分享到:


相關文章: