博客 / 詳情

返回

化整為零、分而治之、異步編排:一文讀懂現代併發的底層心法

LongAdder:化整為零,熱點分散
在Java多線程編程中,‌原子變量(如AtomicLong)‌通過CAS操作實現線程安全的累加。然而,在高併發場景下,大量線程爭搶同一原子變量會引發嚴重的‌緩存一致性問題‌。
‌ 1)緩存行偽共享‌:多個線程頻繁更新同一緩存行,導致緩存失效和MESI協議頻繁觸發,處理器性能急劇下降。
‌ 2)CAS衝突開銷‌:CAS操作需自旋重試,線程競爭激烈時重試次數增加,進一步拖慢性能。
為解決上述瓶頸,Java 8引入了‌LongAdder‌,其核心思想是‌“分散競爭,延遲求和‌”。
‌ 1)分段累加‌:將單一累加變量拆分為多個‌分段變量(cells)‌,每個線程僅更新其專屬的分段,避免全局競爭。
‌ 2)基礎值優化‌:在低併發場景下,直接更新基礎值(base),減少分段數組的開銷。
‌ 3)最終一致性求和‌:通過遍歷所有分段和基礎值,延遲計算總和,降低實時競爭壓力。
image

LongAdder的內部實現原理,可以用下面的偽代碼演示。

public class LongAdder {
    private final AtomicLong[] cells; // 分段數組,每個線程映射到特定分段
    private final AtomicLong base;    // 基礎值,低併發時直接更新

    public LongAdder() {
        cells = new AtomicLong[16]; // 假設初始化16個分段
        for (int i = 0; i < cells.length; i++) {
            cells[i] = new AtomicLong(0); // 初始化分段值為0
        }
        base = new AtomicLong(0); // 初始化基礎值為0
    }

    public void add(long x) {
        // 1. 計算線程對應的分段索引(簡單取模實現)
        int index = (int) (Thread.currentThread().getId() % cells.length);
        // 2. 對專屬分段執行CAS累加,避免全局競爭
        cells[index].addAndGet(x);
    }

    public long sum() {
        // 3. 求和:累加基礎值和所有分段值
        long sum = base.get();
        for (AtomicLong cell : cells) {
            sum += cell.get();
        }
        return sum;
    }
}

然而,LongAdder 並非萬能鑰匙。在併發度較低的場景,AtomicLong 的簡單直接反而更高效,就好比小型聚會中,大家直接共享一個果盤更方便,而不需要多個果籃。另外,當需要頻繁讀取累計結果時,LongAdder 的彙總過程就像逐個籃子統計水果數量,略顯繁瑣,性能會受影響。Fork/Join:分而治之,任務竊取
Java 7 引入的 ‌Fork/Join Framework‌ 是一種強大的並行編程模型,專為解決“分而治之”(Divide and Conquer)類型的問題而設計。它充分利用多核處理器的計算能力,通過分解任務、並行執行和合並結果,顯著提升程序的執行效率。
Fork/Join特別適合處理可以被分解成獨立子任務的問題,以實現任務的並行執行,如排序、搜索等。
image

if (任務足夠小) {
    直接計算並返回結果;
} else {
    將任務拆分為N個子任務;
    對每個子任務調用 fork() 進行並行計算;
    調用 join() 合併子任務的結果;
    返回最終結果;
}

在Java 8中引入的並行流計算(Parallel stream computing),內部就是採用的ForkJoinPool來實現的。例如,下面使用並行流實現數組並行求和計算。

public class SumArray {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
         // map在實際編程中,可以是耗時高,計算量大的任務
        int sum = numbers.parallelStream().map(i -> i * i).reduce(0, Integer::sum);
    }
}

並行編程模型
並行編程模型(Parallel programming model)是一種用於描述和組織並行計算的框架或範式。它提供了一組抽象概念、編程接口和規範,用於指導開發者在多核處理器、分佈式系統或並行計算環境中編寫並行程序。
常見的並行編程模型包括:數據並行(Data parallelism)、任務並行(Task parallelism)、多線程並行(Multithread parallelism)等。工作竊取
工作竊取(Work Stealing)是一種高效的並行計算調度策略,其主要目標是解決負載不均衡問題,以充分利用所有的處理器核心,從而提升程序的執行效率。
在工作竊取模型中,每個處理器都維護着自己的雙端隊列(Deque),用於存儲分配給自己的任務。當一個處理器完成了所有自己的任務後,它會嘗試從其他處理器的任務隊列的末尾“竊取”任務來執行。這種策略確保了所有的處理器都能儘可能地保持忙碌狀態,從而提高整體的並行性能。
工作竊取策略的一個顯著優點是其能夠動態地平衡負載。這意味着它能夠適應各種不同的任務分佈和處理器性能,從而在各種情況下都能提供優秀的性能。
在實際編程實踐中,工作竊取調度策略通常由並行編程框架或庫來實現。例如,Java的Fork/Join框架就採用了工作竊取策略來動態地分配任務,C++的Intel TBB(Threading Building Blocks)庫也使用了類似的策略。
image

CompletableFuture:構建優雅的異步流水線
CompletableFuture 是 Java 8 引入的一個核心類,它實現了 Future 接口,並在其基礎上提供了更強大、更靈活的異步編程能力。與傳統的 Future 相比,CompletableFuture 不僅能夠表示一個異步計算的結果,還支持豐富的函數式編程特性,使得開發者能夠以聲明式的方式處理異步任務的執行流程。
CompletableFuture 的核心優勢在於其支持鏈式調用(Chaining),允許在一個 CompletableFuture 上附加多個操作(如轉換結果、處理異常、組合多個任務等)。這些操作會在 CompletableFuture 完成時自動觸發,從而形成一個流水線式的任務處理流程。
‌ 以下是一個簡單的示例,展示了 ‌CompletableFuture‌ 的鏈式調用。

// supplyAsync方法用於啓動一個異步任務,thenApply方法用於在任務完成時對結果進行轉換
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
     ......
    // 異步任務,模擬網絡請求
    return "Hello";
}).thenApply(result -> {
    // 對結果進行轉換
    return result + " World";
});
// 主線程等待異步操作完成
future.join();
// 獲取異步執行結果
future.get();

異步編程
異步編程是一種高效的編程範式,旨在優化程序的執行效率和資源利用率。它通過允許程序在等待耗時操作(如網絡請求、文件I/O等)完成的同時,繼續執行其他任務,從而避免了傳統同步編程中的線程阻塞問題。這種非阻塞的執行方式顯著提升了程序的響應速度和併發處理能力,尤其適用於I/O密集型任務。
在傳統的同步編程模型中,當一個操作需要較長時間完成時,程序會停滯在該操作上,直到操作結束,這種現象稱為阻塞。相比之下,異步編程模型允許程序在啓動一個耗時操作後,立即轉而執行其他任務,而無需等待該操作完成。當操作完成後,程序會通過回調、事件或Future等機制收到通知,並處理操作結果。
異步編程在多種編程語言中都有應用和實現。例如,Java中的CompletableFuture、JavaScript中的Promise,以及Python中的asyncio庫等,都提供了強大的異步編程支持。
以下是一個簡單的同步與異步讀取文件的對比示例(偽代碼)。

// 同步執行讀取文件操作
let a = read("a.txt");
// 同步等待上次一次文件操作完成
let b = read("b.txt");
// 假設單個文件讀取耗時50ms,一共需要耗時100ms
print(a+b)

// 異步執行讀取文件操作
let op_a = read_async("a.txt");
let op_b = read_async("b.txt");
let a = wait_until_get_ready(op_a);
let b = wait_until_get_ready(op_b);
// 假設單個文件讀取耗時50ms, 由於兩次讀取文件操作同時異步執行,最終耗時50ms
print(a+b);

fn wait_until_get_ready(Operation) -> Response {
  // 阻塞任務,掛起線程,直到operation就緒再喚醒線程(喚醒操作,需要操作系統和硬件的底層支撐)
}

異步編程可以更有效地處理併發問題。當程序需要同時處理多個任務時,異步編程可以讓這些任務併發執行,而不是按順序一個接一個地執行。然而,異步編程的實現離不開操作系統和硬件的底層支持。如果在異步任務執行完之前,處理線程一直對異步任務執行狀態進行空輪詢,這將會浪費處理器資源。因此,操作系統和硬件的底層支持,如Linux系統支持的select、poll、epoll等技術,對於異步編程的實現至關重要。
以下是一個沒有底層支持時,異步編程可能面臨的問題的示例(偽代碼)。

let op_a = read_async("a.txt");
let a = "";
// 如果沒有操作系統和硬件的底層支撐,將不斷輪詢op_a的任務狀態
while true {
  if op_a.is_finish() {
    a = op_a.get_content();
    break;
  }
}
print(a);

鏈式調用
“回調地獄”(Callback Hell)是異步編程中常見的一個問題,尤其在 JavaScript 等單線程、事件驅動型語言中尤為突出。當多個異步操作需要按照特定順序執行時,開發者可能不得不在一個回調函數中嵌套另一個回調函數,導致代碼層級過深、可讀性差、難以維護。
以下是一個典型的“回調地獄”示例(JavaScript)。

login(user => {
    getStatus(status => {
        getOrder(order => {
            getPayment(payment => {
                getRecommendAdvertisements(ads => {
                    setTimeout(() => {
                        alert(ads)
                    }, 1000)
                })
            })
        })
    })
})

鏈式調用(Chaining)成為一種常用的優化手段。在 JavaScript 中,‌Promise‌ 機制提供了鏈式調用的能力。每個 ‌Promise‌ 對象都包含一個 ‌then‌ 方法,該方法返回一個新的 ‌Promise‌ 對象,從而允許開發者將多個異步操作串聯起來,形成清晰的邏輯流。
以下是一個使用 ‌Promise‌ 鏈式調用的示例(JavaScript)。

login(username, password)
    .then(user => getStatus(user.id))
    .then(status => getOrder(status.id))
    .then(order => getPayment(order.id))
    .then(payment => getRecommendAdvertisements(payment.total_amount))
    .then(ads => {/*...*/});

需要注意的是,‌Promise‌ 並不是一種可以將同步代碼轉變為異步代碼的魔法工具。它只是一種編程手法,或者説是一種封裝方式,並沒有藉助操作系統的額外能力。‌Promise‌ 的主要作用是提供了一種更優雅的方式來組織和管理異步操作,使得代碼更易於閲讀和理解。

總結:與硬件共舞,與衝突和解
Disruptor的環形緩衝、LongAdder的分散熱點、Fork/Join的工作竊取、CompletableFuture的異步編排——這些看似迥異的技術,實則殊途同歸。它們共同揭示了現代併發設計的核心要義:與其在衝突發生後被動地加鎖仲裁,不如在設計之初就主動地消除衝突。
這標誌着併發編程的關注點,已從“如何正確加鎖”的戰術層面,昇華為“如何精妙分工、避免鎖”的戰略高度。
當摩爾定律的紅利逐漸消退,真正的性能突破不再依賴於硬件的暴力堆砌,而是源於軟件層面與硬件底層機制的“共舞”。無論是利用緩存行特性的內存對齊,還是藉助CAS原子指令的樂觀更新,每一次技術優化都印證了一個理念:硬件性能的極致發揮,源於對計算本質的深刻理解。這正是併發編程從技術邁向藝術的精髓所在。

很高興與你相遇!如果你喜歡本文內容,記得關注哦!!!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.