2025-12-14 JAVA并發(fā)常用概念

阿里巴巴研發(fā)手冊(cè)-并發(fā)處理要求

1.【強(qiáng)制】獲取單例對(duì)象需要保證線程安全,其中的方法也要保證線程安全。
說(shuō)明:資源驅(qū)動(dòng)類、工具類、單例工廠類都需要注意。

2.【強(qiáng)制】創(chuàng)建線程或線程池時(shí)請(qǐng)指定有意義的線程名稱,方便出錯(cuò)時(shí)回溯。

3.【強(qiáng)制】線程資源必須通過(guò)線程池提供,不允許在應(yīng)用中自行顯式創(chuàng)建線程。
說(shuō)明:線程池的好處是減少在創(chuàng)建和銷毀線程上所消耗的時(shí)間以及系統(tǒng)資源的開銷,解決資源不足的問(wèn)題。
如果不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導(dǎo)致消耗完內(nèi)存或者“過(guò)度切換”的問(wèn)題。

4.【強(qiáng)制】線程池不允許使用 Executors 去創(chuàng)建,而是通過(guò) ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則,規(guī)避資源耗盡的風(fēng)險(xiǎn)。
說(shuō)明:Executors 返回的線程池對(duì)象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允許的請(qǐng)求隊(duì)列長(zhǎng)度為 Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求,從而導(dǎo)致 OOM。
2)CachedThreadPool:
允許的創(chuàng)建線程數(shù)量為 Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程,從而導(dǎo)致 OOM。

5.【強(qiáng)制】SimpleDateFormat是線程不安全的類,一般不要定義為static變量,如果定義為static,必須加鎖,或者使用DateUtils工具類。

6.【強(qiáng)制】必須回收自定義的 ThreadLocal 變量,尤其在線程池場(chǎng)景下,線程經(jīng)常會(huì)被復(fù)用,如果不清理自定義的 ThreadLocal 變量,可能會(huì)影響后續(xù)業(yè)務(wù)邏輯和造成內(nèi)存泄露等問(wèn)題。盡量在代理中使用 try-finally 塊進(jìn)行回收。

7.【強(qiáng)制】高并發(fā)時(shí),同步調(diào)用應(yīng)該去考量鎖的性能損耗。能用無(wú)鎖數(shù)據(jù)結(jié)構(gòu),就不要用鎖;能鎖區(qū)塊,就不要鎖整個(gè)方法體;能用對(duì)象鎖,就不要用類鎖。
說(shuō)明:盡可能使加鎖的代碼塊工作量盡可能的小,避免在鎖代碼塊中調(diào)用 RPC 方法。

8.【強(qiáng)制】對(duì)多個(gè)資源、數(shù)據(jù)庫(kù)表、對(duì)象同時(shí)加鎖時(shí),需要保持一致的加鎖順序,否則可能會(huì)造成死鎖。
說(shuō)明:線程一需要對(duì)表 A、B、C 依次全部加鎖后才可以進(jìn)行更新操作,那么線程二的加鎖順序也必須是 A、 B、C,否則可能出現(xiàn)死鎖。

9.【強(qiáng)制】在使用阻塞等待獲取鎖的方式中,必須在 try 代碼塊之外,并且在加鎖方法與 try 代碼塊之間沒(méi)有任何可能拋出異常的方法調(diào)用,避免加鎖成功后,在 finally 中無(wú)法解鎖。
說(shuō)明一:如果在 lock 方法與 try 代碼塊之間的方法調(diào)用拋出異常,那么無(wú)法解鎖,造成其它線程無(wú)法成功 獲取鎖。
說(shuō)明二:如果 lock 方法在 try 代碼塊之內(nèi),可能由于其它方法拋出異常,導(dǎo)致在 finally 代碼塊中unlock 對(duì)未加鎖的對(duì)象解鎖,它會(huì)調(diào)用 AQS 的 tryRelease 方法(取決于具體實(shí)現(xiàn)類),拋出 IllegalMonitorStateException 異常。
說(shuō)明三:在 Lock 對(duì)象的 lock 方法實(shí)現(xiàn)中可能拋出 unchecked 異常,產(chǎn)生的后果與說(shuō)明二相同。

10.【強(qiáng)制】在使用嘗試機(jī)制來(lái)獲取鎖的方式中,進(jìn)入業(yè)務(wù)代碼塊之前,必須先判斷當(dāng)前線程是否持有鎖。鎖的釋放規(guī)則與鎖的阻塞等待方式相同。
說(shuō)明:Lock 對(duì)象的 unlock 方法在執(zhí)行時(shí),它會(huì)調(diào)用 AQS 的 tryRelease方法(取決于具體實(shí)現(xiàn)類),如果當(dāng)前線程不持有鎖,則拋出 IllegalMonitorStateException 異常。

11.【強(qiáng)制】并發(fā)修改同一記錄時(shí),避免更新丟失,需要加鎖。要么在應(yīng)用層加鎖,要么在緩存加鎖,要么在數(shù)據(jù)庫(kù)層使用樂(lè)觀鎖,使用 version 作為更新依據(jù)。
說(shuō)明:如果每次訪問(wèn)沖突概率小于 20%,推薦使用樂(lè)觀鎖,否則使用悲觀鎖。樂(lè)觀鎖的重試次數(shù)不得小于 3 次。

12.【強(qiáng)制】多線程并行處理定時(shí)任務(wù)時(shí),Timer 運(yùn)行多個(gè) TimeTask 時(shí),只要其中之一沒(méi)有捕獲拋出的異常,其它任務(wù)便會(huì)自動(dòng)終止運(yùn)行,使用 ScheduledExecutorService 則沒(méi)有這個(gè)問(wèn)題。

13.【推薦】資金相關(guān)的金融敏感信息,使用悲觀鎖策略。
說(shuō)明:樂(lè)觀鎖在獲得鎖的同時(shí)已經(jīng)完成了更新操作,校驗(yàn)邏輯容易出現(xiàn)漏洞,另外,樂(lè)觀鎖對(duì)沖突的解決策略有較復(fù)雜的要求,處理不當(dāng)容易造成系統(tǒng)壓力或數(shù)據(jù)異常,所以資金相關(guān)的金融敏感信息不建議使用樂(lè)觀鎖更新。
正例:悲觀鎖遵循一鎖、二判、三更新、四釋放的原則。

面試官的問(wèn)題:
多線程編程中,CompletableFuture和傳統(tǒng)Future的區(qū)別是什么?如何避免線程池的OOM風(fēng)險(xiǎn)?
CompletableFuture 與傳統(tǒng) Future 的區(qū)別(基于 Java 8)-ds - 知乎 (zhihu.com)

代碼示例:

Executor執(zhí)行的任務(wù)有四個(gè)生命周期階段:創(chuàng)建、提交、開始和完成。

import com.google.common.collect.Lists;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.List;
import java.util.concurrent.*;

public class FutureTest {
    private static String test1() {
        return "test1";
    }


    private static String test2() {
        return "test2";
    }


    private static String test3() {
        return "test3";
    }


    private static String test4() {
        return "test4";
    }


    private static void test5() {
        System.out.println("test5");
    }


    private static void test6() {
        System.out.println("test6");
    }


    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 10,
                TimeUnit.SECONDS, new LinkedBlockingDeque<>(20), new ThreadFactoryBuilder().setNameFormat("測(cè)試-testThread").build());


        List<String> resultA = Lists.newArrayList();
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            return test1();
        }, threadPoolExecutor).whenComplete((x, e) -> {
            resultA.add(x);
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            return test2();
        }, threadPoolExecutor).whenComplete((x, e) -> {
            resultA.add(x);
        });


        try {
            CompletableFuture.allOf(future1, future2).get(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        System.out.println(resultA);


        List<Future> futureList = Lists.newArrayList();

        Future<String> futureTest3 = threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return test3();
            }
        });
        futureList.add(futureTest3);


        Future<String> futureTest4 = threadPoolExecutor.submit(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return test4();
            }
        });
        futureList.add(futureTest4);

        for (Future future : futureList) {
            try {
                String result = (String) future.get(2, TimeUnit.SECONDS);
                System.out.println(result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }


        }


        CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                test5();
            }
        }, threadPoolExecutor);

        CompletableFuture.runAsync(new Runnable() {
            @Override
            public void run() {
                test6();
            }
        }, threadPoolExecutor);
    }
}

對(duì)象的共享

volatile
可見(jiàn)行,禁止指令重排序。
加鎖的含義不僅僅局限于互斥行為,還包括內(nèi)存可見(jiàn)性。為了確保所有線程都能看到共享變量的最新值,所有執(zhí)行讀操作或者寫操作的線程都必須在同一個(gè)鎖上同步。
當(dāng)且僅當(dāng)滿足以下所有條件時(shí),才應(yīng)該使用volatile變量:

  • 對(duì)變量的寫入操作不依賴變量的當(dāng)前值,或者你能確保只有單個(gè)線程更新變量的值。
  • 該變量不會(huì)與其他狀態(tài)變量一起納入比變性條件中;
  • 在訪問(wèn)變量時(shí)不需要加鎖。
直接使用值
volatile boolean asleep;
while(!asleep){
    countSomeSheep();
}

volatile的語(yǔ)義不足以確保遞增操作(count++)的原子性

ThreadLocal
這個(gè)類能使線程中的某個(gè)值與保存值的線程關(guān)聯(lián)起來(lái)。ThreadLocal提供了get與set等訪問(wèn)接口或者方法,這些方法為每個(gè)使用該變量的線程都存有一份獨(dú)立的副本,因此get總是返回由當(dāng)前執(zhí)行線程在調(diào)用set時(shí)設(shè)置的最新值。
使每個(gè)線程都可以擁有某個(gè)變量的一個(gè)私有“版本”。
示例場(chǎng)景:
1.當(dāng)某個(gè)頻繁執(zhí)行的操作需要一個(gè)臨時(shí)對(duì)象,例如一個(gè)緩沖區(qū),而同時(shí)又希望避免在每次執(zhí)行時(shí)都重新分配該臨時(shí)對(duì)象,就可以使用這項(xiàng)技術(shù)。
2.一些執(zhí)行上下文與某個(gè)執(zhí)行中的線程關(guān)聯(lián)起來(lái)。

線程封閉:線程封閉的對(duì)象只能由一個(gè)線程擁有,對(duì)象被封閉在該線程中,并且只能由這個(gè)線程修改。
只讀共享:在沒(méi)有額外同步的情況下,共享的只讀對(duì)象可以由多個(gè)線程并發(fā)訪問(wèn),但任何線程都不能修改它。共享的只讀對(duì)象包括不可變對(duì)象和事實(shí)不可變對(duì)象。
線程安全共享:線程安全的對(duì)象在其內(nèi)部實(shí)現(xiàn)同步,因此多個(gè)線程可以通過(guò)對(duì)對(duì)象的公有接口來(lái)進(jìn)行訪問(wèn)而不需要進(jìn)一步的同步。
保護(hù)對(duì)象:被保護(hù)的對(duì)象只能通過(guò)持有特定的鎖來(lái)訪問(wèn),保護(hù)對(duì)象包括封裝在其他線程安全對(duì)象中的對(duì)象,以及已發(fā)布的并且由某個(gè)特定鎖保護(hù)的對(duì)象。

并發(fā)容器
一些復(fù)合操作,例如“若沒(méi)有則添加”,“若相等則移除”,“若相等則替換”等,都已經(jīng)實(shí)現(xiàn)為原子操作并在ConcurrentMap的接口中聲明。如果需要在現(xiàn)有的同步Map中添加這樣的功能,那么很多可能就意味著應(yīng)該考慮使用ConcurrentMap了。

線程取消與關(guān)閉

通常,中斷是實(shí)現(xiàn)取消的最合理方式。

每當(dāng)調(diào)用另一個(gè)方法時(shí),都要對(duì)它的行為保持懷疑,不要盲目地認(rèn)為它一定會(huì)正常返回,或者一定會(huì)拋出在方法原型中聲明的某個(gè)已檢查異常。對(duì)調(diào)用的代碼越不熟悉,就越應(yīng)該對(duì)其代碼行為保持懷疑。

性能與可伸縮性

  • 可伸縮性:當(dāng)增加計(jì)算資源時(shí)(例如CPU、內(nèi)存、存儲(chǔ)容量或者I/O帶寬),程序的吞吐量或者處理能力相應(yīng)地增加。
  • 避免不成熟的優(yōu)化。首先使程序正確,然后再提高運(yùn)行速度——如果它還運(yùn)行得不夠快。
  • 以測(cè)試為基準(zhǔn),不要猜測(cè)。
  • 對(duì)性能的調(diào)優(yōu),一定要有明確的性能要求。

在使用某個(gè)方法比其他方案“更快”之前,首先問(wèn)自己一些問(wèn)題:

  • “更快”的含義是什么?
  • 該方案在什么條件下運(yùn)行得更快?在低負(fù)載還是高負(fù)載的情況下?大數(shù)據(jù)集還是小數(shù)據(jù)集?能否通過(guò)測(cè)試結(jié)果來(lái)驗(yàn)證你的答案?
  • 這些條件在運(yùn)行環(huán)境中的發(fā)生頻率?能否通過(guò)測(cè)試結(jié)果來(lái)驗(yàn)證你的答案?
  • 在其他不同條件的環(huán)境中能否使用這里的代碼?
  • 在實(shí)現(xiàn)這種性能提升時(shí)需要付出哪些隱含的代價(jià),例如增加開發(fā)風(fēng)險(xiǎn)或維護(hù)開銷?這種權(quán)衡是否合適?

減少鎖的競(jìng)爭(zhēng):

  1. 縮小鎖的范圍(快進(jìn)快出)
  2. 減小鎖的粒度
  3. 鎖分段
  4. 避免熱點(diǎn)域
  5. 一些替代獨(dú)占鎖的方法
    (采用非獨(dú)占的鎖或非阻塞鎖來(lái)代替獨(dú)占鎖)

CAS:比較并交換
ABA問(wèn)題:V值從首先由A變成B,再由B變成A。

Happens-Before
A和B之間必須滿足happens-before關(guān)系。如果兩個(gè)操作之間缺乏happens-before關(guān)系,那么JVM可以對(duì)它們?nèi)我獾刂嘏判颉?br> happens-before的規(guī)則:
1.程序順序規(guī)則:如果程序中操作A在操作B之前,那么在線程中A操作將在B操作之前執(zhí)行。
2.監(jiān)視器鎖規(guī)則:在監(jiān)視器鎖上的解鎖操作必須在同一個(gè)監(jiān)視器鎖上的加鎖操作之前執(zhí)行。
3.volatile變量規(guī)則
4.線程啟動(dòng)規(guī)則:在線程上Thread.start()的調(diào)用必須在該線程中執(zhí)行任何操作之前執(zhí)行。
5.線程結(jié)束規(guī)則:線程中的任何操作都必須在其他線程檢測(cè)到該線程已經(jīng)結(jié)束之前執(zhí)行,或者從Thread.join()中成功返回,或者在調(diào)用Thread.isAlive()時(shí)返回false
6.中斷規(guī)則:當(dāng)一個(gè)線程在另一個(gè)線程上調(diào)用interrupt時(shí),必須在被中斷線程檢測(cè)到interrupt調(diào)用之前執(zhí)行。
7.終結(jié)器規(guī)則:對(duì)象的構(gòu)造函數(shù)必須在啟動(dòng)該對(duì)象的終結(jié)器之前執(zhí)行完成。
8.傳遞性:如果操作A在操作B之前執(zhí)行,并且操作B在操作C之前執(zhí)行,那么操作A必須在操作C之前執(zhí)行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容