面試突擊37:執行緒安全問題的解決方案有哪些?

執行緒安全是指某個方法或某段程式碼,在多執行緒中能夠正確的執行,不會出現數據不一致或數據污染的情況,我們把這樣的程式稱之為執行緒安全的,反之則為非執行緒安全的。在 Java 中,解決執行緒安全問題有以下 3 種手段:

  1. 使用執行緒安全類,比如 AtomicInteger。
  2. 加鎖排隊執行
    1. 使用 synchronized 加鎖。
    2. 使用 ReentrantLock 加鎖。
  3. 使用執行緒本地變數 ThreadLocal。

接下來我們逐個來看它們的實現。

執行緒安全問題演示

我們創建一個變數 number 等於 0,之後創建執行緒 1,執行 100 萬次 ++ 操作,同時再創建執行緒 2 執行 100 萬次 — 操作,等執行緒 1 和執行緒 2 都執行完之後,列印 number 變數的值,如果列印的結果為 0,則說明是執行緒安全的,否則則為非執行緒安全的,示例程式碼如下:

public class ThreadSafeTest {
    // 全局變數
    private static int number = 0;
    // 循環次數(100W)
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1:執行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                number++;
            }
        });
        t1.start();

        // 執行緒2:執行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                number--;
            }
        });
        t2.start();

        // 等待執行緒 1 和執行緒 2,執行完,列印 number 最終的結果
        t1.join();
        t2.join();
        System.out.println("number 最終結果:" + number);
    }
}

以上程式的執行結果如下圖所示:
image.png
從上述執行結果可以看出,number 變數最終的結果並不是 0,和預期的正確結果不相符,這就是多執行緒中的執行緒安全問題。

解決執行緒安全問題

1.原子類AtomicInteger

AtomicInteger 是執行緒安全的類,使用它可以將 ++ 操作和 — 操作,變成一個原子性操作,這樣就能解決非執行緒安全的問題了,如下程式碼所示:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerExample {
    // 創建 AtomicInteger
    private static AtomicInteger number = new AtomicInteger(0);
    // 循環次數
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1:執行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // ++ 操作
                number.incrementAndGet();
            }
        });
        t1.start();

        // 執行緒2:執行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // -- 操作
                number.decrementAndGet();
            }
        });
        t2.start();

        // 等待執行緒 1 和執行緒 2,執行完,列印 number 最終的結果
        t1.join();
        t2.join();
        System.out.println("最終結果:" + number.get());
    }
}

以上程式的執行結果如下圖所示:
image.png

2.加鎖排隊執行

Java 中有兩種鎖:synchronized 同步鎖和 ReentrantLock 可重入鎖。

2.1 同步鎖synchronized

synchronized 是 JVM 層面實現的自動加鎖和自動釋放鎖的同步鎖,它的實現程式碼如下:

public class SynchronizedExample {
    // 全局變數
    private static int number = 0;
    // 循環次數(100W)
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1:執行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // 加鎖排隊執行
                synchronized (SynchronizedExample.class) {
                    number++;
                }
            }
        });
        t1.start();

        // 執行緒2:執行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                // 加鎖排隊執行
                synchronized (SynchronizedExample.class) {
                    number--;
                }
            }
        });
        t2.start();

        // 等待執行緒 1 和執行緒 2,執行完,列印 number 最終的結果
        t1.join();
        t2.join();
        System.out.println("number 最終結果:" + number);
    }
}

以上程式的執行結果如下圖所示:
image.png

2.2 可重入鎖ReentrantLock

ReentrantLock 可重入鎖需要程式設計師自己加鎖和釋放鎖,它的實現程式碼如下:

import java.util.concurrent.locks.ReentrantLock;

/**
 * 使用 ReentrantLock 解決非執行緒安全問題
 */
public class ReentrantLockExample {
    // 全局變數
    private static int number = 0;
    // 循環次數(100W)
    private static final int COUNT = 1_000_000;
    // 創建 ReentrantLock
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1:執行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                lock.lock();    // 手動加鎖
                number++;       // ++ 操作
                lock.unlock();  // 手動釋放鎖
            }
        });
        t1.start();

        // 執行緒2:執行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < COUNT; i++) {
                lock.lock();    // 手動加鎖
                number--;       // -- 操作
                lock.unlock();  // 手動釋放鎖
            }
        });
        t2.start();

        // 等待執行緒 1 和執行緒 2,執行完,列印 number 最終的結果
        t1.join();
        t2.join();
        System.out.println("number 最終結果:" + number);
    }
}

以上程式的執行結果如下圖所示:
image.png

3.執行緒本地變數ThreadLocal

使用 ThreadLocal 執行緒本地變數也可以解決執行緒安全問題,它是給每個執行緒獨自創建了一份屬於自己的私有變數,不同的執行緒操作的是不同的變數,所以也不會存在非執行緒安全的問題,它的實現程式碼如下:

public class ThreadSafeExample {
    // 創建 ThreadLocal(設置每個執行緒中的初始值為 0)
    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
    // 全局變數
    private static int number = 0;
    // 循環次數(100W)
    private static final int COUNT = 1_000_000;

    public static void main(String[] args) throws InterruptedException {
        // 執行緒1:執行 100W 次 ++ 操作
        Thread t1 = new Thread(() -> {
            try {
                for (int i = 0; i < COUNT; i++) {
                    // ++ 操作
                    threadLocal.set(threadLocal.get() + 1);
                }
                // 將 ThreadLocal 中的值進行累加
                number += threadLocal.get();
            } finally {
                threadLocal.remove(); // 清除資源,防止記憶體溢出
            }
        });
        t1.start();

        // 執行緒2:執行 100W 次 -- 操作
        Thread t2 = new Thread(() -> {
            try {
                for (int i = 0; i < COUNT; i++) {
                    // -- 操作
                    threadLocal.set(threadLocal.get() - 1);
                }
                // 將 ThreadLocal 中的值進行累加
                number += threadLocal.get();
            } finally {
                threadLocal.remove(); // 清除資源,防止記憶體溢出
            }
        });
        t2.start();

        // 等待執行緒 1 和執行緒 2,執行完,列印 number 最終的結果
        t1.join();
        t2.join();
        System.out.println("最終結果:" + number);
    }
}

以上程式的執行結果如下圖所示:
image.png

總結

在 Java 中,解決執行緒安全問題的手段有 3 種:1.使用執行緒安全的類,如 AtomicInteger 類;2.使用鎖 synchronized 或 ReentrantLock 加鎖排隊執行;3.使用執行緒本地變數 ThreadLocal 來處理。

是非審之於己,毀譽聽之於人,得失安之於數。

公眾號:Java面試真題解析

面試合集://gitee.com/mydb/interview