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

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

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

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

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

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

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

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

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

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

大家覺得難度如何呢?

Java

Java 中常用集合有哪些?

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

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

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

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

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

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

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

在 JDK 1.7 版本之前, HashMap 數(shù)據(jù)結(jié)構(gòu)是數(shù)組和鏈表,HashMap通過哈希算法將元素的鍵(Key)映射到數(shù)組中的槽位(Bucket)。如果多個(gè)鍵映射到同一個(gè)槽位,它們會(huì)以鏈表的形式存儲(chǔ)在同一個(gè)槽位上,因?yàn)殒湵淼牟樵儠r(shí)間是O(n),所以沖突很嚴(yán)重,一個(gè)索引上的鏈表非常長(zhǎng),效率就很低了。

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

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

HashSet 實(shí)現(xiàn)原理:

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

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

元素存儲(chǔ)過程

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

元素查找過程

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

HashSet 使用原理:

添加元素

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

檢查元素是否存在

    • :使用 contains 方法來檢查一個(gè)元素是否存在于 HashSet 中。例如,set.contains("example")
    • 會(huì)返回 true,因?yàn)閯倓偺砑恿诉@個(gè)元素。

刪除元素

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

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

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

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

插入和刪除操作的效率不同:ArrayList在尾部的插入和刪除操作效率較高,但在中間或開頭的插入和刪除操作效率較低,需要移動(dòng)元素。LinkedList在任意位置的插入和刪除操作效率都比較高,因?yàn)橹恍枰{(diào)整節(jié)點(diǎn)之間的指針。

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

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

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

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

雙親委派策略是什么?

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

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

啟動(dòng)類加載器(Bootstrap ClassLoader)

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

擴(kuò)展類加載器(Extension ClassLoader)

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

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

    • :也稱為系統(tǒng)類加載器,它的父加載器是擴(kuò)展類加載器。主要負(fù)責(zé)加載用戶類路徑(classpath
    • )上的類庫,這是我們?cè)谌粘>幊讨凶畛=佑|到的類加載器,對(duì)應(yīng)的 Java 類是sun.misc.Launcher$AppClassLoader。

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

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

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

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

安全性

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

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

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

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

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

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

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

1.繼承Thread類

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

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

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

采用繼承Thread類方式

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

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

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

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

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

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

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

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

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

class?MyCallable?implements?Callable<Integer>?{
????@Override
????public?Integer?call()?throws?Exception?{
????????//?線程執(zhí)行的代碼,這里返回一個(gè)整型結(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();
????}
}

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

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

Thread.currentThread()

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

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)閉線程池
}

采用線程池方式:

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

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

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

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

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

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

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

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

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

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

阻塞隊(duì)列(Blocking Queue)容量

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

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

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

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

shutdown

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

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

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

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

共享資源訪問控制

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

資源的釋放和清理

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Java 21 新特性知道哪些?

新新語言特性:

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

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

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

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

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

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

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

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

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

SpringBoot 的核心注解有哪些?

Bean 相關(guān):

@Component:將一個(gè)類標(biāo)識(shí)為 Spring 組件(Bean),可以被 Spring 容器自動(dòng)檢測(cè)和注冊(cè)。通用注解,適用于任何層次的組件。

@ComponentScan:自動(dòng)掃描指定包及其子包中的 Spring 組件。

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

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

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

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

依賴注入:

@Autowired:用于自動(dòng)注入依賴對(duì)象,Spring 框架提供的注解。

@Resource:按名稱自動(dòng)注入依賴對(duì)象(也可以按類型,但默認(rèn)按名稱),JDK 提供注解。

@Qualifier:與 @Autowired 一起使用,用于指定要注入的 Bean 的名稱。當(dāng)存在多個(gè)相同類型的 Bean 時(shí),可以使用 @Qualifier 來指定注入哪一個(gè)。

讀取配置:

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

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

Web相關(guān):

@RequestMapping:用于映射 HTTP 請(qǐng)求到處理方法上,支持 GET、POST、PUT、DELETE 等請(qǐng)求方法??梢詷?biāo)注在類或方法上。標(biāo)注在類上時(shí),表示類中的所有響應(yīng)請(qǐng)求的方法都是以該類路徑為父路徑。

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

其他常用注解:

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

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

場(chǎng)景

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

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

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

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

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

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

針對(duì)刪除緩存異常的情況,可以使用 2 個(gè)方案避免:

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

消息隊(duì)列方案

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

      • 如果應(yīng)用

    刪除緩存失敗

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

    重試機(jī)制

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

    刪除緩存成功

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

舉個(gè)例子,來說明重試機(jī)制的過程。

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

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

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

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

下圖是 Canal 的工作原理:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

前端層:

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

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

應(yīng)用層

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

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

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

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

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

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

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

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

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

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

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

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

相關(guān)推薦