到底什么是線程安全? 如何保證線程安全?
隨著硬件技術(shù)的快速發(fā)展(比如多核處理器,超線程技術(shù)),我們通常會(huì)在代碼中使用多線程(比如線程池)來(lái)提高性能,但是,多線程又會(huì)帶來(lái)線程安全問(wèn)題。因此,本文將深入探討Java中的線程安全問(wèn)題。

1.什么是線程安全?
首先,我們來(lái)看看維基百科對(duì)線程安全是如何描述的,如下圖:

總結(jié)一下:線程安全(Thread Safety)是指多個(gè)線程訪問(wèn)共享資源時(shí),不會(huì)破壞資源的完整性。如下圖:

請(qǐng)注意,導(dǎo)致線程安全問(wèn)題一定要同時(shí)具備以下 3個(gè)條件,缺一不可:
- 多線程環(huán)境:如果是單線程,程序肯定會(huì)串行順序執(zhí)行,不可能出現(xiàn)線程安全問(wèn)題。
- 操作共享資源:所謂共享資源是指多個(gè)線程或進(jìn)程可以同時(shí)訪問(wèn)和使用的資源。如果每個(gè)線程都是操作自己的局部變量,盡管滿足條件1,但也不會(huì)出現(xiàn)線程安全問(wèn)題。
- 至少存在一個(gè)寫操作:如果是多線程讀取共享資源,盡管滿足了前 2個(gè)條件,但是讀操作天然是冪等的,因此也不會(huì)出現(xiàn)線程安全的問(wèn)題,所以線程中至少存在一個(gè)寫操作。
上面從表象上說(shuō)明線程安全需要具備的 3個(gè)條件,在 Java中,線程安全性通常涉及以下 3個(gè)指標(biāo):
- 原子性(Atomicity):操作要么全部完成,要么全部不完成。
- 可見(jiàn)性(Visibility):一個(gè)線程對(duì)共享變量的修改對(duì)其他線程是立即可見(jiàn)的。
- 有序性(Ordering):程序的執(zhí)行順序符合預(yù)期,不會(huì)因?yàn)榫幾g器優(yōu)化或CPU重排序而改變。
2. 產(chǎn)生線程安全的根因
在 Java中,造成線程安全問(wèn)題的根因是硬件結(jié)構(gòu),為了消除 CPU和主內(nèi)存之間的硬件速度差,通常會(huì)在兩者之間設(shè)置多級(jí)緩存(L1 ~ L3),如下圖:

Java為了適配這種多級(jí)緩存的硬件構(gòu)造,設(shè)計(jì)了一套與之對(duì)應(yīng)的內(nèi)存模型(JMM,Java memory model,包括主內(nèi)存和工作內(nèi)存,如下圖:

- 主內(nèi)存:所有的變量都存儲(chǔ)在主內(nèi)存中。
- 工作內(nèi)存:每個(gè)線程都有自己的工作內(nèi)存,會(huì)將主內(nèi)存的共享變量復(fù)制到自己的工作內(nèi)存中,然后做后續(xù)業(yè)務(wù)操作,最終再將工作內(nèi)存中的變量刷新到主內(nèi)存。
線程對(duì)變量的所有操作(讀取、寫入)都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存中的變量。線程之間無(wú)法直接訪問(wèn)對(duì)方的工作內(nèi)存,變量的傳遞需要通過(guò)主內(nèi)存來(lái)完成。
關(guān)于 Java內(nèi)存模型的原理,我們會(huì)在另外的文章中單獨(dú)講解,本文只是概要性的總結(jié)。
3. 原子性
在數(shù)據(jù)庫(kù)事務(wù)ACID中也有原子性(Atomicity)的概念,它是指一個(gè)操作是不可分割的,即要么全部執(zhí)行,要么全部不執(zhí)行。Java線程安全中的原子性與數(shù)據(jù)庫(kù)事務(wù)中的原子性本質(zhì)是一樣的,只是它們應(yīng)用的上下文和具體實(shí)現(xiàn)有所不同。
Java提供了多種方式來(lái)保證原子性,比如 同步塊、鎖或者原子類。
為了更好的說(shuō)明原子性,我們這里以一個(gè)反例來(lái)展示不具備原子性,如下代碼:
public class AtomicityTest {
private int i = 0;
public void increment() {
i++;
}
}在上述代碼中,i++這種寫法在我們的日常開(kāi)發(fā)經(jīng)常使用,但它不是一個(gè)原子操作,實(shí)際上i++分為三步:
- 讀取i的值
- 將i的值加 1
- 將結(jié)果寫回給i
如果多個(gè)線程同時(shí)執(zhí)行increment()方法,可能會(huì)導(dǎo)致i的值不正確,比如有 3個(gè)線程A,B,C:
- 線程A讀取i的值,并且將i的值加 1,但是還未將結(jié)果寫回給i;
- 此時(shí),線程B讀取i的值仍然是0,并且將i的值加 1;
- 線程A 將結(jié)果寫回給i,將i設(shè)置為 1;
- 線程B 將結(jié)果寫回給i,將i設(shè)置為 1;
- 線程C 讀取i的值為1,并且將i的值加 1,并且將結(jié)果寫回給i,將i設(shè)置為 2;
3個(gè)線程都對(duì)i進(jìn)行i++操作,預(yù)期i的最終值是 3,但因?yàn)閕++無(wú)法保證原子性,因此,i最終的值未達(dá)到預(yù)期的值。
4. 可見(jiàn)性
可見(jiàn)性是指一個(gè)線程對(duì)共享變量的修改,其他線程能立刻看到。在Java中,volatile關(guān)鍵字可以保證變量的可見(jiàn)性。
為了更好的說(shuō)明可見(jiàn)性,我們這里以一個(gè)示例進(jìn)行分析,如下代碼:
public class VisibilityTest {
private boolean running = true;
public void stop() {
running = false;
}
public void run() {
while (running) {
// do something
}
}
}在上述代碼中,變量running是一個(gè)全局變量,如果沒(méi)有使用volatile關(guān)鍵字,running 變量的修改可能不會(huì)被其他線程立即看到。
5. 有序性
有序性是指程序代碼的執(zhí)行順序。在單線程環(huán)境中,代碼的執(zhí)行順序通常是按照代碼的書寫順序執(zhí)行的。然而,在多線程環(huán)境中,編譯器、JVM和CPU可能會(huì)為了優(yōu)化性能進(jìn)行指令重排序(Instruction Reordering),這可能會(huì)導(dǎo)致代碼的執(zhí)行順序與預(yù)期不一致。
Java內(nèi)存模型(Java Memory Model, JMM)允許編譯器和處理器進(jìn)行指令重排序,但會(huì)保證單線程內(nèi)的執(zhí)行結(jié)果和多線程內(nèi)的同步結(jié)果是正確的。
這里以一個(gè)反例來(lái)展示不具備有序性,如下代碼:
public class ReorderingExample {
private int x = 0;
private boolean flag = false;
public void writer() {
x = 42;
flag = true;
}
public void read() {
if (flag) {
System.out.println(x); // 可能輸出0
}
}
}在上述代碼中,read()方法可能會(huì)看到flag=true,但x仍然為 0,因?yàn)榫幾g器或CPU可能對(duì)指令進(jìn)行重排序。
6. 如何保證線程安全
在 Java中,通??梢酝ㄟ^(guò)以下幾個(gè)方式來(lái)保證線程安全。
(1) synchronized關(guān)鍵字
synchronized是Java的一個(gè)原語(yǔ)關(guān)鍵字,它可以保證方法或代碼塊在同一時(shí)刻只能被一個(gè)線程執(zhí)行,從而確保原子性和可見(jiàn)性。
下面的代碼是synchronized關(guān)鍵字的簡(jiǎn)單使用:
public class SynchronizedTest {
private int i = 0;
public synchronized void increment() {
i++;
}
public synchronized int getCount() {
return i;
}
}(2) Lock 接口
Lock接口提供了比synchronized更靈活的鎖機(jī)制,常用的實(shí)現(xiàn)類有 ReentrantLock 可重入鎖。
下面的代碼是Lock關(guān)鍵字的簡(jiǎn)單使用:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}(3) 原子類
Java提供了一些原子類,如 AtomicInteger、AtomicLong 和 AtomicReference,它們通過(guò)CAS(Compare-And-Swap)操作實(shí)現(xiàn)了非阻塞的線程安全。
下面的代碼是AtomicInteger原子類的簡(jiǎn)單使用:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
private AtomicInteger atomic = new AtomicInteger();
public void increment() {
atomic.incrementAndGet();
}
public int getCount() {
return atomic.get();
}
}(4) ThreadLocal 類
ThreadLocal類提供了線程局部變量,每個(gè)線程都有自己獨(dú)立的變量副本,從而避免了共享數(shù)據(jù)的競(jìng)爭(zhēng)。
下面的代碼是ThreadLocal類的簡(jiǎn)單使用:
public class ThreadLocalExample {
private static final ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 1);
public int getValue() {
return threadLocal.get();
}
public void setValue(int value) {
threadLocal.set(value);
}
}(5) 分布式鎖
Redis 分布式鎖 或者 Zookeeper分布式鎖是分布式環(huán)境下保證線程安全的常用方法。關(guān)于兩種分布式鎖的原理,會(huì)在其他的文章詳細(xì)分析。
7. 總結(jié)
線程安全是 Java多線程編程中很重要的一部分,本文講解了什么是線程安全以及產(chǎn)生線程安全問(wèn)題的根因,并且通過(guò)原子性,有序性,可見(jiàn)性對(duì)線程安全進(jìn)行了分析。
- 硬件的多級(jí)緩存和Java與之對(duì)應(yīng)的內(nèi)存模型是導(dǎo)致線程安全的根因;
- volatile可以保證變量的可見(jiàn)性,但不能保證原子性,因此無(wú)法保證線程安全;
- synchronized,虛擬機(jī)鎖,原子類,分布式鎖可以保證線程的安全性;
































