前言
2023-9-12 12:12:04
2023-09-14 16:13:04
公開發布于
2024-5-22 00:16:21
以下內容源自《【面試準備】》
僅供學習交流使用
版權
禁止其他平臺發布時刪除以下此話
本文首次發布于CSDN平臺
作者是CSDN@日星月云
博客主頁是https://blog.csdn.net/qq_51625007
禁止其他平臺發布時刪除以上此話
面試框架
Spring
SpringMVC
深入理解SpringMVC工作原理,像大牛一樣手寫SpringMVC框架
SpringBoot
[SpringBoot源碼分析]SpringBoot是如何啟動的
MySQL 數據庫
Redis 中間件
一文搞懂redis
ElasticSearch 搜索框架
「掃盲」Elasticsearch
Kafka 消息隊列
Kafka 科普
Kafka 架構、核心機制和場景解讀
Kafka 是一款非常優秀的開源消息引擎,以消息吞吐量高、可動態擴容、可持久化存儲、高可用的特性,以及完善的文檔和社區支持成為目前最流行的消息隊列中間件。
1.整體架構
一個典型的 Kafka 體系架構包括若干 Producer、若干 Broker、若干 Consumer,以及一個 ZooKeeper 集群,如圖所示。其中 ZooKeeper 是 Kafka 用來負責集群元數據的管理、控制器 的選舉等操作的。Producer 將消息發送到 Broker,Broker 負責將收到的消息存儲到磁盤中,而 Consumer 負責從 Broker 訂閱并消費消息。
整個 Kafka 體系結構中引入了以下 3 個術語。
- Producer:生產者,也就是發送消息的一方。生產者負責創建消息,然后將其投遞到 Kafka 中。
- Consumer:消費者,也就是接收消息的一方。消費者連接到 Kafka 上并接收消息,進 而進行相應的業務邏輯處理。
- Broker:服務代理節點。對于 Kafka 而言,Broker 可以簡單地看作一個獨立的 Kafka 服務節點或 Kafka 服務實例。大多數情況下也可以將 Broker 看作一臺 Kafka 服務器,前提是這臺服務器上只部署了一個 Kafka 實例。一個或多個 Broker 組成了一個 Kafka 集群。一般而言, 我們更習慣使用首字母小寫的 broker 來表示服務代理節點。
2.Kafka基礎概念
在 Kafka 中還有兩個特別重要的概念——主題(Topic)與分區(Partition)。Kafka 中的消息以 topic 題為單位進行歸類,生產者負責將消息發送到特定的 topic (發送到 Kafka 集群中的每一條消息都要指定一個主題),而消費者負責訂閱主題并進行消費。
主題是一個邏輯上的概念,它還可以細分為多個分區,一個分區只屬于單個主題,很多時候也會把分區稱為主題分區(Topic-Partition)。同一主題下的不同分區包含的消息是不同的,分區在存儲層面可以看作一個可追加的日志(Log)文件,消息在被追加到分區日志文件的時候都會分配一個特定的偏移量(offset)。offset 是消息在分區中的唯一標識,Kafka 通過它來保證消息在分區內的順序性,不過 offset 并不跨越分區,也就是說,Kafka 保證的是分區有序而不是主題有序。
如上圖(左)所示,主題中有 3 個分區,消息被順序追加到每個分區日志文件的尾部。Kafka 中的分區可以分布在不同的服務器(broker)上,也就是說,一個主題可以橫跨多個 broker,以此來提供比單個 broker 更強大的性能。
每一條消息被發送到 broker 之前,會根據分區規則選擇被存儲到哪個具體的分區。如果分區規則設定得合理,所有的消息都可以均勻地分配在不同的分區中。如果一個主題只對應一個文件,那么這個文件所在的機器 I/O 將會成為這個主題的性能瓶頸,而分區解決了這個問題。 在創建主題的時候可以通過指定的參數來設置分區的個數,當然也可以在主題創建完成之后去修改分區的數量,通過增加分區的數量可以實現水平擴展。
3.消費者與消費組
在 Kafka 的消費理念中還有一層消費組(Consumer Group))的概念,每個消費者都有一個對應的消費組。當消息發布到主題后,只會被投遞給訂閱它的每個消費組中的一個消費者。
如上圖所示,某個主題中共有 4 個分區(Partition): P0、P1、P2、P3。有兩個消費組 A 和 B 都訂閱了這個主題,消費組 A 中有 4 個消費者(C0、C1、C2 和 C3),消費組 B 中有 2 個消費者(C4 和 C5)。按照 Kafka 默認的規則,最后的分配結果是消費組 A 中的每一個消費者分配到 1 個分區,消費組 B 中的每一個消費者分配到 2 個分區,兩個消費組之間互不影響。 每個消費者只能消費所分配到的分區中的消息。換言之,每一個分區只能被一個消費組中的一 個消費者所消費。
我們再來看一下消費組內的消費者個數變化時所對應的分區分配的演變。假設目前某消費組內只有一個消費者 C0,訂閱了一個主題,這個主題包含 7 個分區:P0、P1、P2、P3、P4、 P5、P6。也就是說,這個消費者 C0 訂閱了 7 個分區,具體分配情形參考下圖(左上)。
此時消費組內又加入了一個新的消費者 C1,按照既定的邏輯,需要將原來消費者 C0 的部 分分區分配給消費者 C1 消費,如上圖(右上) 所示。消費者 C0 和 C1 各自負責消費所分配到的分區, 彼此之間并無邏輯上的干擾。
緊接著消費組內又加入了一個新的消費者 C2,消費者 C0、C1 和 C2 按照上圖(左下)中的方式 各自負責消費所分配到的分區。
消費者與消費組這種模型可以讓整體的消費能力具備橫向伸縮性,我們可以增加(或減少) 消費者的個數來提高(或降低)整體的消費能力。對于分區數固定的情況,一味地增加消費者 并不會讓消費能力一直得到提升,如果消費者過多,出現了消費者的個數大于分區個數的情況, 就會有消費者分配不到任何分區。參考上圖(右下),一共有 8 個消費者,7 個分區,那么最后的消費者 C7 由于分配不到任何分區而無法消費任何消息。
消費者分區分配策略:RangeAssignor、RoundRobinAssignor、StickyAssignor(0.11)。
消費者分區分配策略可以自定義實現,比如自定義實現上圖的“組內廣播”。
4.存儲視圖
主題和分區都是提供給上層用戶的抽象,而在副本層面或更加確切地說是 Log 層面才有實際物理上的存在。同一個分區中的多個副本必須分布在不同的 broker 中,這樣才能提供有效的數據冗余。
為了防止 Log 過大, Kafka 又引入了日志分段(LogSegment)的概念,將 Log 切分為多個 LogSegment,相當于一個巨型文件被平均分配為多個相對較小的文件,這樣也便于消息的維護和清理。事實上,Log 和 LogSegment 也不是純粹物理意義上的概念,Log 在物理上只以文件夾的形式存儲,而每個 LogSegment 對應于磁盤上的一個日志文件和兩個索引文件,以及可能的其他文件(比如以 “.txnindex”為后綴的事務索引文件) 。
為了便于消息的檢索,每個 LogSegment 中的日志文件(以“.log”為文件后綴)都有對應的兩個索引文件:偏移量索引文件(以“.index”為文件后綴)和時間戳索引文件(以“.timeindex” 為文件后綴)。每個 LogSegment 都有一個基準偏移量 baseOffset,用來表示當前 LogSegment 中第一條消息的 offset。偏移量是一個 64 位的長整型數,日志文件和兩個索引文件都是根據基 準偏移量(baseOffset)命名的,名稱固定為 20 位數字,沒有達到的位數則用 0 填充。比如第一個 LogSegment 的基準偏移量為 0,對應的日志文件為 00000000000000000000.log。
5.多副本
Kafka 為分區引入了多副本(Replica)機制,通過增加副本數量可以提升容災能力。同一分區的不同副本中保存的是相同的消息(在同一時刻,副本之間并非完全一樣),副本之間是一主多從的關系,其中 leader 副本負責處理讀寫請求,follower 副本只負責與 leader 副本的消息同步。副本處于不同的 broker 中,當 leader 副本出現故障時,從 follower 副本中重新選舉新的 leader 副本對外提供服務。Kafka 通過多副本機制實現了故障的自動轉移,當 Kafka 集群中某個 broker 失效時仍然能保證服務可用。
如上圖所示,Kafka 集群中有 4 個 broker,某個主題中有 3 個分區,且副本因子(即副本個數)也為 3,如此每個分區便有 1 個 leader 副本和 2 個 follower 副本。生產者和消費者只與 leader 副本進行交互,而 follower 副本只負責消息的同步,很多時候 follower 副本中的消息相對 leader 副本而言會有一定的滯后。
分區中的所有副本統稱為 AR (Assigned Replicas) 。所有與 leader 副本保持一定程度同步的副本(包括 leader 副本在內)組成 ISR (In-Sync Replicas) ,ISR 集合是 AR 集合中的一個子 集。消息會先發送到 leader 副本,然后 follower 副本才能從 leader 副本中拉取消息進行同步, 同步期間內 follower 副本相對于 leader 副本而言會有一定程度的滯后。
前面所說的“一定程度的同步”是指可忍受的滯后范圍,這個范圍可以通過參數進行配置。與 leader 副本同步滯后過多的副本(不包括 leader 副本)組成 OSR (Out-of-Sync Replicas) ,由此可見,AR =ISR+OSR。 在正常情況下,所有的 follower 副本都應該與 leader 副本保持一定程度的同步,即 AR=ISR, OSR 集合為空。
leader 副本負責維護和跟蹤 ISR 集合中所有 follower 副本的滯后狀態,當 follower 副本落后太多或失效時,leader 副本會把它從 ISR 集合中剔除。如果 OSR 集合中有 follower 副本“追上” 了 leader 副本,那么 leader 副本會把它從 OSR 集合轉移至 ISR 集合。默認情況下,當 leader 副 本發生故障時,只有在 ISR 集合中的副本才有資格被選舉為新的 leader,而在 OSR 集合中的副 本則沒有任何機會(不過這個原則也可以通過修改相應的參數配置來改變)。
ISR 與 HW 和 LEO 也有緊密的關系。HW 是 High Watermark 的縮寫,俗稱高水位,它標識 了一個特定的消息偏移量(offset),消費者只能拉取到這個 offset 之前的消息。
如上圖所示,它代表一個日志文件,這個日志文件中有 9 條消息,第一條消息的offset
(LogStartOffset)為 0,最后一條消息的 offset 為 8,offset 為 9 的消息用虛線框表示,代表下一條待寫入的消息。日志文件的 HW 為 6,表示消費者只能拉取到 offset 在 0 至 5 之間的消息, 而 offset 為 6 的消息對消費者而言是不可見的。
LEO 是 Log End Offset 的縮寫,它標識當前日志文件中下一條待寫入消息的 offset,上圖中 offset 為 9 的位置即為當前日志文件的 LEO,LEO 的大小相當于當前日志分區中最后一條消息的 offset 值加 1。分區 ISR 集合中的每個副本都會維護自身的 LEO,而 ISR 集合中最小的 LEO 即為分區的 HW,對消費者而言只能消費 HW 之前的消息。
注意要點:很多資料中會將上圖中的 offset 為 5 的位置看作 HW,而把 offset 為 8 的位置 看作 LEO,這顯然是不對的。
性能機制
零拷貝
Kafka使用了 Zero Copy 技術提升了消費的效率。(sendfile 系統調用在 Linux 內核版本 2.1 中被引入,目的是簡化通過網絡在兩個通道之間進行的數據傳輸過程。sendfile 允許操作系統從 Page Cache 直接將文件發送至 socket 緩存區,節省了內核空間 & 用戶空間的兩次冗余的 cpu 拷貝操作,最后只需要通過 DMA 傳輸將數據復制到網卡。這樣使得調用變得更加簡潔:不僅減少了 CPU 拷貝的次數,由于文件傳輸拷貝僅發生在內核空間,還減少了上下文切換的次數。)
RocketMQ 消息隊列
事務消息
生產者事務和消費者事務的最終一致性
Guava Cache 本地緩存
淺析本地緩存技術-Guava Cache | 京東物流技術團隊
3 guava cache的使用方式
Cache<String, String> localCache = CacheBuilder.newBuilder().initialCapacity(5).maximumSize(10).concurrencyLevel(3).expireAfterWrite(10, TimeUnit.SECONDS).build();
以上示例實例化了一個本地緩存,接下來介紹一下初始化的各項參數的含義:
initialCapacity:內部哈希表的最小容量,也就是cache的初始容量。
maximumSize:cache的最大緩存數。
concurrencyLevel:并發等級,也可以定義為同時操作緩存的線程數,由
int getConcurnencyLevel() { return this.concurrencyLevel = -1 ? 4 : this.concurrencyLevel; }
可以看出,這個線程數默認為4。
expireAfterWrite:緩存寫入后刷新時間。
從緩存中獲取數據調用的方法為get(K key, Callable<? extends V> loader)方法,此方法的含義是根據鍵key獲取數據,若key不存在,則通過執行指定的Callable方法來構造緩存,示例代碼如下所示:
Map<String, Dicdetail> dicDetailMap = ObLocateCache.locateConfigCache.get(key.toString(), new Callable<Map<String, Dicdetail>>() {@Overridepublic Map<String, Dicdetail> call() throws Exception {return getConfigParameterFromMaster(baseDomain, KeyConstants.WMS5_LOCATE_MANUAL);}});
從cache中刪除數據分為被動刪除和主動刪除兩種:
1.被動刪除:
基于數據大小刪除:LRU+FIFO
基于過期時間刪除:在指定時間內沒有被訪問
基于引用刪除:通過weakKeys和weakValues方法指定Cache只保存對緩存記錄key和value的弱引用。這樣當沒有其他強引用指向key和value時,key和value對象就會被垃圾回收器回收
2.主動刪除:
//刪除指定的key對應數據
cache.invalidate("s");
//將一批對應的數據刪除
cache.invalidateAll(Arrays.asList("st","r","ing"));
//全部刪除
cache.invalidateAll();
guava cache的數據結構跟ConcurrentHashMap類似,二者最基本的區別是ConcurrentMap會一直保存所有添加的元素,直至將添加的元素移除。相對地,guava cache為了限制內存占用,通常都設定為自動回收元素。
guava cache的核心類為LocalCache,LocalCache實現了ConcurrentMap接口。其中有一個Segment數組,如下所示
Caffine 本地緩存
常見本地緩存、分布式緩存解決方案詳解
Quartz 定時框架
SpringBoot——Quartz定時框架的使用詳解和總結
特點
- 支持分布式高可用,我們需要某個定時任務在多個節點中只有某個節點可以執行時,就需要Quartz來實現,否則使用@Scheduled等方式會造成所有節點都執行一遍。
- 支持持久化,Quartz有專門的數據表來實現定時任務的持久化。
- 支持多任務調度和管理,Quartz可以在數據庫中存儲多個定時任務進行作業調度,可以實現定時任務的增刪改查等管理。
組成
Quartz由三部分組成:
- 任務:JobDetail
- 觸發器:Trigger(分為SimpleTrigger和CronTrigger)
- 調度器:Scheduler
JobDetail
Trigger
Scheduler
Cron表達式
RateLimiter 限流器
限速神器RateLimiter源碼解析 | 京東云技術團隊
使用DCL
常用的限流算法
常用的限流算法有如下幾種:
- 算法一、信號量算法。 維護最大的并發請求數(如連接數),當并發請求數達到閾值時報錯或等待,如線程池。
- 算法二、漏桶算法。 模擬一個按固定速率漏出的桶,當流入的請求量大于桶的容量時溢出。
- 算法三、令牌桶算法。 以固定速率向桶內發放令牌。請求處理時,先從桶里獲取令牌,只服務有令牌的請求。
本次要介紹的RateLimiter使用的是令牌桶算法。RateLimiter是google的guava包中的一個輕巧限流組件,它主要有兩個java類文件,RateLimiter.java和SmoothRateLimiter.java。兩個類文件共有java代碼301行、注釋420行,注釋比java代碼還要多,寫的非常詳細,后面的介紹也有相關內容是翻譯自其注釋,有些描述英文原版更加準確清晰,有興趣的也可以結合原版注釋進行更詳細的了解。
算法介紹
在介紹之前,先說一下RateLimiter中的幾個名詞:
- 許可( permit ): 代表一個令牌,獲取到許可的請求才能放行。
- 資源利用不足( underunilization ): 許可的發放一般是勻速的,但請求未必是勻速的,有時會有無請求(資源利用不足)的場景,令牌桶會有貯存機制。
- 貯存許可( storedPermit ): 令牌桶支持對空閑資源進行許可貯存,許可請求時優先使用貯存許可。
- 新鮮許可( freshPermit ): 當貯存許可為空時,采用透支方式,下發新鮮許可,同時設置下次許可生效時間為本次新鮮許可的結束時間。
如下為一個許可發放示例,矩形代表整個令牌桶,許可產生速度為1個/秒,令牌桶里有一個貯存桶,容量為2。
以上示例中,在T1貯存容量為0,許可請求時直接返回1個新鮮許可,貯存容量隨著時間推移,增長至最大值2,在T2時收到3個許可的請求,此時會先從貯存桶中取出2個,然后再產生1個新鮮許可,0.5s后在T3時刻又來了1個許可請求,由于最近的許可0.5s后才會下發,因此先sleep0.5s再下發。
RateLimiter的核心功能是限速,我們首先想到的限速方案是記住最后一次下發令牌許可(permit)時間,下次許可請求時,如果與最后一次下發許可時間的間隔小于1/QPS,則進行sleep至1/QPS,否則直接發放,但該方法不能感知到資源利用不足的場景。一方面,隔了很長一段再來請求許可,則可能系統此時相對空閑,可下發更多的許可以充分利用資源;另一方面,隔了很長一段時間再來請求許可,也可能意味著處理請求的資源變冷(如緩存失效),處理效率會下降。因此在RateLimiter中,增加了資源利用不足(underutilization)的管理,在代碼中體現為貯存許可(storedPermits),貯存許可值最開始為0,隨著時間的增加,一直增長為最大貯存許可數。許可獲取時,首先從貯存許可中獲取,然后再根據下次新鮮許可獲取時間來進行新鮮許可獲取。這里要說的是RateLimiter是記住了下次令牌發放的時間,類似于透支的功能,當前許可獲取時立刻返回,同時記錄下次獲取許可的時間。
TreadPool 線程池
Java—線程池ThreadPoolExecutor詳解
要求:線程資源必須通過線程池提供,不允許在應用自行顯式創建線程;
說明:使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗內存或者“過度切換”的問題。
by 《阿里巴巴Java手冊》
七大參數:
int corePoolSize:該線程池中核心線程數最大值
int maximumPoolSize:線程總數最大值
long keepAliveTime:非核心線程空閑超時時間
TimeUnit unit:是keepAliveTime 的單位
BlockingQueue workQueue:存放任務的阻塞隊列
ThreadFactory threadFactory:創建線程的方式
RejectedExecutionHandler handler:飽和策略
BlockingQueue workQueue:存放任務的阻塞隊列
當核心線程都在工作的時候,新提交的任務就會被添加到這個工作阻塞隊列中進行排隊等待;如果阻塞隊列也滿了,線程池就新建非核心線程去執行任務。workQueue維護的是等待執行的Runnable對象。
常用的 workQueue 類型:(無界隊列、有界隊列、同步移交隊列)
- SynchronousQueue:同步移交隊列,適用于非常大的或者無界的線程池,可以避免任務排隊,SynchronousQueue隊列接收到任務后,會直接將任務從生產者移交給工作者線程,這種移交機制高效。它是一種不存儲元素的隊列,任務不會先放到隊列中去等線程來取,而是直接移交給執行的線程。只有當線程池是無界的或可以拒絕任務的時候,SynchronousQueue隊列的使用才有意義,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即無限大。要將一個元素放入SynchronousQueue,就需要有另一個線程在等待接收這個元素。若沒有線程在等待,并且線程池的當前線程數小于最大值,則ThreadPoolExecutor就會新建一個線程;否則,根據飽和策略,拒絕任務。newCachedThreadPool默認使用的就是這種同步移交隊列。吞吐量高于LinkedBlockingQueue。
- LinkedBlockingQueue:基于鏈表結構的阻塞隊列,FIFO原則排序。當任務提交過來,若當前線程數小于corePoolSize核心線程數,則線程池新建核心線程去執行任務;若當前線程數等于corePoolSize核心線程數,則進入工作隊列進行等待。LinkedBlockingQueue隊列沒有最大值限制,只要任務數超過核心線程數,都會被添加到隊列中,這就會導致總線程數永遠不會超過 corePoolSize,所以maximumPoolSize 是一個無效設定。newFixedThreadPool和newSingleThreadPool默認是使用的是無界LinkedBlockingQueue隊列。吞吐量高于ArrayBlockingQueue。
- ArrayBlockingQueue:基于數組結構的有界阻塞隊列,可以設置隊列上限值,FIFO原則排序。當任務提交時,若當前線程小于corePoolSize核心線程數,則新建核心線程執行任務;若當先線程數等于corePoolSize核心線程數,則進入隊列排隊等候;若隊列的任務數也排滿了,則新建非核心線程執行任務;若隊列滿了且總線程數達到了maximumPoolSize最大線程數,則根據飽和策略進行任務的拒絕。
- DelayQueue:延遲隊列,隊列內的元素必須實現 Delayed 接口。當任務提交時,入隊列后只有達到指定的延時時間,才會執行任務
- PriorityBlockingQueue:優先級阻塞隊列,根據優先級執行任務,優先級是通過自然排序或者是Comparator定義實現。
注意: 只有當任務相互獨立沒有任何依賴的時候,線程池或工作隊列設置有界是合理的;若任務之間存在依賴性,需要使用無界的線程池,如newCachedThreadPool,否則有可能會導致死鎖問題。
執行流程:
一般流程即為:創建worker線程;添加任務入workQueue隊列;worker線程執行任務。
當一個任務被添加進線程池時:
- 當前線程數量未達到 corePoolSize,則新建一個線程(核心線程)執行任務
- 當前線程數量達到了 corePoolSize,則將任務移入阻塞隊列等待,讓空閑線程處理;
- 當阻塞隊列已滿,新建線程(非核心線程)執行任務
- 當阻塞隊列已滿,總線程數又達到了 maximumPoolSize,就會按照拒絕策略處理無法執行的任務,比如RejectedExecutionHandler拋出異常。
四個線程池
newCacheThreadPool():可緩存線程池
newFixedThreadPool():定長線程池單線程的線程
newScheduledThreadPool():定時線程池
newSingleThreadExecutor():單線程化的線程池
newCachedThreadPool將創建一個可緩存的線程,如果當前線程數超過處理任務時,回收空閑線程;當需求增加時,可以添加新線程去處理任務。
- 線程數無限制,corePoolSize數值為0, maximumPoolSize 的數值都是為 Integer.MAX_VALUE。
- 若線程未回收,任務到達時,會復用空閑線程;若無空閑線程,則新建線程執行任務。
- 因為復用性,一定程序減少頻繁創建/銷毀線程,減少系統開銷。
- 工作隊列可以選用SynchronousQueue。
newFixedThreadPool創建一個固定長度的線程池,每次提交一個任務的時候就會創建一個新的線程,直到達到線程池的最大數量限制。
- 定長,可以控制線程最大并發數, corePoolSize 和 maximumPoolSize 的數值都是nThreads。
- 超出的線程會在隊列中等待。
- 工作隊列可以選用LinkedBlockingQueue。
newScheduledThreadPool創建一個固定長度的線程池,并且以延遲或者定時的方式去執行任務。
newSingleThreadExecutor顧名思義,是一個單線程的Executor,只創建一個工作線程執行任務,若這個唯一的線程異常故障了,會新建另一個線程來替代,newSingleThreadExecutor可以保證任務依照在工作隊列的排隊順序來串行執行。
- 有且僅有一個工作線程執行任務;
- 所有任務按照工作隊列的排隊順序執行,先進先出的順序。
- 單個線程的線程池就是線程池中只有一個線程負責任務,所以 corePoolSize 和 maximumPoolSize 的數值都是為 1;當這個線程出現任何異常后,線程池會自動創建一個線程,始終保持線程池中有且只有一個存活的線程。
- 工作隊列可以選用LinkedBlockingQueue。
四種拒絕策略
ThreadPoolExecutor的飽和策略可以通過調用setRejectedExecutionHandler來修改。JDK提供了幾種不同的RejectedExecutionHandler實現,每種實現都包含有不同的飽和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
拒絕策略如下:
- CallerRunsPolicy : 調用線程處理任務
- AbortPolicy : 拋出異常
- DiscardPolicy : 直接丟棄
- DiscardOldestPolicy : 丟棄隊列中最老的任務,執行新任務
最后
我們都有光明的未來
祝大家考研上岸
祝大家工作順利
祝大家得償所愿
祝大家如愿以償
點贊收藏關注哦