CodeGym /課程 /JAVA 25 SELF /AtomicInteger、AtomicReference:原子操作

AtomicInteger、AtomicReference:原子操作

JAVA 25 SELF
等級 53 , 課堂 3
開放

1. 為什麼 i++ 在多執行緒中不可靠?

先從經典問題開始:我們有一個計數器變數,例如已處理的請求數或已下載的檔案數。我們希望多個執行緒一起遞增這個計數器。如果只是寫下 i++,會出什麼問題?

範例:遞增時的資料競爭

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // 非原子!
    }
}

想像有兩個執行緒同時呼叫 increment()。兩個執行緒都讀到舊值、都把它加 1,然後都寫回去……寫成一樣的新值!結果就是有一次遞增「遺失」了。如果反覆多次,最後的結果會小於預期。

為什麼會這樣?
操作 i++ 其實包含三個步驟:

  1. 讀取變數的值(例如,5)。
  2. 將此值加上 1
  3. 把新值寫回記憶體。

在多執行緒環境中,其他執行緒可能在這些步驟之間改變了變數。結果就是「資料競爭」(race condition)。

什麼是原子操作?

原子操作指的是要麼完整執行、要麼完全不執行,且沒有其他執行緒能在操作進行的中途「插入」。

在 Java 中有一組類別,為原生型別與參考提供這些操作。它們位於套件 java.util.concurrent.atomic。最常見的有:

  • AtomicInteger — 原子的整數型別。
  • AtomicLong — 原子的 long
  • AtomicBoolean — 原子的 boolean
  • AtomicReference<T> — 指向任意型別物件的原子參考。

2. AtomicInteger:執行緒安全的計數器

宣告與基本用法

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // 原子遞增
    }

    public int get() {
        return count.get();
    }
}

這裡 incrementAndGet() 會把「遞增並回傳新值」當作一個不可分割的操作來執行。即使有 100 個執行緒同時呼叫這個方法,也不會遺失任何一次遞增。

常用方法:

方法 說明
get()
取得目前值
set(int value)
設定值
incrementAndGet()
加 1 並回傳新值
getAndIncrement()
先回傳目前值,再加 1
addAndGet(int delta)
加上 delta 並回傳新值
compareAndSet(expect, update)
若目前值等於 expect,則設為 update(CAS)

範例:多執行緒計數器

假設我們有個類別用來統計聊天中處理過的訊息數量。

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("訊息總數: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

內部機制:AtomicInteger 如何運作?

在內部,AtomicInteger 使用處理器的特殊指令 — CAS(Compare-And-Swap,「比較並交換」)。這是一種原子操作:它會將變數的目前值與預期值做比較,若相符便寫入新值;如果在此期間其他執行緒改變了變數,操作就不會生效,並會重試。

工作流程:

1. 讀取目前值(例如,5)
2. 與預期值比較(5)
3. 若相符 — 寫入新值(6)
4. 若不相符 — 重新嘗試

以上過程非常快速且無鎖(lock‑free)。因此原子類別常常比 synchronized 更快,尤其在執行緒很多時。

3. AtomicReference:物件的原子參考

AtomicReference<T> 是可包裝任何物件的通用原子容器。它允許從多個執行緒安全地變更對該物件的參考。

範例:執行緒安全地更新參考

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

compareAndSet 的用法

最有趣的操作是 compareAndSet(expected, newValue)。它允許只在值自上次讀取以來沒有改變時才更新。

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("更新成功!");
    } else {
        System.out.println("已有人修改值,請再試一次。");
    }
}

這是非阻塞演算法的基礎:從佇列與堆疊到快取,當你想避免不必要的鎖時尤其重要。

4. 在應用中的使用範例

範例 1:多執行緒訊息計數器

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... 處理訊息 ...
        int count = messageCount.incrementAndGet();
        System.out.println("新訊息: " + message + "。訊息總數: " + count);
    }
}

範例 2:安全更新最後一則訊息的參考

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... 處理 ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

如果需要只在最後一則訊息沒有改變時才更新參考(避免同時更新導致的「丟失」),請使用 compareAndSet

5. 限制與陷阱

原子類別並非萬靈藥:何時不適用?

原子變數非常適合簡單操作:遞增、設定值、比較與替換。但若需要同時更新多個變數,就無法保證原子性。比如你有兩個計數器,想把它們視為一個操作一併增加——此時需要 synchronized 或其他同步機制。

錯誤用法範例

// 非原子!
if (ref.get() == null) {
    ref.set("Hello");
}

get()set(...) 之間,其他執行緒可能已經改變了值,使得條件不再成立。遇到這種情況請使用 compareAndSet

原子類別 ≠ 執行緒安全的物件

如果 AtomicReference 指向的物件本身不是執行緒安全的,那麼替換參考雖然是原子的,但修改該物件欄位並非如此。比如在 AtomicReference<List<String>> 中放的是一般的 ArrayList,清單本身不會因此變成執行緒安全。

6. 進階原子類別

在套件 java.util.concurrent.atomic 中還有其他實用類別:

  • AtomicLongAtomicBoolean — 用於 longboolean
  • AtomicIntegerArrayAtomicReferenceArray — 對陣列進行原子操作。
  • LongAdderLongAccumulator — 適合高負載的計數器。

LongAdder 與 LongAccumulator

如果執行緒非常多,而一般的 AtomicInteger 變成「瓶頸」(所有執行緒都競爭同一個變數),請使用 LongAdder。它會把計數器拆分為多個內部儲存格,查詢值時再彙總,能在高競爭下帶來效能提升。

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. 使用原子變數時的常見錯誤

錯誤 1:期待複合操作具有原子性。
若需要對某個值執行多個步驟,原子類別並不能保證整體原子性——在步驟之間其他執行緒可能更改資料。對於複合操作,請使用 compareAndSet 或同步機制。

錯誤 2:忽視巢狀物件的執行緒安全。
即便 AtomicReference 中放著一般物件,只有替換參考是原子的;該物件的方法與欄位並不會因此變得執行緒安全。原子的是「參考的替換」。

錯誤 3:不必要地使用原子類別。
在單執行緒程式碼中,原子型別是多餘的,且因為附帶檢查會比一般變數稍慢。

錯誤 4:過早最佳化。
有時使用 synchronized 更簡單且更可靠,尤其當邏輯複雜且同時涉及多個變數時。並非所有情況都值得構建無鎖(lock‑free)方案。

錯誤 5:忽略 ABA 問題。
較少見但重要的情況:值從 A 變為 B 又變回 A —— compareAndSet 會「以為」什麼都沒改變。這類情境可以使用 AtomicStampedReference(或 AtomicMarkableReference)等特殊類別。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION