實(shí)現(xiàn)線(xiàn)程安全的11種方法,你學(xué)會(huì)了嗎?
Java原生支持多線(xiàn)程,意味著通過(guò)在獨(dú)立的線(xiàn)程中并發(fā)運(yùn)行,JVM能夠提升應(yīng)用程序的性能。
盡管多線(xiàn)程是一個(gè)強(qiáng)大的特性,但它也有代價(jià)。在多線(xiàn)程環(huán)境中,我們需要時(shí)刻崩著線(xiàn)程安全這根弦,即在不同的線(xiàn)程可以訪(fǎng)問(wèn)相同的資源,而不會(huì)暴露錯(cuò)誤行為或產(chǎn)生不可預(yù)測(cè)的結(jié)果,這種編程方法被稱(chēng)為“線(xiàn)程安全”。
一、無(wú)狀態(tài)實(shí)現(xiàn)
在大多數(shù)情況下,多線(xiàn)程中的錯(cuò)誤是由于多個(gè)線(xiàn)程之間錯(cuò)誤地共享狀態(tài)導(dǎo)致的。
因此,我們首先要探討的方法是:使用無(wú)狀態(tài)實(shí)現(xiàn)來(lái)達(dá)到線(xiàn)程安全。
為了更好地理解這種方法,先創(chuàng)建一個(gè)簡(jiǎn)單的工具類(lèi),它有一個(gè)靜態(tài)方法用于計(jì)算數(shù)字的階乘:
public class MathUtils {
public static BigInteger factorial(int number) {
BigInteger f = new BigInteger("1");
for (int i = 2; i <= number; i++) {
f = f.multiply(BigInteger.valueOf(i));
}
return f;
}
}
factorial()方法是一個(gè)無(wú)狀態(tài)的確定性函數(shù):給定特定的輸入,總是得到相同的輸出。
該方法既不依賴(lài)外部狀態(tài),也不維護(hù)狀態(tài)。因此,它被認(rèn)為是線(xiàn)程安全的,可以同時(shí)被多個(gè)線(xiàn)程安全地調(diào)用。
所有線(xiàn)程都可以安全地調(diào)用factorial()方法,并將獲得預(yù)期的結(jié)果,而不會(huì)相互干擾,也不會(huì)改變?cè)摲椒槠渌€(xiàn)程生成的輸出。
因此,無(wú)狀態(tài)實(shí)現(xiàn)是實(shí)現(xiàn)線(xiàn)程安全的最簡(jiǎn)單方法。
二、不可變實(shí)現(xiàn)
如果我們需要在不同線(xiàn)程之間共享狀態(tài),我們可以通過(guò)使類(lèi)不可變來(lái)創(chuàng)建線(xiàn)程安全的類(lèi)。
不可變性是一個(gè)強(qiáng)大的、與語(yǔ)言無(wú)關(guān)的概念,在Java中很容易實(shí)現(xiàn)。在函數(shù)式編程中,很重要的一個(gè)技巧就是不可變,參見(jiàn)什么是函數(shù)式編程?。
簡(jiǎn)單地說(shuō),當(dāng)一個(gè)類(lèi)實(shí)例在構(gòu)造后其內(nèi)部狀態(tài)不能被修改時(shí),它就是不可變的。
在Java中創(chuàng)建不可變類(lèi)的最簡(jiǎn)單方法是聲明所有字段為私有且為final,并且不提供設(shè)置器:
public class MessageService {
privatefinal String message;
public MessageService(String message) {
this.message = message;
}
public String getAndPrint() {
System.out.println(message);
return message;
}
public String getMessage() {
return message;
}
}
一個(gè)MessageService對(duì)象,在其構(gòu)造后其狀態(tài)不能改變,所以是線(xiàn)程安全的。
此外,如果MessageService是可變的,但多個(gè)線(xiàn)程對(duì)其只有只讀權(quán)限,它也是線(xiàn)程安全的。
如我們所見(jiàn),不可變性是實(shí)現(xiàn)線(xiàn)程安全的另一種方式。
三、線(xiàn)程局部字段
在面向?qū)ο缶幊蹋∣OP)中,對(duì)象實(shí)際上需要通過(guò)字段維護(hù)狀態(tài),并通過(guò)一個(gè)或多個(gè)方法實(shí)現(xiàn)行為。
如果我們確實(shí)需要維護(hù)狀態(tài),我們可以使用線(xiàn)程局部字段來(lái)創(chuàng)建線(xiàn)程安全的類(lèi),線(xiàn)程局部字段在線(xiàn)程之間就不共享狀態(tài)。
我們可以通過(guò)在Thread類(lèi)中定義私有字段輕松創(chuàng)建字段為線(xiàn)程局部的類(lèi)。
比如,我們可以定義一個(gè)Thread類(lèi),它存儲(chǔ)一個(gè)整數(shù)數(shù)組:
public class ThreadA extends Thread {
private final List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
@Override
public void run() {
numbers.forEach(System.out::println);
}
}
另一個(gè)Thread類(lèi)可能持有一個(gè)字符串?dāng)?shù)組:
public class ThreadB extends Thread {
private final List<String> letters = Arrays.asList("a", "b", "c", "d", "e", "f");
@Override
public void run() {
letters.forEach(System.out::println);
}
}
在這兩種實(shí)現(xiàn)中,類(lèi)都有自己的狀態(tài),但不與其他線(xiàn)程共享。因此,這些類(lèi)是線(xiàn)程安全的。
類(lèi)似地,我們可以通過(guò)將ThreadLocal實(shí)例分配給一個(gè)字段來(lái)創(chuàng)建線(xiàn)程局部字段。
考慮以下StateHolder類(lèi):
public class StateHolder {
private String state;
public StateHolder(String state) {
this.state = state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
我們可以很容易地使其成為一個(gè)線(xiàn)程局部變量:
public class ThreadState {
public static final ThreadLocal<StateHolder> statePerThread =
ThreadLocal.withInitial(() -> new StateHolder("active"));
public static StateHolder getState() {
return statePerThread.get();
}
}
線(xiàn)程局部字段與普通類(lèi)字段非常相似,不同之處在于每個(gè)通過(guò)setter/getter訪(fǎng)問(wèn)它們的線(xiàn)程,都會(huì)獲得該字段的獨(dú)立初始化副本,以便每個(gè)線(xiàn)程都有自己的狀態(tài)。
四、同步集合
我們可以通過(guò)使用集合框架中包含的同步包裝器輕松創(chuàng)建線(xiàn)程安全的集合。
比如,我們可以創(chuàng)建一個(gè)線(xiàn)程安全的集合:
Collection<Integer> syncCollection = Collections.synchronizedCollection(new ArrayList<>());
Thread thread1 = new Thread(() -> syncCollection.addAll(Arrays.asList(1, 2, 3, 4, 5, 6)));
Thread thread2 = new Thread(() -> syncCollection.addAll(Arrays.asList(7, 8, 9, 10, 11, 12)));
thread1.start();
thread2.start();
請(qǐng)記住,同步集合在每個(gè)方法中使用內(nèi)部鎖,這意味著這些方法一次只能被一個(gè)線(xiàn)程訪(fǎng)問(wèn),而其他線(xiàn)程將被阻塞,直到第一個(gè)線(xiàn)程釋放該方法的鎖。
五、并發(fā)集合
作為同步集合的替代方案,我們可以使用并發(fā)集合來(lái)創(chuàng)建線(xiàn)程安全的集合。
Java提供了java.util.concurrent包,其中包含幾個(gè)并發(fā)集合,比如ConcurrentHashMap:
Map<String,String> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.put("1", "one");
concurrentMap.put("2", "two");
concurrentMap.put("3", "three");
與同步集合不同,并發(fā)集合通過(guò)將數(shù)據(jù)分割成段來(lái)實(shí)現(xiàn)線(xiàn)程安全。例如,在ConcurrentHashMap中,多個(gè)線(xiàn)程可以獲取不同映射段的鎖,因此多個(gè)線(xiàn)程可以同時(shí)訪(fǎng)問(wèn)該映射。
由于并發(fā)線(xiàn)程訪(fǎng)問(wèn)的固有優(yōu)勢(shì),并發(fā)集合比同步集合性能更高。
需要注意的是,無(wú)論同步集合還是并發(fā)集合,都是集合本身線(xiàn)程安全,其內(nèi)容并不是。
六、原子對(duì)象
我們還可以使用Java提供的原子類(lèi)集合來(lái)實(shí)現(xiàn)線(xiàn)程安全,包括AtomicInteger、AtomicLong、AtomicBoolean和AtomicReference。
原子類(lèi)允許我們執(zhí)行原子操作,這些操作是線(xiàn)程安全的,而無(wú)需使用同步。
為了理解這解決了什么問(wèn)題,讓我們看一下以下Counter類(lèi):
public class Counter {
private int counter = 0;
public void incrementCounter() {
counter += 1;
}
public int getCounter() {
return counter;
}
}
假設(shè)在一個(gè)競(jìng)爭(zhēng)條件下,兩個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)incrementCounter()方法。
理論上,counter字段的最終值將為2。但我們不能確定結(jié)果,因?yàn)榫€(xiàn)程同時(shí)執(zhí)行相同的代碼塊,并且遞增操作不是原子的??梢詤⒁?jiàn)在多線(xiàn)程中使用ArrayList會(huì)發(fā)生什么?的說(shuō)明。
讓我們使用AtomicInteger對(duì)象創(chuàng)建Counter類(lèi)的線(xiàn)程安全實(shí)現(xiàn):
public class AtomicCounter {
private final AtomicInteger counter = new AtomicInteger();
public void incrementCounter() {
counter.incrementAndGet();
}
public int getCounter() {
return counter.get();
}
}
這是線(xiàn)程安全的,因?yàn)殡m然遞增操作++需要多個(gè)操作,但incrementAndGet是原子的。
七、同步方法
前面的方法對(duì)于集合和基本類(lèi)型非常有用,但有時(shí)我們需要更復(fù)雜的控制邏輯。
因此,我們可以使用的另一種常見(jiàn)方法是:實(shí)現(xiàn)同步方法來(lái)實(shí)現(xiàn)線(xiàn)程安全。
簡(jiǎn)單地說(shuō),一次只能有一個(gè)線(xiàn)程訪(fǎng)問(wèn)同步方法,同時(shí)阻止其他線(xiàn)程訪(fǎng)問(wèn)該方法。其他線(xiàn)程將保持阻塞,直到第一個(gè)線(xiàn)程完成或該方法拋出異常。
我們可以通過(guò)將incrementCounter()方法變?yōu)橥椒椒▉?lái)以另一種方式創(chuàng)建其線(xiàn)程安全版本:
public synchronized void incrementCounter() {
counter += 1;
}
我們通過(guò)在方法簽名前加上synchronized關(guān)鍵字創(chuàng)建了一個(gè)同步方法。
由于一次只能有一個(gè)線(xiàn)程訪(fǎng)問(wèn)同步方法,一個(gè)線(xiàn)程將執(zhí)行incrementCounter()方法,其他線(xiàn)程將依次執(zhí)行。不會(huì)發(fā)生任何重疊執(zhí)行。
同步方法依賴(lài)于使用“內(nèi)部鎖”或“監(jiān)視器”,內(nèi)部鎖是與特定類(lèi)實(shí)例相關(guān)聯(lián)的隱式內(nèi)部實(shí)體。具體參加synchronized 鎖同步。
在多線(xiàn)程上下文中,“監(jiān)視器”只是對(duì)鎖在相關(guān)對(duì)象上執(zhí)行的角色的引用,它強(qiáng)制對(duì)一組指定的方法或語(yǔ)句進(jìn)行獨(dú)占訪(fǎng)問(wèn)。
當(dāng)一個(gè)線(xiàn)程調(diào)用同步方法時(shí),它獲取內(nèi)部鎖,線(xiàn)程執(zhí)行完方法后,會(huì)釋放鎖,允許其他線(xiàn)程獲取鎖并訪(fǎng)問(wèn)該方法。
我們可以在實(shí)例方法、靜態(tài)方法和語(yǔ)句(同步語(yǔ)句)中實(shí)現(xiàn)同步。
八、同步語(yǔ)句
有時(shí),如果我們只需要使方法的一部分線(xiàn)程安全,同步整個(gè)方法可能有些過(guò)度。
我們?cè)僦貥?gòu)incrementCounter()方法:
public void incrementCounter() {
// 其他未同步的操作
synchronized(this) {
counter += 1;
}
}
假設(shè)該方法現(xiàn)在執(zhí)行一些其他不需要同步的操作,我們通過(guò)將相關(guān)的狀態(tài)修改部分包裝在同步塊中來(lái)僅同步這部分。
與同步方法不同,同步語(yǔ)句必須指定提供內(nèi)部鎖的對(duì)象,通常是this引用。
同步是有代價(jià)的,通過(guò)同步代碼塊,能夠僅同步方法的相關(guān)部分。
(一)其他對(duì)象作為鎖
我們可以通過(guò)利用另一個(gè)對(duì)象作為監(jiān)視器鎖(而不是this)來(lái)稍微改進(jìn)Counter類(lèi)的線(xiàn)程安全實(shí)現(xiàn)。
這不僅在多線(xiàn)程環(huán)境中為共享資源提供了協(xié)調(diào)訪(fǎng)問(wèn),還使用外部實(shí)體來(lái)強(qiáng)制對(duì)資源的獨(dú)占訪(fǎng)問(wèn):
public class ObjectLockCounter {
privateint counter = 0;
privatefinal Object lock = new Object();
public void incrementCounter() {
synchronized (lock) {
counter += 1;
}
}
public int getCounter() {
return counter;
}
}
我們使用一個(gè)普通的Object實(shí)例來(lái)實(shí)現(xiàn)互斥,它提高了鎖級(jí)別的安全性。
當(dāng)使用this進(jìn)行內(nèi)部鎖時(shí),攻擊者可以通過(guò)獲取內(nèi)部鎖并觸發(fā)拒絕服務(wù)(DoS)條件來(lái)導(dǎo)致死鎖。
相反,當(dāng)使用其他對(duì)象時(shí),無(wú)法從外部訪(fǎng)問(wèn)這個(gè)私有對(duì)象,攻擊者很難獲取鎖并導(dǎo)致死鎖。
(二)注意事項(xiàng)
盡管我們可以使用任何Java對(duì)象作為內(nèi)部鎖,但我們應(yīng)該避免使用String進(jìn)行鎖定:
public class Class1 {
privatestaticfinal String LOCK = "Lock";
// 使用LOCK作為內(nèi)部鎖
}
publicclass Class2 {
privatestaticfinal String LOCK = "Lock";
// 使用LOCK作為內(nèi)部鎖
}
乍一看,似乎這兩個(gè)類(lèi)使用了兩個(gè)不同的對(duì)象作為它們的鎖。然而,由于字符串駐留,這兩個(gè)“Lock”值實(shí)際上可能在字符串池中引用同一個(gè)對(duì)象。也就是說(shuō),Class1和Class2共享同一個(gè)鎖!
除了String,我們應(yīng)該避免使用任何可緩存或可重用的對(duì)象作為內(nèi)部鎖。比如,Integer.valueOf()方法緩存小數(shù)字。因此,即使在不同的類(lèi)中調(diào)用Integer.valueOf(1)也會(huì)返回同一個(gè)對(duì)象。
九、易失性字段
同步方法和塊對(duì)于解決線(xiàn)程之間的變量可見(jiàn)性問(wèn)題很方便,即便如此,常規(guī)類(lèi)字段的值可能會(huì)被CPU緩存。因此,即使對(duì)特定字段進(jìn)行了同步更新,其他線(xiàn)程可能也看不到這些更新。
為了防止這種情況,我們可以使用易失性類(lèi)字段(通過(guò)volatile關(guān)鍵字標(biāo)記):
public class Counter {
private volatile int counter;
// 標(biāo)準(zhǔn)的構(gòu)造函數(shù)/獲取器
}
通過(guò)使用volatile關(guān)鍵字,我們指示JVM和編譯器將counter變量存儲(chǔ)在主內(nèi)存中。這樣,我們確保每次JVM讀取counter變量的值時(shí),它實(shí)際上是從主內(nèi)存中讀取,而不是從CPU緩存中讀取。同樣,每次JVM寫(xiě)入counter變量時(shí),值將被寫(xiě)入主內(nèi)存。
此外,使用易失性變量確保給定線(xiàn)程可見(jiàn)的所有變量也將從主內(nèi)存中讀取。
比如:
public class User {
private String name;
private volatile int age;
// 標(biāo)準(zhǔn)的構(gòu)造函數(shù)/獲取器
}
在這種情況下,每次JVM將age易失性變量寫(xiě)入主內(nèi)存時(shí),它也會(huì)將非易失性name變量寫(xiě)入主內(nèi)存。這確保了兩個(gè)變量的最新值都存儲(chǔ)在主內(nèi)存中,因此對(duì)變量的后續(xù)更新將自動(dòng)對(duì)其他線(xiàn)程可見(jiàn)。
類(lèi)似地,如果一個(gè)線(xiàn)程讀取易失性變量的值,該線(xiàn)程可見(jiàn)的所有變量也將從主內(nèi)存中讀取。
易失性變量提供的這種擴(kuò)展保證被稱(chēng)為完全易失性可見(jiàn)性保證。
十、可重入鎖
Java提供了一組改進(jìn)的鎖實(shí)現(xiàn),其行為比上面討論的內(nèi)部鎖稍微復(fù)雜一些。
對(duì)于內(nèi)部鎖,鎖獲取模型相當(dāng)嚴(yán)格:一個(gè)線(xiàn)程獲取鎖,然后執(zhí)行一個(gè)方法或代碼塊,最后釋放鎖,以便其他線(xiàn)程可以獲取它并訪(fǎng)問(wèn)該方法。內(nèi)部鎖沒(méi)有實(shí)現(xiàn)檢查排隊(duì)的線(xiàn)程并優(yōu)先訪(fǎng)問(wèn)等待時(shí)間最長(zhǎng)的線(xiàn)程,即屬于非公平鎖。
ReentrantLock實(shí)例允許我們做到這一點(diǎn),防止排隊(duì)的線(xiàn)程遭受某些類(lèi)型的資源饑餓:
public class ReentrantLockCounter {
privateint counter;
privatefinal ReentrantLock reLock = new ReentrantLock(true);
public void incrementCounter() {
reLock.lock();
try {
counter += 1;
} finally {
reLock.unlock();
}
}
public int getCounter() {
return counter;
}
}
ReentrantLock構(gòu)造函數(shù)接受一個(gè)可選的公平性布爾參數(shù),當(dāng)設(shè)置為true且多個(gè)線(xiàn)程試圖獲取鎖時(shí),JVM將優(yōu)先考慮等待時(shí)間最長(zhǎng)的線(xiàn)程并授予其訪(fǎng)問(wèn)鎖的權(quán)限,即實(shí)現(xiàn)公平鎖。
十一、讀寫(xiě)鎖
我們可以使用讀寫(xiě)鎖實(shí)現(xiàn)來(lái)實(shí)現(xiàn)線(xiàn)程安全。讀寫(xiě)鎖實(shí)際上使用一對(duì)相關(guān)聯(lián)的鎖,一個(gè)用于只讀操作,另一個(gè)用于寫(xiě)入操作。
因此,只要沒(méi)有線(xiàn)程正在寫(xiě)入資源,就可以有多個(gè)線(xiàn)程讀取該資源。此外,寫(xiě)入資源的線(xiàn)程將阻止其他線(xiàn)程讀取它。
以下是我們?nèi)绾问褂米x寫(xiě)鎖:
public class ReentrantReadWriteLockCounter {
privateint counter;
privatefinal ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
privatefinal Lock readLock = rwLock.readLock();
privatefinal Lock writeLock = rwLock.writeLock();
public void incrementCounter() {
writeLock.lock();
try {
counter += 1;
} finally {
writeLock.unlock();
}
}
public int getCounter() {
readLock.lock();
try {
return counter;
} finally {
readLock.unlock();
}
}
}
文末總結(jié)
在本文中,我們了解了Java中的線(xiàn)程安全是什么,并深入研究了實(shí)現(xiàn)線(xiàn)程安全的11種方法。