12條技巧,打造出超高性能的接口API
共 37436字,需瀏覽 75分鐘
·
2024-07-02 12:58
1. 并行處理
簡(jiǎn)要說(shuō)明
舉個(gè)例子:在價(jià)格查詢鏈路中,我們需要獲取多種獨(dú)立的價(jià)格配置項(xiàng)信息,如基礎(chǔ)價(jià)、折扣價(jià)、商戶活動(dòng)價(jià)、平臺(tái)活動(dòng)價(jià)等等。為了加快處理速度,可以使用多線程并行處理的方式,利用并發(fā)計(jì)算的優(yōu)勢(shì)。而 CompletableFuture 是一種流行的實(shí)現(xiàn)多線程的方式,它可以輕松地管理線程的創(chuàng)建、執(zhí)行和回調(diào),提高程序的可擴(kuò)展性和并發(fā)性。
然而,多線程的使用也存在一些弊端,例如硬件資源的限制和線程間的通信開(kāi)銷等。因此,我們需要在使用多線程的同時(shí),考慮到 I/O 密集型和 CPU 密集型的差異,以避免過(guò)度開(kāi)啟線程導(dǎo)致性能下降。同時(shí),對(duì)于線程池的運(yùn)行情況,我們也需要有一定的了解和控制,以確保程序的高效穩(wěn)定運(yùn)行。
CompletableFuture 是銀彈嗎?
我們常說(shuō)“手拿錘子看什么都像釘子”,使用 CompletableFuture 的確能夠幫助我們解決許多獨(dú)立處理邏輯的問(wèn)題,但是如果使用過(guò)多的線程,反而會(huì)導(dǎo)致線程調(diào)度時(shí)間不能得到保障,線程會(huì)被浪費(fèi)在等待 CPU 時(shí)間片上,特別是對(duì)于那些本來(lái)執(zhí)行速度就很快的任務(wù),使用 CompletableFuture 之后反而會(huì)拖慢整體執(zhí)行時(shí)長(zhǎng)。
因此,在使用 CompletableFuture 時(shí),我們需要根據(jù)具體的場(chǎng)景和任務(wù),仔細(xì)考慮是否需要并行處理。如果需要并行處理,我們需要根據(jù)任務(wù)的性質(zhì)和執(zhí)行速度,選擇合適的線程池大小和并行線程數(shù)量,以避免線程調(diào)度時(shí)間的浪費(fèi)和執(zhí)行效率的下降。
測(cè)試案例
執(zhí)行 a,b,c,d4 個(gè)方法,比較同步執(zhí)行與異步執(zhí)行的耗時(shí)情況。
全同步執(zhí)行
private voidtest(){
longs=System.currentTimeMillis();
a(10);
b(10);
c(10);
d(10);
longe=System.currentTimeMillis();
System.out.println(e - s);
}
publicvoida(int time){
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
}
publicvoidb(int time){
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
}
publicvoidc(int time){
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
}
publicvoidd(int time){
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
}
全異步執(zhí)行
private voidtest2(){
longs=System.currentTimeMillis();
List<CompletableFuture<?>> completableFutureList =newArrayList<>();
CompletableFuture<Void> future1 =CompletableFuture.runAsync(()->{
a(10);
});
completableFutureList.add(future1);
CompletableFuture<Void> future2 =CompletableFuture.runAsync(()->{
b(10);
});
completableFutureList.add(future2);
CompletableFuture<Void> future3 =CompletableFuture.runAsync(()->{
c(10);
});
completableFutureList.add(future3);
CompletableFuture<Void> future4 =CompletableFuture.runAsync(()->{
d(10);
});
completableFutureList.add(future4);
CompletableFuture<?>[] futures = completableFutureList.toArray(newCompletableFuture[0]);
CompletableFuture<Void> futureAll =CompletableFuture.allOf(futures);
futureAll.join();
longe=System.currentTimeMillis();
System.out.println(e - s);
}
結(jié)果統(tǒng)計(jì)
測(cè)試結(jié)論
在分配了相對(duì)合理的線程池的情況下,通過(guò)以上分析,可以得出下列兩個(gè)結(jié)論:
? 方法耗時(shí)越少,同步比異步越好。
? 方法數(shù)量越少,同步比異步越好。
半異步,半同步
有時(shí)候,如果方法較多,為了減少高并發(fā)時(shí) P99 較高,我們可以讓耗時(shí)多的方法異步執(zhí)行,耗時(shí)少的方法同步執(zhí)行。
通過(guò)以下數(shù)據(jù)可以看出,耗時(shí)是差不多的,但可以節(jié)省不少線程資源。
總結(jié)
CompletableFuture 提供了一種優(yōu)雅而強(qiáng)大的方式來(lái)處理并發(fā)請(qǐng)求和任務(wù)。然而,正如在處理高并發(fā)時(shí)使用過(guò)多的線程會(huì)導(dǎo)致資源浪費(fèi)和效率下降一樣,使用過(guò)多的 CompletableFuture 也會(huì)導(dǎo)致同樣的問(wèn)題。這種現(xiàn)象被稱為 "線程調(diào)度問(wèn)題",它會(huì)導(dǎo)致性能下降和吞吐量下降(P99 值較高)。
因此,我們需要在使用 CompletableFuture 時(shí)考慮實(shí)際場(chǎng)景和負(fù)載情況,并根據(jù)需要使用恰當(dāng)?shù)募夹g(shù)來(lái)優(yōu)化性能。
2. 最小化事務(wù)范圍
簡(jiǎn)要說(shuō)明
首先,我們需要明確的是,事務(wù)的存在勢(shì)必會(huì)對(duì)性能產(chǎn)生影響,特別是在高并發(fā)的情況下,因?yàn)殒i的競(jìng)爭(zhēng),會(huì)帶來(lái)極大的性能損耗。因此,在處理數(shù)據(jù)交互的過(guò)程中,我們始終堅(jiān)持盡可能地減少事務(wù)的范圍,從而提升接口的響應(yīng)速度。
一般來(lái)說(shuō),我們可以利用@Transactional 注解輕松實(shí)現(xiàn)事務(wù)的控制。但是,由于@Transactional 注解的最小粒度僅限于方法級(jí)別,因此,為了更好地控制事務(wù)的范圍,我們需要通過(guò)編程式事務(wù)來(lái)實(shí)現(xiàn)。
在編程式事務(wù)中,我們可以更靈活地控制事務(wù)的開(kāi)啟和結(jié)束,以及對(duì)數(shù)據(jù)庫(kù)操作的處理。通過(guò)適當(dāng)?shù)脑O(shè)置事務(wù)參數(shù)和操作規(guī)則,我們可以實(shí)現(xiàn)事務(wù)的最小化,從而提升系統(tǒng)的性能和可靠性。
編程式事務(wù)模板
public interfaceTransactionControlService{
/**
* 事務(wù)處理
*
* @param objectLogicFunction 業(yè)務(wù)邏輯
* @param <T> result type
* @return 處理結(jié)果
* @throws Exception 業(yè)務(wù)異常信息
*/
<T> T execute(ObjectLogicFunction<T> objectLogicFunction)throwsException;
/**
* 事務(wù)處理
*
* @param voidLogicFunction 業(yè)務(wù)邏輯
* @throws Exception 業(yè)務(wù)異常信息
*/
voidexecute(VoidLogicFunction voidLogicFunction)throwsException;
}
@Service
publicclassTransactionControlServiceImplimplementsTransactionControlService{
@Autowired
privatePlatformTransactionManager platformTransactionManager;
@Autowired
privateTransactionDefinition transactionDefinition;
/**
* 事務(wù)處理
*
* @param businessLogic 業(yè)務(wù)邏輯
* @param <T> result type
* @return 處理結(jié)果
* @throws Exception 業(yè)務(wù)異常信息
*/
@Override
public<T> T execute(ObjectLogicFunction<T> businessLogic)throwsException{
TransactionStatustransactionStatus= platformTransactionManager.getTransaction(transactionDefinition);
try{
Tresp= businessLogic.logic();
platformTransactionManager.commit(transactionStatus);
return resp;
}catch(Exception e){
platformTransactionManager.rollback(transactionStatus);
thrownewException(e);
}
}
/**
* 事務(wù)處理
*
* @param businessLogic 業(yè)務(wù)邏輯
*/
@Override
publicvoidexecute(VoidLogicFunction businessLogic)throwsException{
TransactionStatustransactionStatus= platformTransactionManager.getTransaction(transactionDefinition);
try{
businessLogic.logic();
platformTransactionManager.commit(transactionStatus);
}catch(Exception e){
platformTransactionManager.rollback(transactionStatus);
thrownewException(e);
}
}
}
@FunctionalInterface
public interface ObjectLogicFunction<T> {
/**
* 業(yè)務(wù)邏輯處理
*
* @return 業(yè)務(wù)處理結(jié)果
* @throws BusinessException e
*/
T logic() throws BusinessException;
}
@FunctionalInterface
public interface VoidLogicFunction {
/**
* 業(yè)務(wù)邏輯處理
*
* @throws Exception e
*/
void logic() throws Exception;
}
transactionControlService.execute(() -> {
// 把需要事務(wù)控制的業(yè)務(wù)邏輯寫在這里即可
});
3. 緩存
簡(jiǎn)要說(shuō)明
緩存,這一在性能提升方面堪稱萬(wàn)金油的技術(shù)手段,它的重要性在各種計(jì)算機(jī)應(yīng)用領(lǐng)域中無(wú)可比擬。
緩存作為一種高效的數(shù)據(jù)讀取和寫入的優(yōu)化方式,被廣泛應(yīng)用于各種領(lǐng)域,包括電商、金融、游戲、直播等。
雖然在網(wǎng)絡(luò)上關(guān)于緩存的文章不勝枚舉,但要想充分發(fā)揮緩存的作用,需要針對(duì)具體的業(yè)務(wù)場(chǎng)景進(jìn)行深入分析和探討。因此,在本節(jié)中,我們將不過(guò)多贅述緩存的具體使用方法,而是重點(diǎn)列舉一些使用緩存時(shí)的注意事項(xiàng).
使用緩存時(shí)的注意事項(xiàng)
? 緩存過(guò)期時(shí)間: 設(shè)置合適的過(guò)期時(shí)間可以保證緩存的有效性,但過(guò)期時(shí)間過(guò)長(zhǎng)可能會(huì)浪費(fèi)內(nèi)存空間,過(guò)期時(shí)間過(guò)短可能會(huì)導(dǎo)致頻繁刷新緩存,影響性能。
? 緩存一致性: 如果緩存的數(shù)據(jù)與數(shù)據(jù)庫(kù)中的數(shù)據(jù)不一致,可能會(huì)導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問(wèn)題。因此,在使用緩存時(shí)需要考慮緩存一致性的問(wèn)題。
? 緩存容量限制: 緩存容量有限,如果緩存的數(shù)據(jù)量過(guò)大,可能會(huì)導(dǎo)致內(nèi)存溢出或者緩存頻繁清理。因此,在使用緩存時(shí)需要注意緩存容量的限制。
? 緩存需要考慮負(fù)載均衡: 在高并發(fā)場(chǎng)景下,需要考慮緩存的負(fù)載均衡問(wèn)題,避免某些緩存服務(wù)器因?yàn)闊狳c(diǎn)數(shù)據(jù)等問(wèn)題負(fù)載過(guò)重導(dǎo)致系統(tǒng)崩潰或者響應(yīng)變慢。
? 緩存需要考慮并發(fā)讀寫: 當(dāng)多個(gè)用戶同時(shí)訪問(wèn)緩存時(shí),需要考慮并發(fā)讀寫的問(wèn)題,避免緩存沖突和數(shù)據(jù)一致性問(wèn)題。
? 緩存穿透問(wèn)題: 當(dāng)大量的查詢請(qǐng)求都無(wú)法命中緩存時(shí),導(dǎo)致每次查詢都會(huì)落到數(shù)據(jù)庫(kù)上,從而造成數(shù)據(jù)庫(kù)壓力過(guò)大。
? 緩存擊穿問(wèn)題: 當(dāng)緩存數(shù)據(jù)失效后,導(dǎo)致大量的請(qǐng)求直接打到數(shù)據(jù)庫(kù)中,從而造成數(shù)據(jù)庫(kù)壓力過(guò)大。
? 查詢時(shí)間復(fù)雜度: 需額外注意緩存查詢的時(shí)間復(fù)雜度問(wèn)題,如果是 O(n),甚至更差的時(shí)間復(fù)雜度,則會(huì)因?yàn)榫彺娴臄?shù)據(jù)量增加而跟著增加。
考慮到這些問(wèn)題通常優(yōu)化的手段
? 數(shù)據(jù)壓縮: 選擇合理的數(shù)據(jù)類型,舉個(gè)例子:如果用
Integer[]和int[]來(lái)比較,Integer 占用的空間大約是 int 的 4 倍。其他情況下,使用一些常見(jiàn)數(shù)據(jù)編碼壓縮技術(shù)也是常見(jiàn)的節(jié)省內(nèi)存的方式,比如:BitMap、字典編碼等。? 預(yù)加載: 當(dāng)行為可預(yù)測(cè)時(shí),那么提前加載便可解決構(gòu)建緩存時(shí)的壓力。
? 熱點(diǎn)數(shù)據(jù): 熱點(diǎn)數(shù)據(jù)如果不能打散,那么通常就會(huì)構(gòu)建多級(jí)緩存,比如將應(yīng)用服務(wù)設(shè)為一級(jí)緩存,Redis 設(shè)為二級(jí)緩存,一級(jí)緩存,緩存全量熱點(diǎn)數(shù)據(jù),從而實(shí)現(xiàn)壓力分?jǐn)偂?/p>
? 緩存穿透、擊穿: 針對(duì)命中不了緩存的查詢也可以緩存一個(gè)額外的標(biāo)識(shí);而針對(duì)緩存失效,要么就在失效前,主動(dòng)刷新一次,要么就分散失效時(shí)間,避免大量緩存同時(shí)失效。
? 時(shí)間復(fù)雜度: 在設(shè)計(jì)緩存時(shí),優(yōu)先考慮選擇常數(shù)級(jí)的時(shí)間復(fù)雜度的方法。
4. 合理使用線程池
簡(jiǎn)要說(shuō)明
在本文開(kāi)始提到的使用 CompletableFuture 并行處理時(shí),實(shí)際上就已經(jīng)使用到線程池了,池化技術(shù)的好處,我想應(yīng)該不用再過(guò)多闡述了,但關(guān)于線程池的使用還是有很多注意點(diǎn)的。
使用場(chǎng)景
異步任務(wù)
簡(jiǎn)單來(lái)說(shuō)就是某些不需要同步返回業(yè)務(wù)處理結(jié)果的場(chǎng)景,比如:短信、郵件等通知類業(yè)務(wù),評(píng)論、點(diǎn)贊等互動(dòng)性業(yè)務(wù)。
并行計(jì)算
就像 MapReduce 一樣,充分利用多線程的并行計(jì)算能力,將大任務(wù)拆分為多個(gè)子任務(wù),最后再將所有子任務(wù)計(jì)算后的結(jié)果進(jìn)行匯總,ForkJoinPool 就是 JDK 中典型的并行計(jì)算框架。
同步任務(wù)
前面講到的 CompletableFuture 使用,就是典型的同步改異步的方式,如果任務(wù)之間沒(méi)有依賴,那么就可以利用線程,同時(shí)進(jìn)行處理,這樣理論上就只需要等待耗時(shí)最長(zhǎng)的步驟結(jié)束即可(實(shí)際情況可參考 CompletableFuture 分析)。
線程池的創(chuàng)建
不要直接使用 Executors 創(chuàng)建線程池,應(yīng)通過(guò) ThreadPoolExecutor 的方式,主動(dòng)明確線程池的參數(shù),避免產(chǎn)生意外。
每個(gè)參數(shù)都要顯示設(shè)置,例如像下面這樣:
private staticfinalExecutorServiceexecutor=newThreadPoolExecutor(
2,
4,
1L,
TimeUnit.MINUTES,
newLinkedBlockingQueue<>(100),
newThreadFactoryBuilder().setNameFormat("common-pool-%d").build(),
newThreadPoolExecutor.CallerRunsPolicy());
參數(shù)的配置建議
CorePoolSize(核心線程數(shù))
一般在配置核心線程數(shù)的時(shí)候,是需要結(jié)合線程池將要處理任務(wù)的特性來(lái)決定的,而任務(wù)的性質(zhì)一般可以劃分為:CPU 密集型、I/O 密集型。
比較通用的配置方式如下
? CPU 密集型: 一般建議線程的核心數(shù)與 CPU 核心數(shù)保持一致。
? I/O 密集型: 一般可以設(shè)置 2 倍的 CPU 核心數(shù)的線程數(shù),因?yàn)榇祟惾蝿?wù) CPU 比較空閑,可以多分配點(diǎn)線程充分利用 CPU 資源來(lái)提高效率。
通過(guò)Runtime.getRuntime().availableProcessors()可以獲取核心線程數(shù)。
另外還有一個(gè)公式可以借鑒
? 線程核心數(shù) = cpu 核心數(shù) / (1-阻塞系數(shù))
? 阻塞系數(shù) = 阻塞時(shí)間/(阻塞時(shí)間+使用 CPU 的時(shí)間)
實(shí)際上大多數(shù)線上業(yè)務(wù)所消耗的時(shí)間主要就是 I/O 等待,因此一般線程數(shù)都可以設(shè)置的多一點(diǎn),比如 tomcat 中默認(rèn)的線程數(shù)就是 200,所以最佳的核心線程數(shù)是需要根據(jù)特定場(chǎng)景,然后通過(guò)實(shí)際上線上允許結(jié)果分析后,再不斷的進(jìn)行調(diào)整。
MaximumPoolSize
maximumPoolSize 的設(shè)置也是看實(shí)際應(yīng)用場(chǎng)景,如果設(shè)置的和 corePoolSize 一樣,那就完全依靠阻塞隊(duì)列和拒絕策略來(lái)控制任務(wù)的處理情況,如果設(shè)置的比 corePoolSize 稍微大一點(diǎn),那就可以更好的應(yīng)對(duì)一些有突發(fā)流量產(chǎn)生的場(chǎng)景。
KeepAliveTime
由 maximumPoolSize 創(chuàng)建出來(lái)的線程,在經(jīng)過(guò) keepAliveTime 時(shí)間后進(jìn)行銷毀,依據(jù)突發(fā)流量持續(xù)的時(shí)間來(lái)決定。
WorkQueue
那么阻塞隊(duì)列應(yīng)該設(shè)置多大呢?我們知道當(dāng)線程池中所有的線程都在工作時(shí),如果再有任務(wù)進(jìn)來(lái),就會(huì)被放到阻塞隊(duì)列中等待,如果阻塞隊(duì)列設(shè)置的太小,可能很快隊(duì)列就滿了,導(dǎo)致任務(wù)被丟棄或者異常(由拒絕策略決定),如果隊(duì)列設(shè)置的太大,又可能會(huì)帶來(lái)內(nèi)存資源的緊張,甚至 OOM,以及任務(wù)延遲時(shí)間過(guò)長(zhǎng)。
所以阻塞隊(duì)列的大小,又是要結(jié)合實(shí)際場(chǎng)景來(lái)設(shè)置的。
一般會(huì)根據(jù)處理任務(wù)的速度與任務(wù)產(chǎn)生的速度進(jìn)行計(jì)算得到一個(gè)大概的數(shù)值。
假設(shè)現(xiàn)在有 1 個(gè)線程,每秒鐘可以處理 10 個(gè)任務(wù),正常情況下每秒鐘產(chǎn)生的任務(wù)數(shù)小于 10,那么此時(shí)隊(duì)列長(zhǎng)度為 10 就足以。
但是如果高峰時(shí)期,每秒產(chǎn)生的任務(wù)數(shù)會(huì)達(dá)到 20,會(huì)持續(xù) 10 秒,且任務(wù)又不希望丟棄,那么此時(shí)隊(duì)列的長(zhǎng)度就需要設(shè)置到 100。
監(jiān)控 workQueue 中等待任務(wù)的數(shù)量是非常重要的,只有了解實(shí)際的情況,才能做出正確的決定。
在有些場(chǎng)景中,可能并不希望因?yàn)槿蝿?wù)被丟進(jìn)阻塞隊(duì)列而等待太長(zhǎng)的時(shí)間,而是希望直接開(kāi)啟設(shè)置的
MaximumPoolSize線程池?cái)?shù)來(lái)執(zhí)行任務(wù),這種情況下一般可以直接使用SynchronousQueue隊(duì)列來(lái)實(shí)現(xiàn)
ThreadFactory
通過(guò) threadFactory 我們可以自定義線程組的名字,設(shè)置合理的名稱將有利于你線上進(jìn)行問(wèn)題排查。
Handler
最后拒絕策略,這也是要結(jié)合實(shí)際的業(yè)務(wù)場(chǎng)景來(lái)決定采用什么樣的拒絕方式,例如像過(guò)程類的數(shù)據(jù),可以直接采用 DiscardOldestPolicy 策略。
線程池的監(jiān)控
線上使用線程池時(shí),一定要做好監(jiān)控,以便根據(jù)實(shí)際運(yùn)行情況進(jìn)行調(diào)整,常見(jiàn)的監(jiān)控方式可以通過(guò)線程池提供的 API,然后暴露給 Metrics 來(lái)完成實(shí)時(shí)數(shù)據(jù)統(tǒng)計(jì)。
監(jiān)控示例
線程池自身提供的統(tǒng)計(jì)數(shù)據(jù)
public classThreadPoolMonitor{
privatefinalstaticLoggerlog=LoggerFactory.getLogger(ThreadPoolMonitor.class);
privatestaticfinalThreadPoolExecutorthreadPool=newThreadPoolExecutor(2,4,0,
TimeUnit.SECONDS,newLinkedBlockingQueue<>(100),
newThreadFactoryBuilder().setNameFormat("my_thread_pool_%d").build());
publicstaticvoidmain(String[] args){
log.info("Pool Size: "+ threadPool.getPoolSize());
log.info("Active Thread Count: "+ threadPool.getActiveCount());
log.info("Task Queue Size: "+ threadPool.getQueue().size());
log.info("Completed Task Count: "+ threadPool.getCompletedTaskCount());
}
}
通過(guò) micrometer API 完成統(tǒng)計(jì),這樣就可以接入Prometheus了
package com.springboot.micrometer.monitor;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.micrometer.core.instrument.Metrics;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;
@Component
publicclassThreadPoolMonitor{
privatestaticfinalThreadPoolExecutorthreadPool=newThreadPoolExecutor(4,8,0,
TimeUnit.SECONDS,newLinkedBlockingQueue<>(100),
newThreadFactoryBuilder().setNameFormat("my_thread_pool_%d").build(),newThreadPoolExecutor.DiscardOldestPolicy());
/**
* 活躍線程數(shù)
*/
privateAtomicLongactiveThreadCount=newAtomicLong(0);
/**
* 隊(duì)列任務(wù)數(shù)
*/
privateAtomicLongtaskQueueSize=newAtomicLong(0);
/**
* 完成任務(wù)數(shù)
*/
privateAtomicLongcompletedTaskCount=newAtomicLong(0);
/**
* 線程池中當(dāng)前線程的數(shù)量
*/
privateAtomicLongpoolSize=newAtomicLong(0);
@PostConstruct
privatevoidinit(){
/**
* 通過(guò)micrometer API完成統(tǒng)計(jì)
*
* gauge最典型的使用場(chǎng)景就是統(tǒng)計(jì):list、Map、線程池、連接池等集合類型的數(shù)據(jù)
*/
Metrics.gauge("my_thread_pool_active_thread_count", activeThreadCount);
Metrics.gauge("my_thread_pool_task_queue_size", taskQueueSize);
Metrics.gauge("my_thread_pool_completed_task_count", completedTaskCount);
Metrics.gauge("my_thread_pool_size", poolSize);
// 模擬線程池的使用
newThread(this::runTask).start();
}
privatevoidrunTask(){
// 每5秒監(jiān)控一次線程池的使用情況
monitorThreadPoolState();
// 模擬任務(wù)執(zhí)行
IntStream.rangeClosed(0,500).forEach(i ->{
// 每500毫秒,執(zhí)行一個(gè)任務(wù)
try{
TimeUnit.MILLISECONDS.sleep(500);
}catch(InterruptedException e){
e.printStackTrace();
}
// 每個(gè)處理一個(gè)任務(wù)耗時(shí)5秒
threadPool.submit(()->{
try{
TimeUnit.MILLISECONDS.sleep(5000);
}catch(InterruptedException e){
e.printStackTrace();
}
});
});
}
privatevoidmonitorThreadPoolState(){
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(()->{
activeThreadCount.set(threadPool.getActiveCount());
taskQueueSize.set(threadPool.getQueue().size());
poolSize.set(threadPool.getPoolSize());
completedTaskCount.set(threadPool.getCompletedTaskCount());
},0,5,TimeUnit.SECONDS);
}
}
線程池的資源隔離
在生產(chǎn)環(huán)境中,一定要注意好資源隔離的問(wèn)題,盡量不要將不同類型,不同重要等級(jí)的任務(wù)放入一個(gè)線程池中,以免因?yàn)榫€程資源爭(zhēng)搶而互相影響。
5. 服務(wù)預(yù)熱
服務(wù)預(yù)熱也是很常見(jiàn)的一種優(yōu)化手段,例如數(shù)據(jù)庫(kù)連接、線程池中的核心線程,緩存等信息可以利用服務(wù)啟動(dòng)階段預(yù)先加載,從而避免請(qǐng)求到來(lái)后臨時(shí)構(gòu)建的耗時(shí)。
下面提供一些預(yù)加載的方式
線程池
線程池本身提供了相關(guān)的 API:prestartAllCoreThreads()通過(guò)該方法可以提前將核心線程創(chuàng)建好,非常方便。
Web 服務(wù)
常見(jiàn)的如 Tomcat,其本身也用到了線程池,只是其自身已經(jīng)考慮到了預(yù)加載的問(wèn)題,不需要我們額外處理了。
連接池
連接池常用的一般就是數(shù)據(jù)庫(kù)連接池以及Redis連接池,大多數(shù)這些連接的客戶端也都做了連接提前加載的工作,遇到?jīng)]有預(yù)加載的參考其他客戶端方式搞一下即可。
緩存
一般本地緩存可以在每次服務(wù)啟動(dòng)時(shí)預(yù)先加載好,以免出現(xiàn)緩存擊穿的情況。
靜態(tài)代碼塊
在服務(wù)啟動(dòng)時(shí),靜態(tài)代碼塊中的相關(guān)功能會(huì)優(yōu)先被加載,可以有效避免在運(yùn)行時(shí)再加載的情況。
其他擴(kuò)展
預(yù)熱實(shí)際上可聊的內(nèi)容很多,一般有用到池化技術(shù)的方式,都是需要預(yù)熱的,為了能夠提升響應(yīng)性能,將不在內(nèi)存中的數(shù)據(jù)提前查好放入內(nèi)存中,或者將需要計(jì)算的數(shù)據(jù)提前計(jì)算好,這都是很容易想到的解決方式。
此外還有一些服務(wù)端在設(shè)計(jì)之初就會(huì)針對(duì)性地對(duì)一些熱點(diǎn)數(shù)據(jù)進(jìn)行特殊處理,比如JVM中的JIT、內(nèi)存分配比;OS中的page cache;MySQL中的innodb_buffer_pool等,這些一般可以通過(guò)流量預(yù)熱的方式來(lái)使其達(dá)到最佳狀態(tài)。
6. 緩存對(duì)齊
CPU的多級(jí)緩存
CPU緩存通常分為大小不等的三級(jí)緩存
來(lái)自百度百科對(duì)三級(jí)緩存分類的介紹:
一級(jí)緩存都內(nèi)置在CPU內(nèi)部并與CPU同速運(yùn)行,可以有效的提高CPU的運(yùn)行效率。一級(jí)緩存越大,CPU的運(yùn)行效率越高,但受到CPU內(nèi)部結(jié)構(gòu)的限制,一級(jí)緩存的容量都很小。
二級(jí)緩存,它是為了協(xié)調(diào)一級(jí)緩存和內(nèi)存之間的速度。cpu調(diào)用緩存首先是一級(jí)緩存,當(dāng)處理器的速度逐漸提升,會(huì)導(dǎo)致一級(jí)緩存就供不應(yīng)求,這樣就得提升到二級(jí)緩存了。二級(jí)緩存它比一級(jí)緩存的速度相對(duì)來(lái)說(shuō)會(huì)慢,但是它比一級(jí)緩存的空間容量要大。主要就是做一級(jí)緩存和內(nèi)存之間數(shù)據(jù)臨時(shí)交換的地方用。
三級(jí)緩存是為讀取二級(jí)緩存后未命中的數(shù)據(jù)設(shè)計(jì)的—種緩存,在擁有三級(jí)緩存的CPU中,只有約5%的數(shù)據(jù)需要從內(nèi)存中調(diào)用,這進(jìn)一步提高了CPU的效率。其運(yùn)作原理在于使用較快速的儲(chǔ)存裝置保留一份從慢速儲(chǔ)存裝置中所讀取數(shù)據(jù)并進(jìn)行拷貝,當(dāng)有需要再?gòu)妮^慢的儲(chǔ)存體中讀寫數(shù)據(jù)時(shí),緩存(cache)能夠使得讀寫的動(dòng)作先在快速的裝置上完成,如此會(huì)使系統(tǒng)的響應(yīng)較為快速。
效果演示
逐行寫入
public classCacheLine{
publicstaticvoidmain(String[] args){
int[][] arr =newint[10000][10000];
longs=System.currentTimeMillis();
for(inti=0; i < arr.length; i++){
for(intj=0; j < arr[i].length; j++){
arr[i][j]=0;
}
}
longe=System.currentTimeMillis();
System.out.println(e-s);
}
}
逐列寫入
public classCacheLine{
publicstaticvoidmain(String[] args){
int[][] arr =newint[10000][10000];
longs=System.currentTimeMillis();
for(inti=0; i < arr.length; i++){
for(intj=0; j < arr[i].length; j++){
arr[j][i]=0;
}
}
longe=System.currentTimeMillis();
System.out.println(e-s);
}
}
雖然兩種方式得到的結(jié)果是一樣的,但性能對(duì)比卻相差巨大,這就是緩存行帶來(lái)的影響。
原因分析
CPU的緩存是由多個(gè)緩存行組成的,以緩存行為基本單位,一個(gè)緩存行的大小一般為64字節(jié),二維數(shù)組在內(nèi)存中保存時(shí),實(shí)際上是以按行遍歷的方式進(jìn)行保存,比如:arr[0][0],arr[0][1],arr[1][0],arr[1][1],arr[2][0],arr[2][1]...
所以當(dāng)按行訪問(wèn)時(shí),是按照內(nèi)存存儲(chǔ)的順序進(jìn)行訪問(wèn),那么CPU緩存后面的元素就可以利用到,而如果是按列訪問(wèn),那么CPU的緩存是沒(méi)有用的。
緩存行對(duì)齊
public classCacheLinePadding{
privatestaticclassPadding{
// 一個(gè)long是8個(gè)字節(jié),一共7個(gè)long
// public volatile long p1, p2, p3, p4, p5, p6, p7;
}
privatestaticclassTextendsPadding{
// x變量8個(gè)字節(jié),加上Padding中的變量,剛好64個(gè)字節(jié),獨(dú)占一個(gè)緩存行。
publicvolatilelongx=0L;
}
publicstatic T[] arr =newT[2];
static{
arr[0]=newT();
arr[1]=newT();
}
publicstaticvoidmain(String[] args)throwsException{
Threadt1=newThread(()->{
for(longi=0; i <10000000; i++){
arr[0].x = i;
}
});
Threadt2=newThread(()->{
for(longi=0; i <10000000; i++){
arr[1].x = i;
}
});
finallongstart=System.nanoTime();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println((System.nanoTime()- start)/100000);
}
}
同樣的含有public volatile long p1, p2, p3, p4, p5, p6, p7;這一行代碼與不含性能也相差巨大,這同樣也是因?yàn)榫彺嫘械脑颍?dāng)運(yùn)行在兩個(gè)不同CPU上的兩個(gè)線程要寫入。
7. 減少對(duì)象的產(chǎn)生
避免使用包裝類型
因?yàn)榘b類型的創(chuàng)建和銷毀都會(huì)產(chǎn)生臨時(shí)對(duì)象,因此相比基本數(shù)據(jù)類型來(lái)說(shuō),會(huì)帶來(lái)額外的消耗。
public classMain{
publicstaticvoidmain(String[] args){
longs=System.currentTimeMillis();
testInteger();
longe=System.currentTimeMillis();
System.out.println(e - s);
testInt();
longe2=System.currentTimeMillis();
System.out.println(e2 - e);
}
privatestaticvoidtestInt(){
intsum=1;
for(inti=1; i <50000000; i++){
sum++;
}
System.out.println(sum);
}
privatestaticvoidtestInteger(){
Integersum=1;
for(inti=1; i <50000000; i++){
sum++;
}
System.out.println(sum);
}
}
兩個(gè)方法不僅執(zhí)行時(shí)間相差百倍,在CPU和內(nèi)存的消耗上Integer也明顯弱于int。
Integer內(nèi)存和CPU都能看到明顯的波動(dòng)
int幾乎沒(méi)波動(dòng)
使用不可變對(duì)象
最為典型的案例就是String,我想應(yīng)該不會(huì)有人去通過(guò)new的方式再去構(gòu)建一個(gè)String字符串了吧!
String str = new String("abc");
String str = "abc";
同時(shí),在實(shí)現(xiàn)字符串連接時(shí)通常使用StringBuilder或StringBuffer,這樣可以避免使用連接符,導(dǎo)致每次都創(chuàng)建新的字符串對(duì)象。
靜態(tài)方法
靜態(tài)對(duì)象
Boolean.valueOf("true");
publicstaticBooleanvalueOf(String s){
return parseBoolean(s)? TRUE : FALSE;
}
publicstaticfinalBooleanTRUE=newBoolean(true);
publicstaticfinalBooleanFALSE=newBoolean(false);
靜態(tài)工廠(單例模式)
public classStaticSingleton{
privatestaticclassStaticHolder{
publicstaticfinalStaticSingletonINSTANCE=newStaticSingleton();
}
publicstaticStaticSingletongetInstance(){
returnStaticHolder.INSTANCE;
}
}
枚舉
public enum EnumSingleton { INSTANCE; }
視圖
視圖是返回引用的一種方式。
map的keySet方法,實(shí)際上每次返回的都是同一個(gè)對(duì)象的引用。
public Set<K>keySet(){
Set<K> ks = keySet;
if(ks ==null){
ks =newKeySet();
keySet = ks;
}
return ks;
}
對(duì)象池
對(duì)象池可以有效減少頻繁的對(duì)象創(chuàng)建和銷毀的過(guò)程,一般情況下如果每次創(chuàng)建對(duì)象的過(guò)程較為復(fù)雜,且對(duì)象占用空間又比較大,那么就建議使用對(duì)象池的方式來(lái)優(yōu)化。
使用示例
org.apache.commons提供了對(duì)象池的工具類,可以直接拿來(lái)使用
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
池化的對(duì)象
@Data
public class Cache {
private byte[] size;
}
池化對(duì)象工廠
public classCachePoolObjectFactoryextendsBasePooledObjectFactory<Cache>{
@Override
publicCachecreate(){
Cachecache=newCache();
cache.setSize(newbyte[1024*1024*16]);
return cache;
}
@Override
publicPooledObject<Cache>wrap(Cache cache){
returnnewDefaultPooledObject<>(cache);
}
}
對(duì)象池工具
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.Duration;
publicenumCachePoolUtil{
INSTANCE;
privateGenericObjectPool<Cache> objectPool;
CachePoolUtil(){
GenericObjectPoolConfig<Cache> poolConfig =newGenericObjectPoolConfig<>();
// 對(duì)象池中最大對(duì)象數(shù)
poolConfig.setMaxTotal(50);
// 對(duì)象池中最小空閑對(duì)象數(shù)
poolConfig.setMinIdle(20);
// 對(duì)象池中最大空閑對(duì)象數(shù)
poolConfig.setMaxIdle(20);
// 獲取對(duì)象最大等待時(shí)間 默認(rèn) -1 一直等待
poolConfig.setMaxWait(Duration.ofSeconds(3));
// 創(chuàng)建對(duì)象工廠
CachePoolObjectFactoryobjectFactory=newCachePoolObjectFactory();
// 創(chuàng)建對(duì)象池
objectPool =newGenericObjectPool<>(objectFactory, poolConfig);
}
/**
* 從對(duì)象池中取出一個(gè)對(duì)象
*/
publicCacheborrowObject()throwsException{
return objectPool.borrowObject();
}
publicvoidreturnObject(Cache cache){
// 將對(duì)象歸還給對(duì)象池
objectPool.returnObject(cache);
}
/**
* 獲取活躍的對(duì)象數(shù)
*/
publicintgetNumActive(){
return objectPool.getNumActive();
}
/**
* 獲取空閑的對(duì)象數(shù)
*/
publicintgetNumIdle(){
return objectPool.getNumIdle();
}
}
public classMain{
publicstaticvoidmain(String[] args){
CachePoolUtilcachePoolUtil=CachePoolUtil.INSTANCE;
for(inti=0; i <10; i++){
newThread(newRunnable(){
@SneakyThrows
@Override
publicvoidrun(){
while(true){
Thread.sleep(100);
// 使用對(duì)象池
Cachecache= cachePoolUtil.borrowObject();
m(cache);
cachePoolUtil.returnObject(cache);
// 不使用對(duì)象池
//Cache cache = new Cache();
//cache.setSize(new byte[1024 * 1024 * 2]);
//m(cache);
}
}
}).start();
}
}
// 無(wú)特殊作用
publicstaticvoidm(Cache cache){
if(cache.getSize().length <10){
System.out.println(cache);
}
}
}
使用對(duì)象池
不適用對(duì)象池
8. 并發(fā)處理
鎖的粒度控制
并發(fā)場(chǎng)景下就要考慮線程安全的問(wèn)題,常見(jiàn)的解決方式:volatile、CAS、自旋鎖、對(duì)象鎖、類鎖、分段鎖、讀寫鎖,理論上來(lái)說(shuō),鎖的粒度越小,并行效果就越高。
volatile
volatile是Java中的一個(gè)關(guān)鍵字,用于修飾變量。它的作用是保證被volatile修飾的變量在多線程環(huán)境下的可見(jiàn)性和禁止指令重排序。
volatile雖然不能保證原子性,但如果對(duì)共享變量是純賦值或讀取的操作,那么因?yàn)関olatile保證了可見(jiàn)性,因此也是可以實(shí)現(xiàn)線程安全的。
CAS
compare and swap(比較并交換),CAS主要有三個(gè)參數(shù),
? V:內(nèi)存值
? A:當(dāng)前時(shí)
? B:待更新的值
當(dāng)且僅當(dāng)V等于A時(shí),就將A更新為B,否則什么都不做。V和A的比較是一個(gè)原子性操作保證線程安全。
Random通過(guò)cas的方式保證了線程安全,但在高并發(fā)下很有可能會(huì)失敗,造成頻繁的重試。
protected intnext(int bits){
long oldseed, nextseed;
AtomicLongseed=this.seed;
do{
oldseed = seed.get();
nextseed =(oldseed * multiplier + addend)& mask;
}while(!seed.compareAndSet(oldseed, nextseed));
return(int)(nextseed >>>(48- bits));
}
ThreadLocalRandom進(jìn)行了優(yōu)化,其主要方式就是分段,通過(guò)讓每個(gè)線程擁有獨(dú)立的存儲(chǔ)空間,這樣即保證了線程安全,同時(shí)效率也不會(huì)太差。
public staticThreadLocalRandomcurrent(){
if(U.getInt(Thread.currentThread(), PROBE)==0)
localInit();
return instance;
}
staticfinalvoidlocalInit(){
intp= probeGenerator.addAndGet(PROBE_INCREMENT);
intprobe=(p ==0)?1: p;// skip 0
longseed= mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Threadt=Thread.currentThread();
U.putLong(t, SEED, seed);
U.putInt(t, PROBE, probe);
}
publicintnextInt(){
return mix32(nextSeed());
}
finallongnextSeed(){
Thread t;long r;// read and update per-thread seed
U.putLong(t =Thread.currentThread(), SEED,
r = U.getLong(t, SEED)+ GAMMA);
return r;
}
對(duì)象鎖、類鎖
主要就是通過(guò)synchronized實(shí)現(xiàn),是最基礎(chǔ)的鎖機(jī)制。
自旋鎖
在自旋鎖中,當(dāng)一個(gè)操作需要訪問(wèn)一個(gè)共享資源時(shí),它會(huì)檢查這個(gè)資源是否被其他操作占用。如果是,它會(huì)一直等待,直到資源被釋放。在等待期間,這個(gè)操作會(huì)進(jìn)入一個(gè)自旋狀態(tài),也就是不會(huì)被系統(tǒng)掛起,但是也不會(huì)繼續(xù)執(zhí)行其他任務(wù)。當(dāng)資源被釋放后,這個(gè)操作會(huì)立即返回并繼續(xù)執(zhí)行下一步操作。
自旋鎖是一種簡(jiǎn)單而有效的同步機(jī)制,自旋鎖的優(yōu)點(diǎn)是減少線程上下文切換的開(kāi)銷,但是它也有一些缺點(diǎn)。由于它需要一直進(jìn)行自旋操作,所以會(huì)消耗一定的CPU資源。因此,在使用自旋鎖時(shí)需要仔細(xì)考慮并發(fā)問(wèn)題和性能問(wèn)題。
分段鎖
在分段鎖的模型中,共享數(shù)據(jù)被分割成若干個(gè)段,每個(gè)段都被一個(gè)鎖所保護(hù),同時(shí)只有一個(gè)線程可以在同一時(shí)刻對(duì)同一段進(jìn)行加鎖和解鎖操作。這種鎖機(jī)制可以降低鎖的競(jìng)爭(zhēng),提高并發(fā)訪問(wèn)的效率。
ConcurrentHashMap的設(shè)計(jì)就是采用分段鎖的思想,其會(huì)按照map中的table capacity(默認(rèn)16)來(lái)劃分,也就是說(shuō)每個(gè)線程會(huì)鎖1/16的數(shù)據(jù)段,這樣一來(lái)就大大提升了并發(fā)訪問(wèn)的效率。
讀寫鎖
讀寫鎖主要根據(jù)大多數(shù)業(yè)務(wù)場(chǎng)景都是讀多寫少的情況,在讀數(shù)據(jù)時(shí),無(wú)論多少線程同時(shí)訪問(wèn)都不會(huì)有安全問(wèn)題,所以在讀數(shù)據(jù)的時(shí)候可以不加鎖,不過(guò)一旦有寫請(qǐng)求時(shí)就需要加鎖了。
? 讀、讀:不沖突
? 讀、寫:沖突
? 寫、寫:沖突
典型的如:ReentrantReadWriteLock
寫時(shí)復(fù)制
寫時(shí)復(fù)制最大的優(yōu)勢(shì)在于,在寫數(shù)據(jù)的過(guò)程時(shí),不影響讀,可以理解為讀的是數(shù)據(jù)的副本,而只有當(dāng)數(shù)據(jù)真正寫完后才會(huì)替換副本,當(dāng)副本特別大、寫數(shù)據(jù)過(guò)程比較漫長(zhǎng)時(shí),寫時(shí)復(fù)制就特別有用了。
CopyOnWriteArrayList、CopyOnWriteArraySet就是集合操作時(shí),為保證線程安全,使用寫時(shí)復(fù)制的實(shí)現(xiàn)
public E get(int index){
return elementAt(getArray(), index);
}
finalObject[] getArray(){
return array;
}
publicbooleanadd(E e){
synchronized(lock){
Object[] es = getArray();
intlen= es.length;
es =Arrays.copyOf(es, len +1);
es[len]= e;
setArray(es);
returntrue;
}
}
finalvoidsetArray(Object[] a){
array = a;
}
寫時(shí)復(fù)制也存在兩個(gè)問(wèn)題,可以看到在add方法時(shí)使用了synchronized,也就是說(shuō)當(dāng)存在大量的寫入操作時(shí),效率實(shí)際上是非常低的,另一個(gè)問(wèn)題就是需要copy一份一模一樣的數(shù)據(jù),可能會(huì)造成內(nèi)存的異常波動(dòng),因此寫時(shí)復(fù)制實(shí)際上適用于讀多寫少的場(chǎng)景。
對(duì)比說(shuō)明
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
publicclassThreadSafeSet{
publicstaticvoidmain(String[] args)throwsInterruptedException{
//Set<String> set = ConcurrentHashMap.newKeySet();
//CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet();
readMoreWriteLess(set);
System.out.println("==========華麗的分隔符==========");
//set = ConcurrentHashMap.newKeySet();
//set = new CopyOnWriteArraySet();
writeMoreReadLess(set);
}
privatestaticvoidwriteMoreReadLess(Set<String> set)throwsInterruptedException{
//測(cè)20組
for(intk=1; k <=20; k++){
CountDownLatchcountDownLatch=newCountDownLatch(10);
longs=System.currentTimeMillis();
//創(chuàng)建9個(gè)線程,每個(gè)線程向set中寫1000條數(shù)據(jù)
for(inti=0; i <9; i++){
newThread(()->{
for(intj=0; j <1000; j++){
set.add(UUID.randomUUID().toString());
}
countDownLatch.countDown();
}).start();
}
//創(chuàng)建1個(gè)線程,每個(gè)線程從set中讀取所有數(shù)據(jù),每個(gè)線程一共讀取10次。
for(inti=0; i <1; i++){
newThread(()->{
for(intj=0; j <10; j++){
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
iterator.next();
}
}
countDownLatch.countDown();
}).start();
}
//阻塞,直到10個(gè)線程都執(zhí)行結(jié)束
countDownLatch.await();
longe=System.currentTimeMillis();
System.out.println("寫多讀少:第"+ k +"次執(zhí)行耗時(shí):"+(e - s)+"毫秒"+",容器中元素個(gè)數(shù)為:"+ set.size());
}
}
privatestaticvoidreadMoreWriteLess(Set<String> set)throwsInterruptedException{
//測(cè)20組
for(intk=1; k <=20; k++){
CountDownLatchcountDownLatch=newCountDownLatch(10);
longs=System.currentTimeMillis();
//創(chuàng)建1個(gè)線程,每個(gè)線程向set中寫10條數(shù)據(jù)
for(inti=0; i <1; i++){
newThread(()->{
for(intj=0; j <10; j++){
set.add(UUID.randomUUID().toString());
}
countDownLatch.countDown();
}).start();
}
//創(chuàng)建9個(gè)線程,每個(gè)線程從set中讀取所有數(shù)據(jù),每個(gè)線程一共讀取100萬(wàn)次。
for(inti=0; i <9; i++){
newThread(()->{
for(intj=0; j <1000000; j++){
Iterator<String> iterator = set.iterator();
while(iterator.hasNext()){
iterator.next();
}
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
longe=System.currentTimeMillis();
System.out.println("讀多寫少:第"+ k +"次執(zhí)行耗時(shí):"+(e - s)+"毫秒"+",容器中元素個(gè)數(shù)為:"+ set.size());
}
}
}
經(jīng)過(guò)測(cè)試可以發(fā)現(xiàn)在讀多寫少時(shí)CopyOnWriteArraySet會(huì)明顯優(yōu)于ConcurrentHashMap.newKeySet(),但在寫多讀少時(shí)又會(huì)明顯弱于ConcurrentHashMap.newKeySet()。
當(dāng)然使用
CopyOnWriteArraySet還需要注意一點(diǎn),寫入的數(shù)據(jù)可能不會(huì)被及時(shí)的讀取到,因?yàn)楸闅v的是讀取之前獲取的快照。
這段代碼可以測(cè)試CopyOnWriteArraySet寫入數(shù)據(jù)不能被及時(shí)讀取到的問(wèn)題。
public classCOWSetTest{
publicstaticvoidmain(String[] args)throwsInterruptedException{
CopyOnWriteArraySet<Integer> set =newCopyOnWriteArraySet();
newThread(()->{
try{
set.add(1);
System.out.println("第一個(gè)線程啟動(dòng),添加了一個(gè)元素,睡100毫秒");
Thread.sleep(100);
set.add(2);
set.add(3);
System.out.println("第一個(gè)線程添加了3個(gè)元素,執(zhí)行結(jié)束");
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
//保證讓第一個(gè)線程先執(zhí)行
Thread.sleep(1);
newThread(()->{
try{
System.out.println("第二個(gè)線程啟動(dòng)了!睡200毫秒");
//Thread.sleep(200);//如果在這邊睡眠,可以獲取到3個(gè)元素
Iterator<Integer> iterator = set.iterator();//生成快照
Thread.sleep(200);//如果在這邊睡眠,只能獲取到1個(gè)元素
while(iterator.hasNext()){
System.out.println("第二個(gè)線程開(kāi)始遍歷,獲取到元素:"+ iterator.next());
}
}catch(InterruptedException e){
e.printStackTrace();
}
}).start();
}
}
9. 異步
異步是提升系統(tǒng)響應(yīng)能力的重要手段之一,異步思想的應(yīng)用也非常的廣泛,常見(jiàn)的有:線程、MQ、事件通知、響應(yīng)式編程等方式,有些概念在前面的章節(jié)中也涉及到了,異步最核心的思想就是,先快速接收,后查詢結(jié)果,比如:如果接口處理時(shí)間較長(zhǎng),那么可以優(yōu)先響應(yīng)中間狀態(tài)(處理中),然后提供回調(diào)和查詢接口,這樣就可以大大提升接口的吞吐量!
10. for循環(huán)優(yōu)化
減少循環(huán)
通??梢酝ㄟ^(guò)一些高效的算法或者數(shù)據(jù)結(jié)構(gòu)來(lái)減少循環(huán)次數(shù),尤其當(dāng)出現(xiàn)嵌套循環(huán)時(shí)要格外小心。 常見(jiàn)的方式比如:有序的查找可以用二分,排序可以用快排,檢索可以構(gòu)建Hash索引等等。
批量獲取
優(yōu)化前:每次查詢一次數(shù)據(jù)庫(kù)
for(String userId : userIds){
User user = userMapper.queryById(userId);
if(user.getName().equals("xxx")){
// ...
}
}
優(yōu)化后:先批量查詢出來(lái),再處理
Map<String, User> userMap = userMapper.queryByIds(userIds);
for(String userId : userIds){
User user = userMap.get(userId);
if(user.getName().equals("xxx")){
// ...
}
}
緩存結(jié)果
優(yōu)化前:每次都要根據(jù)每個(gè)用戶的roleId去數(shù)據(jù)庫(kù)查詢一次。
Map<String, User> userMap = userMapper.queryByIds(userIds);
for(String userId : userIds){
User user = userMap.get(userId);
Role role = roleMapper.queryById(user.getRoleId());
}
優(yōu)化后:每次根據(jù)roleId查詢過(guò)以后就暫記下來(lái),后面再遇到相同roleId時(shí)即可直接獲取,這比較適用于一次循環(huán)中roleId重復(fù)次數(shù)較多的場(chǎng)景。
Map<String,User> userMap = userMapper.queryByIds(userIds);
Map<String,Role> roleMap =newHashMap<>();
for(String userId : userIds){
Useruser= userMap.get(userId);
Rolerole= roleMap.get(user.getRoleId());
if(role ==null){
role = roleMapper.queryById(user.getRoleId());
roleMap.put(user.getRoleId(), role);
}
}
并行處理
典型的如parallelStream
Integer sum = numbers.parallelStream().reduce(0, Integer::sum);
11. 減少網(wǎng)絡(luò)傳輸?shù)捏w積
精簡(jiǎn)字段
1.數(shù)據(jù)庫(kù)查詢時(shí)要避免頻繁查詢大文本字段,常見(jiàn)的如下面幾種:select url, describe, remark from t
2.接口傳輸時(shí)同樣要注意盡量減少內(nèi)容傳輸?shù)拇笮 ?/p>
3.精簡(jiǎn)字段除了通過(guò)減少不必要的字段傳輸之外,也可以通過(guò)改變數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)類型來(lái)實(shí)現(xiàn)。
數(shù)據(jù)傳輸格式
常用的如JSON,語(yǔ)法簡(jiǎn)單,相比XML來(lái)說(shuō)傳輸體積更小,解析更快,但如果需要頻繁傳輸大量數(shù)據(jù)時(shí),使用protobuf則更會(huì)更加高效,因?yàn)槠洳捎媒Y(jié)構(gòu)化的數(shù)據(jù)描述語(yǔ)言,并使用二進(jìn)制編碼,因?yàn)轶w積更小,速度更快。
壓縮
常見(jiàn)的數(shù)據(jù)壓縮方式如:GZIP、zlib,而zip常用于文件壓縮。
借助Hutool工具包,可以看下壓縮的效果
gzip壓縮
StringBuilder sb=newStringBuilder();
for(inti=0; i <1000; i++){
sb.append(i);
}
System.out.println("壓縮前:"+ sb.toString().getBytes().length);
byte[] compressedBytes =ZipUtil.gzip(sb.toString(),CharsetUtil.UTF_8);
System.out.println("壓縮后:"+ compressedBytes.length);
Stringstr=ZipUtil.unGzip(compressedBytes,CharsetUtil.UTF_8);
System.out.println("壓縮還原:"+ str.getBytes().length);
壓縮前:2890
壓縮后:1474
壓縮還原:2890
zlib壓縮
StringBuilder sb=newStringBuilder();
for(inti=0; i <1000; i++){
sb.append(i);
}
System.out.println("壓縮前:"+ sb.toString().getBytes().length);
byte[] compressedBytes =ZipUtil.zlib(sb.toString(),CharsetUtil.UTF_8,1);
System.out.println("壓縮后:"+ compressedBytes.length);
Stringstr=ZipUtil.unZlib(compressedBytes,CharsetUtil.UTF_8);
System.out.println("壓縮還原:"+ str.getBytes().length);
壓縮前:2890
壓縮后:1518
壓縮還原:2890
12. 減少服務(wù)之間的依賴
依賴越多,不但會(huì)給服務(wù)的穩(wěn)定性、可靠性造成影響,同時(shí)也會(huì)成為性能提升的瓶頸,因此我們?cè)谠O(shè)計(jì)之初就應(yīng)當(dāng)充分考慮到這個(gè)問(wèn)題,通過(guò)合理的手段來(lái)減少服務(wù)之間的依賴。
鏈路治理
通過(guò)合理的微服務(wù)劃分,可以有效的減少鏈路上的依賴,鏈路調(diào)用之間要避免出現(xiàn)重復(fù)調(diào)用,循環(huán)依賴,以及上、下層級(jí)互相調(diào)用的情況。
重復(fù)調(diào)用
循環(huán)依賴
服務(wù)上、下層級(jí)混亂,互相調(diào)用
數(shù)據(jù)冗余
數(shù)據(jù)冗余是指將非自身維護(hù)的數(shù)據(jù)通過(guò)某種手段保存下來(lái),以便在之后使用時(shí)避免多次發(fā)起數(shù)據(jù)請(qǐng)求,從而實(shí)現(xiàn)減少服務(wù)依賴的手段。
常見(jiàn)的方式如:通用的基礎(chǔ)數(shù)據(jù),字典數(shù)據(jù)等各個(gè)需求方可復(fù)制一份存在本地;建立寬表,冗余部分?jǐn)?shù)據(jù),減少關(guān)聯(lián)查詢。
結(jié)果緩存
將需要頻繁使用的結(jié)果存儲(chǔ)在緩存服務(wù)中,也是有效減少服務(wù)依賴的方式之一。
消息隊(duì)列
消息隊(duì)列天然就有簡(jiǎn)化系統(tǒng)復(fù)雜性的作用,它通過(guò)異步的方式將任務(wù)與任務(wù)之間的關(guān)系進(jìn)行解耦,也就達(dá)到了減少服務(wù)之間依賴的效果。
來(lái)源:juejin.cn/post/7271276760905023549
程序汪接私活項(xiàng)目目錄,2023年總結(jié)
Java項(xiàng)目分享 最新整理全集,找項(xiàng)目不累啦 07版
程序汪10萬(wàn)接的無(wú)線共享充電寶項(xiàng)目,開(kāi)發(fā)周期3個(gè)月
程序汪1萬(wàn)接的企業(yè)官網(wǎng)項(xiàng)目,開(kāi)發(fā)周期15天
程序汪8萬(wàn)接的共享口罩項(xiàng)目,開(kāi)發(fā)周期1個(gè)月
程序汪8萬(wàn)塊的飲水機(jī)物聯(lián)網(wǎng)私活項(xiàng)目經(jīng)驗(yàn)分享
程序汪接的酒店在線開(kāi)房項(xiàng)目,另外一個(gè)好聽(tīng)的名字叫智慧酒店
歡迎添加程序汪個(gè)人微信 itwang008 進(jìn)粉絲群或圍觀朋友圈
