• 正文
    • Java
    • Java創(chuàng)建線程的方式有哪些?
    • 場景
  • 相關(guān)推薦
申請入駐 產(chǎn)業(yè)圖譜

攜程薪資開了,還算滿意,簽了!

01/05 11:25
1854
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

圖解學習網(wǎng)站:https://xiaolincoding.com

大家好,我是小林。昨天公布了互聯(lián)網(wǎng)公司 25 屆校招薪資,底部有讀者留言想看看攜程的薪資和面經(jīng)。

之前有同學剛參加完攜程的線下面試,流程是一二面+HR面,當天下午就直接速通了,速通之后就等后面的 offer 錄取通知了,如果是線上面試的話,有時候流程會需要走 2-3 周,整體還是比較慢,最快也需要一周,所以線下面試效率還是非常快的,快人一步拿 offer。

25 屆攜程開發(fā)崗位的校招薪資如下:

整體看,攜程的年薪是有 30-40w的,薪資待遇還是不錯的,跟一線大廠差不多了,訓(xùn)練營也有同學拿到了攜程 offer,薪資開了 sp offer,還算滿意,最后選擇去攜程。

那攜程面試到底難度如何呢?

那么,這次來分享一位同學攜程的Java 后端開發(fā)的面經(jīng),主要是考察了Java 集合、Java IO、Java 并發(fā)、SSM、場景題、系統(tǒng)設(shè)計方面的知識。一般來說,攜程面試還是會出算法的,不過這個同學當時是沒有手撕算法,面試時長大概 40 分鐘。

大家覺得難度如何呢?

Java

Java 中常用集合有哪些?

List是有序的Collection,使用此接口能夠精確的控制每個元素的插入位置,用戶能根據(jù)索引訪問List中元素。常用的實現(xiàn)List的類有LinkedList,ArrayList,Vector,Stack。

    ArrayList 是容量可變的非線程安全列表,其底層使用數(shù)組實現(xiàn)。當發(fā)生擴容時,會創(chuàng)建更大的數(shù)組,并把原數(shù)組復(fù)制到新數(shù)組。ArrayList支持對元素的快速隨機訪問,但插入與刪除速度很慢。LinkedList本質(zhì)是一個雙向鏈表,與ArrayList相比,,其插入和刪除速度更快,但隨機訪問速度更慢。Vector 與 ArrayList 類似,底層也是基于數(shù)組實現(xiàn),特點是線程安全,但效率相對較低,因為其方法大多被 synchronized 修飾

Map 是一個鍵值對集合,存儲鍵、值和之間的映射。Key 無序,唯一;value 不要求有序,允許重復(fù)。Map 沒有繼承于 Collection 接口,從 Map 集合中檢索元素時,只要給出鍵對象,就會返回對應(yīng)的值對象。主要實現(xiàn)有TreeMap、HashMap、HashTable、LinkedHashMap、ConcurrentHashMap

    HashMap:JDK1.8 之前 HashMap 由數(shù)組+鏈表組成的,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的(“拉鏈法”解決沖突),JDK1.8 以后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為 8)時,將鏈表轉(zhuǎn)化為紅黑樹,以減少搜索時間LinkedHashMap:LinkedHashMap 繼承自 HashMap,所以它的底層仍然是基于拉鏈式散列結(jié)構(gòu)即由數(shù)組和鏈表或紅黑樹組成。另外,LinkedHashMap 在上面結(jié)構(gòu)的基礎(chǔ)上,增加了一條雙向鏈表,使得上面的結(jié)構(gòu)可以保持鍵值對的插入順序。同時通過對鏈表進行相應(yīng)的操作,實現(xiàn)了訪問順序相關(guān)邏輯。HashTable:數(shù)組+鏈表組成的,數(shù)組是 HashMap 的主體,鏈表則是主要為了解決哈希沖突而存在的TreeMap:紅黑樹(自平衡的排序二叉樹)ConcurrentHashMap:Node數(shù)組+鏈表+紅黑樹實現(xiàn),線程安全的(jdk1.8以前Segment鎖,1.8以后volatile + CAS 或者 synchronized)

Set不允許存在重復(fù)的元素,與List不同,set中的元素是無序的。常用的實現(xiàn)有HashSet,LinkedHashSet和TreeSet。

    HashSet通過HashMap實現(xiàn),HashMap的Key即HashSet存儲的元素,所有Key都是用相同的Value,一個名為PRESENT的Object類型常量。使用Key保證元素唯一性,但不保證有序性。由于HashSet是HashMap實現(xiàn)的,因此線程不安全。LinkedHashSet繼承自HashSet,通過LinkedHashMap實現(xiàn),使用雙向鏈表維護元素插入順序。TreeSet通過TreeMap實現(xiàn)的,添加元素到集合時按照比較規(guī)則將其插入合適的位置,保證插入后的集合仍然有序。

HashMap 的實現(xiàn)原理?

在 JDK 1.7 版本之前, HashMap 數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數(shù)組中的槽位(Bucket)。如果多個鍵映射到同一個槽位,它們會以鏈表的形式存儲在同一個槽位上,因為鏈表的查詢時間是O(n),所以沖突很嚴重,一個索引上的鏈表非常長,效率就很低了。

所以在 JDK 1.8 版本的時候做了優(yōu)化,當一個鏈表的長度超過8的時候就轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu),不再使用鏈表存儲,而是使用紅黑樹,查找時使用紅黑樹,時間復(fù)雜度O(log n),可以提高查詢性能,但是在數(shù)量較少時,即數(shù)量小于6時,會將紅黑樹轉(zhuǎn)換回鏈表。

HashSet 的實現(xiàn)原理及使用原理?

HashSet 實現(xiàn)原理:

數(shù)據(jù)結(jié)構(gòu)實現(xiàn)原理

    • :HashSet 是基于哈希表實現(xiàn)的,HashSet 內(nèi)部使用一個 HashMap 來存儲元素。實際上,HashSet 可以看作是對 HashMap 的簡單封裝,它只使用了 HashMap 的鍵(Key)來存儲元素,而值(Value)部分被忽略(在 Java 的 HashMap 實現(xiàn)中,所有 HashSet 中的元素對應(yīng)的 Value 是一個固定的 Object 對象,通常是一個名為 PRESENT 的靜態(tài)常量)。

元素存儲過程

    • :當向 HashSet 中添加一個元素時,首先會計算該元素的哈希碼,然后通過哈希函數(shù)得到桶的索引。接著檢查該桶中是否已經(jīng)存在元素。如果桶為空,則直接將元素插入到該桶中;如果桶不為空,則遍歷桶中的鏈表(或紅黑樹),比較元素的哈希碼和 equals 方法(在 Java 中,判斷兩個元素是否相等,先比較哈希碼是否相同,若相同再比較 equals 方法是否返回 true)。如果沒有找到相同的元素(即哈希碼和 equals 方法都不匹配),則將元素添加到鏈表(或紅黑樹)中;如果找到相同的元素,則認為該元素已經(jīng)存在于 HashSet 中,不會重復(fù)添加。

元素查找過程

    :查找一個元素是否在 HashSet 中也是類似的過程。先計算元素的哈希碼,然后通過哈希函數(shù)得到桶的索引。接著在對應(yīng)的桶中查找元素,通過比較哈希碼和 equals 方法來判斷元素是否存在。由于哈希函數(shù)能夠快速定位到元素可能存在的桶,所以在理想情況下,HashSet 的查找操作時間復(fù)雜度可以接近常數(shù)時間 O (1),但在最壞情況下(所有元素都哈希到同一個桶),時間復(fù)雜度會退化為 O (n),其中 n 是 HashSet 中的元素個數(shù)。

HashSet 使用原理:

添加元素

    • :使用 add 方法可以將元素添加到 HashSet 中。例如,在 Java 中,HashSet<String> set = new HashSet<>(); set.add("example");
    • 就將字符串 “example” 添加到了 HashSet 中。

檢查元素是否存在

    • :使用 contains 方法來檢查一個元素是否存在于 HashSet 中。例如,set.contains("example")
    • 會返回 true,因為剛剛添加了這個元素。

刪除元素

    • :通過 remove 方法刪除元素。如set.remove("example");會將剛剛添加的元素從 HashSet 中刪除。

ArrayList 和 LinkedList 有什么區(qū)別?

ArrayList和LinkedList都是Java中常見的集合類,它們都實現(xiàn)了List接口。

底層數(shù)據(jù)結(jié)構(gòu)不同:ArrayList使用數(shù)組實現(xiàn),通過索引進行快速訪問元素。LinkedList使用鏈表實現(xiàn),通過節(jié)點之間的指針進行元素的訪問和操作。

插入和刪除操作的效率不同:ArrayList在尾部的插入和刪除操作效率較高,但在中間或開頭的插入和刪除操作效率較低,需要移動元素。LinkedList在任意位置的插入和刪除操作效率都比較高,因為只需要調(diào)整節(jié)點之間的指針。

隨機訪問的效率不同:ArrayList支持通過索引進行快速隨機訪問,時間復(fù)雜度為O(1)。LinkedList需要從頭或尾開始遍歷鏈表,時間復(fù)雜度為O(n)。

空間占用:ArrayList在創(chuàng)建時需要分配一段連續(xù)的內(nèi)存空間,因此會占用較大的空間。LinkedList每個節(jié)點只需要存儲元素和指針,因此相對較小。

使用場景:ArrayList適用于頻繁隨機訪問和尾部的插入刪除操作,而LinkedList適用于頻繁的中間插入刪除操作和不需要隨機訪問的場景。

線程安全:這兩個集合都不是線程安全的,Vector是線程安全的

雙親委派策略是什么?

雙親委派模型是 Java 類加載器的一種層次化加載策略。在這種策略下,當一個類加載器收到類加載請求時,它首先不會自己去嘗試加載這個類,而是把這個請求委派給它的父類加載器。只有當父類加載器無法完成加載任務(wù)時,才由自己來加載。

在 Java 中,類加載器主要有以下幾種,并且存在層次關(guān)系。

啟動類加載器(Bootstrap ClassLoader)

    • :它是最頂層的類加載器,主要負責加載 Java 的核心類庫,例如存放在<JAVA_HOME>/lib
    • 目錄下的rt.jar
    • 等核心庫。它是由 C++ 編寫的,是虛擬機的一部分,沒有對應(yīng)的 Java 類,在 Java 代碼中無法直接引用它。

擴展類加載器(Extension ClassLoader)

    • :它的父加載器是啟動類加載器。主要負責加載<JAVA_HOME>/lib/ext
    • 目錄下的類庫或者由java.ext.dirs
    • 系統(tǒng)屬性指定路徑中的類庫。它是由 Java 編寫的,對應(yīng)的 Java 類是sun.misc.Launcher$ExtClassLoader。

應(yīng)用程序類加載器(Application ClassLoader)

    • :也稱為系統(tǒng)類加載器,它的父加載器是擴展類加載器。主要負責加載用戶類路徑(classpath
    • )上的類庫,這是我們在日常編程中最常接觸到的類加載器,對應(yīng)的 Java 類是sun.misc.Launcher$AppClassLoader。

自定義類加載器(Custom Class Loader)

    :開發(fā)者可以根據(jù)需求定制類的加載方式,比如從網(wǎng)絡(luò)加載class文件、數(shù)據(jù)庫、甚至是加密的文件中加載類等。自定義類加載器可以用來擴展Java應(yīng)用程序的靈活性和安全性,是Java動態(tài)性的一個重要體現(xiàn)。

當一個類加載請求到達應(yīng)用程序類加載器時,它會先把請求委派給它的父加載器(擴展類加載器)。擴展類加載器收到請求后,也會先委派給它的父加載器(啟動類加載器)。啟動類加載器會嘗試從自己負責的核心類庫中加載這個類,如果能加載成功,就返回加載的類;如果不能加載,就把請求返回給擴展類加載器。擴展類加載器再嘗試從自己負責的擴展類庫中加載,如果成功就返回,否則將請求返回給應(yīng)用程序類加載器。最后,應(yīng)用程序類加載器從自己負責的類路徑中加載這個類。

雙親委派模型優(yōu)勢是:

安全性

    • :通過雙親委派策略,保證了 Java 核心類庫的安全性。例如,java.lang.Object
    • 這個類是由啟動類加載器加載的。如果沒有這種策略,用戶可能會編寫一個自己的java.lang.Object
    • 類,并且通過自定義的類加載器加載,這會導(dǎo)致整個 Java 類型系統(tǒng)的混亂。而雙親委派策略使得像java.lang.Object
    • 這樣的核心類始終由啟動類加載器加載,防止了用戶代碼對核心類庫的惡意篡改。

避免類的重復(fù)加載

    :由于類加載請求是由上到下進行委派的,當一個類已經(jīng)被父類加載器加載后,子類加載器就不會再重復(fù)加載。例如,某個類在擴展類庫中已經(jīng)被加載,那么應(yīng)用程序類加載器就不會再次加載這個類,從而提高了加載效率,節(jié)省了內(nèi)存空間。

深拷貝和淺拷貝的區(qū)別?怎么實現(xiàn)?

    淺拷貝是指只復(fù)制對象本身和其內(nèi)部的值類型字段,但不會復(fù)制對象內(nèi)部的引用類型字段。換句話說,淺拷貝只是創(chuàng)建一個新的對象,然后將原對象的字段值復(fù)制到新對象中,但如果原對象內(nèi)部有引用類型的字段,只是將引用復(fù)制到新對象中,兩個對象指向的是同一個引用對象。深拷貝是指在復(fù)制對象的同時,將對象內(nèi)部的所有引用類型字段的內(nèi)容也復(fù)制一份,而不是共享引用。換句話說,深拷貝會遞歸復(fù)制對象內(nèi)部所有引用類型的字段,生成一個全新的對象以及其內(nèi)部的所有對象。

序列化和反序列化實現(xiàn)的是深拷貝還是淺拷貝?

Java創(chuàng)建線程的方式有哪些?

1.繼承Thread類

這是最直接的一種方式,用戶自定義類繼承java.lang.Thread類,重寫其run()方法,run()方法中定義了線程執(zhí)行的具體任務(wù)。創(chuàng)建該類的實例后,通過調(diào)用start()方法啟動線程。

class?MyThread?extends?Thread?{
????@Override
????public?void?run()?{
????????//?線程執(zhí)行的代碼
????}
}

public?static?void?main(String[]?args)?{
????MyThread?t?=?new?MyThread();
????t.start();
}

采用繼承Thread類方式

    優(yōu)點: 編寫簡單,如果需要訪問當前線程,無需使用Thread.currentThread ()方法,直接使用this,即可獲得當前線程缺點:因為線程類已經(jīng)繼承了Thread類,所以不能再繼承其他的父類

2.實現(xiàn)Runnable接口

如果一個類已經(jīng)繼承了其他類,就不能再繼承Thread類,此時可以實現(xiàn)java.lang.Runnable接口。實現(xiàn)Runnable接口需要重寫run()方法,然后將此Runnable對象作為參數(shù)傳遞給Thread類的構(gòu)造器,創(chuàng)建Thread對象后調(diào)用其start()方法啟動線程。

class?MyRunnable?implements?Runnable?{
????@Override
????public?void?run()?{
????????//?線程執(zhí)行的代碼
????}
}

public?static?void?main(String[]?args)?{
????Thread?t?=?new?Thread(new?MyRunnable());
????t.start();
}

采用實現(xiàn)Runnable接口方式:

    優(yōu)點:線程類只是實現(xiàn)了Runable接口,還可以繼承其他的類。在這種方式下,可以多個線程共享同一個目標對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU代碼和數(shù)據(jù)分開,形成清晰的模型,較好地體現(xiàn)了面向?qū)ο蟮乃枷?。缺點:編程稍微復(fù)雜,如果需要訪問當前線程,必須使用Thread.currentThread()方法。

3. 實現(xiàn)Callable接口與FutureTask

java.util.concurrent.Callable接口類似于Runnable,但Callable的call()方法可以有返回值并且可以拋出異常。要執(zhí)行Callable任務(wù),需將它包裝進一個FutureTask,因為Thread類的構(gòu)造器只接受Runnable參數(shù),而FutureTask實現(xiàn)了Runnable接口。

class?MyCallable?implements?Callable<Integer>?{
????@Override
????public?Integer?call()?throws?Exception?{
????????//?線程執(zhí)行的代碼,這里返回一個整型結(jié)果
????????return?1;
????}
}

public?static?void?main(String[]?args)?{
????MyCallable?task?=?new?MyCallable();
????FutureTask<Integer>?futureTask?=?new?FutureTask<>(task);
????Thread?t?=?new?Thread(futureTask);
????t.start();

????try?{
????????Integer?result?=?futureTask.get();??//?獲取線程執(zhí)行結(jié)果
????????System.out.println("Result:?"?+?result);
????}?catch?(InterruptedException?|?ExecutionException?e)?{
????????e.printStackTrace();
????}
}

采用實現(xiàn)Callable接口方式:

    • 缺點:編程稍微復(fù)雜,如果需要訪問當前線程,必須調(diào)用

Thread.currentThread()

    方法。優(yōu)點:線程只是實現(xiàn)Runnable或?qū)崿F(xiàn)Callable接口,還可以繼承其他類。這種方式下,多個線程可以共享一個target對象,非常適合多線程處理同一份資源的情形。

4. 使用線程池(Executor框架)

從Java 5開始引入的java.util.concurrent.ExecutorService和相關(guān)類提供了線程池的支持,這是一種更高效的線程管理方式,避免了頻繁創(chuàng)建和銷毀線程的開銷??梢酝ㄟ^Executors類的靜態(tài)方法創(chuàng)建不同類型的線程池。

class?Task?implements?Runnable?{
????@Override
????public?void?run()?{
????????//?線程執(zhí)行的代碼
????}
}

public?static?void?main(String[]?args)?{
????ExecutorService?executor?=?Executors.newFixedThreadPool(10);??//?創(chuàng)建固定大小的線程池
????for?(int?i?=?0;?i?<?100;?i++)?{
????????executor.submit(new?Task());??//?提交任務(wù)到線程池執(zhí)行
????}
????executor.shutdown();??//?關(guān)閉線程池
}

采用線程池方式:

    缺點:程池增加了程序的復(fù)雜度,特別是當涉及線程池參數(shù)調(diào)整和故障排查時。錯誤的配置可能導(dǎo)致死鎖、資源耗盡等問題,這些問題的診斷和修復(fù)可能較為復(fù)雜。優(yōu)點:線程池可以重用預(yù)先創(chuàng)建的線程,避免了線程創(chuàng)建和銷毀的開銷,顯著提高了程序的性能。對于需要快速響應(yīng)的并發(fā)請求,線程池可以迅速提供線程來處理任務(wù),減少等待時間。并且,線程池能夠有效控制運行的線程數(shù)量,防止因創(chuàng)建過多線程導(dǎo)致的系統(tǒng)資源耗盡(如內(nèi)存溢出)。通過合理配置線程池大小,可以最大化CPU利用率和系統(tǒng)吞吐量。

線程池使用的時候應(yīng)該注意哪些問題?

線程池是為了減少頻繁的創(chuàng)建線程和銷毀線程帶來的性能損耗,線程池的工作原理如下圖:

線程池分為核心線程池,線程池的最大容量,還有等待任務(wù)的隊列,提交一個任務(wù),如果核心線程沒有滿,就創(chuàng)建一個線程,如果滿了,就是會加入等待隊列,如果等待隊列滿了,就會增加線程,如果達到最大線程數(shù)量,如果都達到最大線程數(shù)量,就會按照一些丟棄的策略進行處理。

線程池使用的時候應(yīng)該注意以下問題。

1.線程池大小的合理設(shè)置性

核心線程數(shù)(Core Pool Size)

    • :核心線程數(shù)是線程池在沒有任務(wù)時保持的線程數(shù)量。如果設(shè)置得太小,當有大量任務(wù)突然到達時,線程池可能無法及時處理,導(dǎo)致任務(wù)在隊列中等待時間過長。例如,對于一個 CPU 密集型的任務(wù),核心線程數(shù)一般可以設(shè)置為 CPU 核心數(shù)加 1,這樣可以充分利用 CPU 資源,同時避免過多的上下文切換。如果是 I/O 密集型任務(wù),由于線程大部分時間在等待 I/O 操作完成,核心線程數(shù)可以設(shè)置得相對大一些,通常可以根據(jù) I/O 設(shè)備的性能和任務(wù)的 I/O 等待時間來估算,比如可以設(shè)置為 CPU 核心數(shù)的兩倍。

最大線程數(shù)(Maximum Pool Size)

    • :最大線程數(shù)決定了線程池能夠同時處理任務(wù)的上限。如果設(shè)置得過大,可能會導(dǎo)致系統(tǒng)資源耗盡,如內(nèi)存不足或者 CPU 過度切換上下文而導(dǎo)致性能下降。設(shè)置最大線程數(shù)時需要考慮系統(tǒng)的資源限制,包括 CPU、內(nèi)存等。并且,最大線程數(shù)與任務(wù)隊列的大小也有關(guān)系,當任務(wù)隊列滿了之后,線程池會創(chuàng)建新的線程,直到達到最大線程數(shù)。

阻塞隊列(Blocking Queue)容量

    :阻塞隊列用于存儲等待執(zhí)行的任務(wù)。如果隊列容量設(shè)置得過小,可能無法容納足夠的任務(wù),導(dǎo)致任務(wù)被拒絕;而如果設(shè)置得過大,可能會導(dǎo)致任務(wù)在隊列中等待時間過長,增加響應(yīng)時間。對于有優(yōu)先級的任務(wù)隊列,還需要考慮如何合理地設(shè)置優(yōu)先級,以確保高優(yōu)先級的任務(wù)能夠及時得到處理。

2.線程池的生命周期管理

正確的啟動和關(guān)閉順序

    • :要確保線程池在正確的時機啟動和關(guān)閉。在啟動線程池后,才能提交任務(wù)給它執(zhí)行;在系統(tǒng)關(guān)閉或者不再需要線程池時,需要正確地關(guān)閉線程池。關(guān)閉線程池可以使用shutdown
    • 或者shutdownNow
    • 方法。

shutdown

    • 方法會等待正在執(zhí)行的任務(wù)完成后再關(guān)閉線程池,而shutdownNow
    • 方法會嘗試中斷正在執(zhí)行的任務(wù),并立即關(guān)閉線程池,返回尚未執(zhí)行的任務(wù)列表。

避免重復(fù)提交任務(wù)

    :在某些情況下,可能會出現(xiàn)重復(fù)提交任務(wù)的情況。比如在網(wǎng)絡(luò)不穩(wěn)定的情況下,客戶端可能會多次發(fā)送相同的請求,導(dǎo)致任務(wù)被多次提交到線程池。這可能會導(dǎo)致任務(wù)的重復(fù)執(zhí)行或者資源的浪費??梢酝ㄟ^在任務(wù)提交端進行去重處理,或者在任務(wù)本身的邏輯中設(shè)置標志位來判斷任務(wù)是否已經(jīng)在執(zhí)行,避免重復(fù)執(zhí)行。

3.線程安全性和資源管理

共享資源訪問控制

    • :線程池中的線程會并發(fā)地執(zhí)行任務(wù),如果任務(wù)涉及到共享資源的訪問,如共享變量、數(shù)據(jù)庫連接等,需要采取適當?shù)耐酱胧?,如使用synchronized
    • 關(guān)鍵字或者ReentrantLock
    • 等鎖機制,以避免數(shù)據(jù)不一致或者資源競爭的問題。

資源的釋放和清理

    • :線程執(zhí)行任務(wù)可能會占用各種資源,如文件句柄、網(wǎng)絡(luò)連接等。在任務(wù)執(zhí)行完成后,需要確保這些資源得到正確的釋放和清理,避免資源泄漏??梢栽谌蝿?wù)的run
    • 方法或者call
    方法的最后進行資源的清理工作,如關(guān)閉文件流、釋放數(shù)據(jù)庫連接等。

BIO、NIO、AIO 區(qū)別是什么?

    BIO(blocking IO):就是傳統(tǒng)的 java.io 包,它是基于流模型實現(xiàn)的,交互的方式是同步、阻塞方式,也就是說在讀入輸入流或者輸出流時,在讀寫動作完成之前,線程會一直阻塞在那里,它們之間的調(diào)用是可靠的線性順序。優(yōu)點是代碼比較簡單、直觀;缺點是 IO 的效率和擴展性很低,容易成為應(yīng)用性能瓶頸。NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以構(gòu)建多路復(fù)用的、同步非阻塞 IO 程序,同時提供了更接近操作系統(tǒng)底層高性能的數(shù)據(jù)操作方式。AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升級版本,提供了異步非堵塞的 IO 操作方式,所以人們叫它 AIO(Asynchronous IO),異步 IO 是基于事件和回調(diào)機制實現(xiàn)的,也就是應(yīng)用操作之后會直接返回,不會堵塞在那里,當后臺處理完成,操作系統(tǒng)會通知相應(yīng)的線程進行后續(xù)的操作。

三級緩存解決循環(huán)依賴方式是?

環(huán)依賴指的是兩個類中的屬性相互依賴對方:例如 A 類中有 B 屬性,B 類中有 A屬性,從而形成了一個依賴閉環(huán),如下圖。

循環(huán)依賴問題在Spring中主要有三種情況:

    第一種:通過構(gòu)造方法進行依賴注入時產(chǎn)生的循環(huán)依賴問題。第二種:通過setter方法進行依賴注入且是在多例(原型)模式下產(chǎn)生的循環(huán)依賴問題。第三種:通過setter方法進行依賴注入且是在單例模式下產(chǎn)生的循環(huán)依賴問題。

只有【第三種方式】的循環(huán)依賴問題被 Spring 解決了,其他兩種方式在遇到循環(huán)依賴問題時,Spring都會產(chǎn)生異常。

Spring 解決單例模式下的setter循環(huán)依賴問題的主要方式是通過三級緩存解決循環(huán)依賴。三級緩存指的是 Spring 在創(chuàng)建 Bean 的過程中,通過三級緩存來緩存正在創(chuàng)建的 Bean,以及已經(jīng)創(chuàng)建完成的 Bean 實例。具體步驟如下:

實例化 Bean:Spring 在實例化 Bean 時,會先創(chuàng)建一個空的 Bean 對象,并將其放入一級緩存中。

屬性賦值:Spring 開始對 Bean 進行屬性賦值,如果發(fā)現(xiàn)循環(huán)依賴,會將當前 Bean 對象提前暴露給后續(xù)需要依賴的 Bean(通過提前暴露的方式解決循環(huán)依賴)。

初始化 Bean:完成屬性賦值后,Spring 將 Bean 進行初始化,并將其放入二級緩存中。

注入依賴:Spring 繼續(xù)對 Bean 進行依賴注入,如果發(fā)現(xiàn)循環(huán)依賴,會從二級緩存中獲取已經(jīng)完成初始化的 Bean 實例。

通過三級緩存的機制,Spring 能夠在處理循環(huán)依賴時,確保及時暴露正在創(chuàng)建的 Bean 對象,并能夠正確地注入已經(jīng)初始化的 Bean 實例,從而解決循環(huán)依賴問題,保證應(yīng)用程序的正常運行。

Java 21 新特性知道哪些?

新新語言特性:

Switch 語句的模式匹配:該功能在 Java 21 中也得到了增強。它允許在switch

    • 的case
    • 標簽中使用模式匹配,使操作更加靈活和類型安全,減少了樣板代碼和潛在錯誤。例如,對于不同類型的賬戶類,可以在switch
      • 語句中直接根據(jù)賬戶類型的模式來獲取相應(yīng)的余額,如

    case savingsAccount sa -> result = sa.getSavings();數(shù)組模式

    • :將模式匹配擴展到數(shù)組中,使開發(fā)者能夠在條件語句中更高效地解構(gòu)和檢查數(shù)組內(nèi)容。例如,if (arr instanceof int[] {1, 2, 3})
    • ,可以直接判斷數(shù)組arr
    • 是否匹配指定的模式。

字符串模板(預(yù)覽版)

    • :提供了一種更可讀、更易維護的方式來構(gòu)建復(fù)雜字符串,支持在字符串字面量中直接嵌入表達式。例如,以前可能需要使用"hello " + name + ", welcome to the geeksforgeeks!"
    • 這樣的方式來拼接字符串,在 Java 21 中可以使用hello {name}, welcome to the geeksforgeeks!
    這種更簡潔的寫法

新并發(fā)特性方面:

虛擬線程:這是 Java 21 引入的一種輕量級并發(fā)的新選擇。它通過共享堆棧的方式,大大降低了內(nèi)存消耗,同時提高了應(yīng)用程序的吞吐量和響應(yīng)速度??梢允褂渺o態(tài)構(gòu)建方法、構(gòu)建器或ExecutorService來創(chuàng)建和使用虛擬線程。

Scoped Values(范圍值):提供了一種在線程間共享不可變數(shù)據(jù)的新方式,避免使用傳統(tǒng)的線程局部存儲,促進了更好的封裝性和線程安全,可用于在不通過方法參數(shù)傳遞的情況下,傳遞上下文信息,如用戶會話或配置設(shè)置。

SpringBoot 的核心注解有哪些?

Bean 相關(guān):

@Component:將一個類標識為 Spring 組件(Bean),可以被 Spring 容器自動檢測和注冊。通用注解,適用于任何層次的組件。

@ComponentScan:自動掃描指定包及其子包中的 Spring 組件。

@Controller:標識控制層組件,實際上是 @Component 的一個特化,用于表示 Web 控制器。處理 HTTP 請求并返回視圖或響應(yīng)數(shù)據(jù)。

@RestController:是 @Controller 和 @ResponseBody 的結(jié)合,返回的對象會自動序列化為 JSON 或 XML,并寫入 HTTP 響應(yīng)體中。

@Repository:標識持久層組件(DAO 層),實際上是 @Component 的一個特化,用于表示數(shù)據(jù)訪問組件。常用于與數(shù)據(jù)庫交互。

@Bean:方法注解,用于修飾方法,主要功能是將修飾方法的返回對象添加到 Spring 容器中,使得其他組件可以通過依賴注入的方式使用這個對象。

依賴注入:

@Autowired:用于自動注入依賴對象,Spring 框架提供的注解。

@Resource:按名稱自動注入依賴對象(也可以按類型,但默認按名稱),JDK 提供注解。

@Qualifier:與 @Autowired 一起使用,用于指定要注入的 Bean 的名稱。當存在多個相同類型的 Bean 時,可以使用 @Qualifier 來指定注入哪一個。

讀取配置:

@Value:用于注入屬性值,通常從配置文件中獲取。標注在字段上,并指定屬性值的來源(如配置文件中的某個屬性)。

@ConfigurationProperties:用于將配置屬性綁定到一個實體類上。通常用于從配置文件中讀取屬性值并綁定到類的字段上。

Web相關(guān):

@RequestMapping:用于映射 HTTP 請求到處理方法上,支持 GET、POST、PUT、DELETE 等請求方法??梢詷俗⒃陬惢蚍椒ㄉ?。標注在類上時,表示類中的所有響應(yīng)請求的方法都是以該類路徑為父路徑。

@GetMapping、@PostMapping、@PutMapping、@DeleteMapping:分別用于映射 HTTP GET、POST、PUT、DELETE 請求到處理方法上。它們是 @RequestMapping 的特化,分別對應(yīng)不同的 HTTP 請求方法。

其他常用注解:

@Transactional:聲明事務(wù)管理。標注在類或方法上,指定事務(wù)的傳播行為、隔離級別等。

@Scheduled:聲明一個方法需要定時執(zhí)行。標注在方法上,并指定定時執(zhí)行的規(guī)則(如每隔一定時間執(zhí)行一次)。

場景

高并發(fā)的場景下保證數(shù)據(jù)庫和緩存一致性?

對于讀數(shù)據(jù),我會選擇旁路緩存策略,如果 cache 不命中,會從 db 加載數(shù)據(jù)到 cache。對于寫數(shù)據(jù),我會選擇更新 db 后,再刪除緩存。

緩存是通過犧牲強一致性來提高性能的。這是由CAP理論決定的。緩存系統(tǒng)適用的場景就是非強一致性的場景,它屬于CAP中的AP。所以,如果需要數(shù)據(jù)庫和緩存數(shù)據(jù)保持強一致,就不適合使用緩存。

所以使用緩存提升性能,就是會有數(shù)據(jù)更新的延遲。這需要我們在設(shè)計時結(jié)合業(yè)務(wù)仔細思考是否適合用緩存。然后緩存一定要設(shè)置過期時間,這個時間太短、或者太長都不好:

    太短的話請求可能會比較多的落到數(shù)據(jù)庫上,這也意味著失去了緩存的優(yōu)勢。太長的話緩存中的臟數(shù)據(jù)會使系統(tǒng)長時間處于一個延遲的狀態(tài),而且系統(tǒng)中長時間沒有人訪問的數(shù)據(jù)一直存在內(nèi)存中不過期,浪費內(nèi)存。

但是,通過一些方案優(yōu)化處理,是可以最終一致性的。

針對刪除緩存異常的情況,可以使用 2 個方案避免:

    刪除緩存重試策略(消息隊列)訂閱 binlog,再刪除緩存(Canal+消息隊列)

消息隊列方案

我們可以引入消息隊列,將第二個操作(刪除緩存)要操作的數(shù)據(jù)加入到消息隊列,由消費者來操作數(shù)據(jù)。

      • 如果應(yīng)用

    刪除緩存失敗

      • ,可以從消息隊列中重新讀取數(shù)據(jù),然后再次刪除緩存,這個就是

    重試機制

      • 。當然,如果重試超過的一定次數(shù),還是沒有成功,我們就需要向業(yè)務(wù)層發(fā)送報錯信息了。如果

    刪除緩存成功

    ,就要把數(shù)據(jù)從消息隊列中移除,避免重復(fù)操作,否則就繼續(xù)重試。

舉個例子,來說明重試機制的過程。

重試刪除緩存機制還可以,就是會造成好多業(yè)務(wù)代碼入侵

訂閱 MySQL binlog,再操作緩存「先更新數(shù)據(jù)庫,再刪緩存」的策略的第一步是更新數(shù)據(jù)庫,那么更新數(shù)據(jù)庫成功,就會產(chǎn)生一條變更日志,記錄在 binlog 里。

于是我們就可以通過訂閱 binlog 日志,拿到具體要操作的數(shù)據(jù),然后再執(zhí)行緩存刪除,阿里巴巴開源的 Canal 中間件就是基于這個實現(xiàn)的。

Canal 模擬 MySQL 主從復(fù)制的交互協(xié)議,把自己偽裝成一個 MySQL 的從節(jié)點,向 MySQL 主節(jié)點發(fā)送 dump 請求,MySQL 收到請求后,就會開始推送 Binlog 給 Canal,Canal 解析 Binlog 字節(jié)流之后,轉(zhuǎn)換為便于讀取的結(jié)構(gòu)化數(shù)據(jù),供下游程序訂閱使用。

下圖是 Canal 的工作原理:

將binlog日志采集發(fā)送到MQ隊列里面,然后編寫一個簡單的緩存刪除消息者訂閱binlog日志,根據(jù)更新log刪除緩存,并且通過ACK機制確認處理這條更新log,保證數(shù)據(jù)緩存一致性

使用消息隊列還應(yīng)該注意哪些問題?

需要考慮消息可靠性和順序性方面的問題。

消息隊列的可靠性、順序性怎么保證?

消息可靠性可以通過下面這些方式來保證

消息持久化:確保消息隊列能夠持久化消息是非常關(guān)鍵的。在系統(tǒng)崩潰、重啟或者網(wǎng)絡(luò)故障等情況下,未處理的消息不應(yīng)丟失。例如,像 RabbitMQ 可以通過配置將消息持久化到磁盤,通過將隊列和消息都設(shè)置為持久化的方式(設(shè)置durable = true),這樣在服務(wù)器重啟后,消息依然可以被重新讀取和處理。

消息確認機制:消費者在成功處理消息后,應(yīng)該向消息隊列發(fā)送確認(acknowledgment)。消息隊列只有收到確認后,才會將消息從隊列中移除。如果沒有收到確認,消息隊列可能會在一定時間后重新發(fā)送消息給其他消費者或者再次發(fā)送給同一個消費者。以 Kafka 為例,消費者通過commitSync或者commitAsync方法來提交偏移量(offset),從而確認消息的消費。

消息重試策略:當消費者處理消息失敗時,需要有合理的重試策略??梢栽O(shè)置重試次數(shù)和重試間隔時間。例如,在第一次處理失敗后,等待一段時間(如 5 秒)后進行第二次重試,如果重試多次(如 3 次)后仍然失敗,可以將消息發(fā)送到死信隊列,以便后續(xù)人工排查或者采取其他特殊處理。

消息順序性保證的方式如下:

有序消息處理場景識別:首先需要明確業(yè)務(wù)場景中哪些消息是需要保證順序的。例如,在金融交易系統(tǒng)中,對于同用戶的轉(zhuǎn)賬操作順序是不能打亂的。對于需要順序處理的消息,要確保消息隊列和消費者能夠按照特定的順序進行處理。

消息隊列對順序性的支持:部分消息隊列本身提供了順序性保證的功能。比如 Kafka 可以通過將消息劃分到同一個分區(qū)(Partition)來保證消息在分區(qū)內(nèi)是有序的,消費者按照分區(qū)順序讀取消息就可以保證消息順序。但這也可能會限制消息的并行處理程度,需要在順序性和吞吐量之間進行權(quán)衡。

消費者順序處理策略:消費者在處理順序消息時,應(yīng)該避免并發(fā)處理可能導(dǎo)致順序打亂的情況。例如,可以通過單線程或者使用線程池并對順序消息進行串行化處理等方式,確保消息按照正確的順序被消費。

系統(tǒng)設(shè)計

秒殺系統(tǒng)設(shè)計如何做?

系統(tǒng)架構(gòu)分層設(shè)計如下。

前端層:

頁面靜態(tài)化:將商品展示頁面等靜態(tài)內(nèi)容進行緩存,用戶請求時可以直接從緩存中獲取,減少服務(wù)器的渲染壓力。例如,使用內(nèi)容分發(fā)網(wǎng)絡(luò)(CDN)緩存商品圖片、詳情介紹等靜態(tài)資源。

防刷機制:通過驗證碼、限制用戶請求頻率等方式防止惡意刷請求。例如,在秒殺開始前要求用戶輸入驗證碼,并且在一定時間內(nèi)限制單個用戶的請求次數(shù),如每秒最多允許 3 次請求。

應(yīng)用層

負載均衡:采用負載均衡器將用戶請求均勻地分配到多個后端服務(wù)器,避免單點服務(wù)器過載。如使用 Nginx 作為負載均衡器,根據(jù)服務(wù)器的負載情況和性能動態(tài)分配請求。

服務(wù)拆分與微服務(wù)化:將秒殺系統(tǒng)的不同功能模塊拆分成獨立的微服務(wù),如用戶服務(wù)、商品服務(wù)、訂單服務(wù)等。這樣可以獨立部署和擴展各個模塊,提高系統(tǒng)的靈活性和可維護性。

緩存策略:在應(yīng)用層使用緩存來提高系統(tǒng)性能。例如,使用 Redis 緩存商品庫存信息,用戶下單前先從 Redis 中查詢庫存,減少對數(shù)據(jù)庫的直接訪問。

數(shù)據(jù)層:

數(shù)據(jù)庫優(yōu)化:對數(shù)據(jù)庫進行性能優(yōu)化,如數(shù)據(jù)庫索引優(yōu)化、SQL 語句優(yōu)化等。對于庫存表,可以為庫存字段添加索引,加快庫存查詢和更新的速度。

數(shù)據(jù)庫集群與讀寫分離

    :采用數(shù)據(jù)庫集群來提高數(shù)據(jù)庫的處理能力,同時進行讀寫分離。將讀操作(如查詢商品信息)和寫操作(如庫存扣減、訂單生成)分布到不同的數(shù)據(jù)庫節(jié)點上,提高系統(tǒng)的并發(fā)處理能力。

高并發(fā)場景下扣減庫存的方式:

預(yù)扣庫存:在用戶下單時,先預(yù)扣庫存,將庫存數(shù)量在緩存(如 Redis)中進行減 1 操作。同時設(shè)置一個較短的過期時間,如 1 - 2 分鐘。如果用戶在過期時間內(nèi)完成支付,正式扣減庫存;如果未完成支付,庫存自動回補。

異步更新數(shù)據(jù)庫:通過 Redis 判斷之后,去更新數(shù)據(jù)庫的請求都是必要的請求,這些請求數(shù)據(jù)庫必須要處理,但是如果數(shù)據(jù)庫還是處理不過來這些請求怎么辦呢?這個時候就可以考慮削峰填谷操作了,削峰填谷最好的實踐就是 MQ 了。經(jīng)過 Redis 庫存扣減判斷之后,我們已經(jīng)確保這次請求需要生成訂單,我們就可以通過異步的形式通知訂單服務(wù)生成訂單并扣減庫存。

數(shù)據(jù)庫樂觀鎖防止超賣

    • :更新數(shù)據(jù)庫減庫存的時候,采用樂觀鎖方式,進行庫存限制條件,update goods set stock = stock - 1 where goods_id = ? and stock >0

相關(guān)推薦