跳轉到

Java進階:執行緒安全與同步機制

1. 引言

接續昨天的話題,今天將探討Java中的執行緒安全概念和同步機制,幫助開發者更好地理解和應用這些重要的並行程式設計技術。

2. 執行緒安全的概念

執行緒安全是多執行緒程式設計中的一個核心概念。
一個類別或方法被稱為執行緒安全的,意味著可以在多執行緒環境中正確運行,而不會產生意外的結果。

什麼是執行緒安全?

執行緒安全指的是在多執行緒環境下,程式碼能夠正確地處理共享資源,確保資料的一致性和正確性。換句話說,無論有多少執行緒同時訪問該程式碼,都能得到預期的結果。

常見的執行緒安全問題: 1. 競爭條件(Race Condition):多個執行緒同時訪問共享資源,導致不可預期的結果。

範例:

public class Counter {
    private int count = 0;
    public void increment() {
        count++; // 這不是原子操作,可能導致競爭條件
    }
}

  1. 可見性問題:一個執行緒對共享變數的修改,其他執行緒可能無法立即看到。

  2. 指令重排序:編譯器或處理器可能改變指令的執行順序,導致非預期的結果。

要解決這些問題,Java提供多種同步機制和工具,我們將在接下來的章節中詳細討論。理解執行緒安全的概念對於開發高質量的多執行緒應用程式至關重要。

3. Java中的同步機制

Java提供多種同步機制來確保執行緒安全。以下是三種最常用的同步機制:

  1. synchronized關鍵字: synchronized是Java中最基本的同步機制。它可以用於方法或程式碼區塊,確保同一時間只有一個執行緒可以執行該段程式碼。

範例:

public class SynchronizedCounter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
}

或者使用同步區塊:

public void increment() {
    synchronized(this) {
        count++;
    }
}

  1. volatile關鍵字: volatile關鍵字用於確保變數的可見性,當一個變數被宣告為volatile時,值會立即被寫入主記憶體,而不是暫存在CPU快取中。

範例:

public class VolatileExample {
    private volatile boolean flag = false;
    public void setFlag() {
        flag = true;
    }
    public boolean isFlag() {
        return flag;
    }
}

  1. 鎖(Lock)介面: Java 5引入java.util.concurrent.locks包,提供比synchronized更靈活的鎖機制。

範例:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

Lock介面提供更多的功能,如嘗試獲取鎖(tryLock())、可中斷的鎖等待等。

synchronized簡單易用,volatile適用於簡單的共享變數,而Lock則提供更高的靈活性和功能。

4. 同步集合類別

Java提供多種執行緒安全的集合類別,這些類別在多執行緒環境下可以安全地使用,無需額外的同步處理。以下是幾種常見的同步集合類別:

  1. Vector和Hashtable: 這兩個類別是Java早期提供的執行緒安全集合,所有方法都是同步的。

範例:

Vector<String> vector = new Vector<>();
vector.add("元素1");

Hashtable<String, Integer> hashtable = new Hashtable<>();
hashtable.put("鍵", 1);

然而,由於同步粒度較大,在高並發環境下可能會影響效能。

  1. Collections.synchronizedXXX方法: 這些方法可以將非同步的集合轉換為同步的集合。

範例:

List<String> synchronizedList = Collections.synchronizedList(new ArrayList<>());
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(new HashMap<>());

  1. java.util.concurrent包中的類別: Java 5引入的concurrent包提供更高效的執行緒安全集合類別。

a. ConcurrentHashMap: 比Hashtable更高效的執行緒安全Map實現。

ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("鍵", 1);

b. CopyOnWriteArrayList: 適用於讀多寫少的場景,每次寫操作都會複製整個列表。

CopyOnWriteArrayList<String> copyOnWriteList = new CopyOnWriteArrayList<>();
copyOnWriteList.add("元素");

c. ConcurrentLinkedQueue: 執行緒安全的無界隊列。

ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("元素");

5. 原子類別

Java 5引入java.util.concurrent.atomic包,提供一系列的原子類別,即不可中斷的操作,確保在多執行緒環境下的資料一致性。

常見的原子類別包括:

  1. AtomicInteger和AtomicLong: 用於整數和長整數的原子操作。

範例:

AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // 原子性地增加1並獲取新值

  1. AtomicBoolean: 用於布林值的原子操作。

範例:

AtomicBoolean flag = new AtomicBoolean(false);
flag.compareAndSet(false, true); // 如果當前值為false,則設置為true

  1. AtomicReference: 用於物件引用的原子操作。

範例:

AtomicReference<String> reference = new AtomicReference<>("初始值");
reference.set("新值");

原子類別的優勢:

  1. 無鎖操作:原子類別使用硬體級的原子指令(如比較並交換,CAS),避免使用鎖帶來的開銷。

  2. 更高的效能:在高競爭的情況下,原子類別通常比使用同步塊或鎖的方式效能更好。

  3. 避免ABA問題:某些原子類別(如AtomicStampedReference)提供版本戳記功能,可以避免ABA問題。

  4. 簡化程式碼:使用原子類別可以簡化多執行緒程式碼,減少出錯的機會。

6. 執行緒本地變數(ThreadLocal)

ThreadLocal是Java提供的一種機制,用於創建只能由同一個執行緒讀寫的變數,來確保資料在多執行緒環境中的隔離性。

ThreadLocal的使用場景:

  1. 在多執行緒環境下保存線程安全的資料,如交易ID、用戶身份等。
  2. 避免在方法間傳遞參數,特別是在框架開發中。
  3. 實現執行緒安全的單例模式。

如何使用ThreadLocal:

  1. 創建ThreadLocal物件:

    private static ThreadLocal<String> userIdHolder = new ThreadLocal<>();
    

  2. 設置值:

    userIdHolder.set("user123");
    

  3. 獲取值:

    String userId = userIdHolder.get();
    

  4. 移除值:

    userIdHolder.remove();
    

完整範例:

public class ThreadLocalExample {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            threadLocal.set("執行緒1的資料");
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set("執行緒2的資料");
            System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
    }
}

注意事項: 1. 記得在不需要時調用remove()方法,避免記憶體洩漏。 2. 避免將ThreadLocal與執行緒池一起使用,因為執行緒池中的執行緒可能被重複使用。

7. 並發工具類別

Java的java.util.concurrent包提供多種並發工具類別,用於協調多執行緒的行為。以下是三個常用的並發工具類別:

  1. CountDownLatch(倒數計數器): 允許一個或多個執行緒等待其他執行緒完成操作。

範例:

CountDownLatch latch = new CountDownLatch(3);

// 在工作執行緒中
latch.countDown(); // 完成一項工作

// 在主執行緒中
latch.await(); // 等待所有工作完成

使用場景:等待多個並行任務完成後再繼續執行。

  1. CyclicBarrier 允許多個執行緒互相等待,直到到達某個barrier point。

範例:

CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有執行緒都到barrier point"));

// 在每個執行緒中
barrier.await(); // 等待其他執行緒到達barrier point

使用場景:多執行緒計算,需要等待所有執行緒完成當前階段後再一起進入下一階段。

  1. Semaphore(信號量): 控制同時訪問某個資源的執行緒數量。

範例:

Semaphore semaphore = new Semaphore(5); // 允許5個執行緒同時訪問

semaphore.acquire(); // 獲取許可
try {
    // 訪問資源
} finally {
    semaphore.release(); // 釋放許可
}

使用場景:限制對資源的並發訪問數量,如連接池。

8. 實踐與注意事項

  1. 避免死鎖:
  2. 始終以固定的順序獲取多個鎖。
  3. 使用顯式鎖(如ReentrantLock)時,考慮使用tryLock()方法與超時機制。

範例:

public void transferMoney(Account fromAccount, Account toAccount, double amount) {
    if (fromAccount.getLock().tryLock()) {
        try {
            if (toAccount.getLock().tryLock()) {
                try {
                    // 轉帳邏輯
                } finally {
                    toAccount.getLock().unlock();
                }
            }
        } finally {
            fromAccount.getLock().unlock();
        }
    }
}

  1. 使用不可變物件: 不可變物件天生就是執行緒安全的,可以減少同步的需求。

範例:

public final class ImmutablePerson {
    private final String name;
    private final int age;

    public ImmutablePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 只提供 getter 方法
}

  1. 最小化同步範圍: 縮小同步塊的範圍可以減少執行緒競爭,提高效能。

範例:

public void addToList(String item) {
    // 只在必要的操作上使用同步
    synchronized (this) {
        list.add(item);
    }
    // 其他非同步操作...
}

  1. 優先使用並發集合: 使用java.util.concurrent包中的集合類別,而不是手動同步普通集合。

  2. 正確使用volatile: volatile適用於單個變數的讀寫操作,不適用於複合操作。

  3. 避免過度同步: 過度同步可能導致效能下降,應該在確實需要時才使用同步機制。

  4. 使用執行緒池: 優先使用執行緒池而不是直接創建執行緒,以便更好地管理系統資源。

  5. 注意執行緒安全的單例模式: 使用枚舉或靜態內部類實現執行緒安全的單例模式。

  6. 定期檢查和測試: 使用工具如Java Flight Recorder和Thread Dump分析執行緒行為,並進行壓力測試。

本篇文章同步刊載iThome: iThome
筆者個人的網站: JUNYI