線程安全是指某個方法或某段代碼,在多線程中能夠正确的執行,不會出現資料不一緻或資料污染的情況,我們把這樣的程式稱之為線程安全的,反之則為非線程安全的。在 Java 中,解決線程安全問題有以下 3 種手段:
- 使用線程安全類,比如 AtomicInteger。
- 加鎖排隊執行
- 使用 synchronized 加鎖。
- 使用 ReentrantLock 加鎖。
- 使用線程本地變量 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);
}
}
以上程式的執行結果如下圖所示:
從上述執行結果可以看出,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());
}
}
以上程式的執行結果如下圖所示:
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);
}
}
以上程式的執行結果如下圖所示:
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);
}
}
以上程式的執行結果如下圖所示:
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);
}
}
以上程式的執行結果如下圖所示:
總結
在 Java 中,解決線程安全問題的手段有 3 種:1.使用線程安全的類,如 AtomicInteger 類;2.使用鎖 synchronized 或 ReentrantLock 加鎖排隊執行;3.使用線程本地變量 ThreadLocal 來處理。
是非審之于己,毀譽聽之于人,得失安之于數。