您好,登錄后才能下訂單哦!
線程安全性概念
當(dāng)對一個復(fù)雜對象進(jìn)行某種操作時,從操作開始到操作結(jié)束,被操作的對象往往會經(jīng)歷若干非法的中間狀態(tài)。調(diào)用一個函數(shù)(假設(shè)該函數(shù)是正確的)操作某對象常常會使該對象暫時陷入不可用的狀態(tài)(通常稱為不穩(wěn)定狀態(tài)),等到操作完全結(jié)束,該對象才會重新回到完全可用的狀態(tài)。如果其他線程企圖訪問一個處于不可用狀態(tài)的對象,該對象將不能正確響應(yīng)從而產(chǎn)生無法預(yù)料的結(jié)果,如何避免這種情況發(fā)生是線程安全性的核心問題。
原子概念
當(dāng)方法調(diào)用似乎立即生效時,該方法就是原子的。 因此,其他線程在方法調(diào)用之前或之后只能看到狀態(tài),而沒有中間狀態(tài)。 讓我們看一下非原子方法,看看原子方法如何使類具有線程安全性。
public class UniqueIdNotAtomic {
private volatile long counter = 0;
public long nextId() {
return counter++;
}
}
類UniqueIdNotAtomic通過使用易失性變量計數(shù)器創(chuàng)建唯一的ID。 我在第2行使用了volatile字段,以確保線程始終看到當(dāng)前值,如此處更詳細(xì)的說明。 要查看此類是否是線程安全的,我們使用以下測試:
public class TestUniqueIdNotAtomic {
private final UniqueIdNotAtomic uniqueId = new UniqueIdNotAtomic();
private long firstId;
private long secondId;
private void updateFirstId() {
firstId = uniqueId.nextId();
}
private void updateSecondId() {
secondId = uniqueId.nextId();
}
@Test
public void testUniqueId() throws InterruptedException {
try (AllInterleavings allInterleavings =
new AllInterleavings("TestUniqueIdNotAtomic");) {
while(allInterleavings.hasNext()) {
Thread first = new Thread( () -> { updateFirstId(); } ) ;
Thread second = new Thread( () -> { updateSecondId(); } ) ;
first.start();
second.start();
first.join();
second.join();
assertTrue( firstId != secondId );
}
}
}
}
為了測試計數(shù)器是否是線程安全的,我們需要在第16和17行中創(chuàng)建兩個線程。我們啟動這兩個線程(第18和19行)。然后,我們等待直到兩個線程都通過第20和21行結(jié)束。 在兩個線程都停止之后,我們檢查兩個ID是否唯一,如第22行所示。
為了測試所有線程交織,我們使用來自vmlens第15行的AllInterleavings類,將完整的測試放在while循環(huán)中迭代所有線程交織。
運行測試,我們看到以下錯誤:
java.lang.AssertionError:
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.assertTrue(Assert.java:43)
發(fā)生該錯誤的原因是,由于操作++不是原子操作,因此兩個線程可以覆蓋另一個線程的結(jié)果。 我們可以在vmlens的報告中看到這一點:
在發(fā)生錯誤的情況下,兩個線程首先并行讀取變量計數(shù)器。 然后,兩個都創(chuàng)建相同的ID。 為了解決這個問題,我們通過使用同步塊使方法原子化:
private final Object LOCK = new Object();
public long nextId() {
synchronized(LOCK) {
return counter++;
}
}
現(xiàn)在,該方法是原子的。 同步塊可確保其他線程無法看到該方法的中間狀態(tài)。
不訪問共享狀態(tài)的方法是自動原子的。 具有只讀狀態(tài)的類也是如此。 因此,無狀態(tài)和不可變的類是實現(xiàn)線程安全類的簡便方法。 他們所有的方法都是自動的。
并非原子方法的所有用法都是自動線程安全的。 將多個原子方法組合為相同的值通常會導(dǎo)致爭用條件。 讓我們看看從ConcurrentHashMap獲取和放置的原子方法以了解原因。 當(dāng)以前的映射不存在時,讓我們使用這些方法在映射中插入一個值:
public class TestUpdateTwoAtomicMethods {
public void update(ConcurrentHashMap<Integer,Integer> map) {
Integer result = map.get(1);
if( result == null ) {
map.put(1, 1);
}
else {
map.put(1, result + 1 );
}
}
@Test
public void testUpdate() throws InterruptedException {
try (AllInterleavings allInterleavings =
new AllInterleavings("TestUpdateTwoAtomicMethods");) {
while(allInterleavings.hasNext()) {
final ConcurrentHashMap<Integer,Integer> map =
new ConcurrentHashMap<Integer,Integer>();
Thread first = new Thread( () -> { update(map); } ) ;
Thread second = new Thread( () -> { update(map); } ) ;
first.start();
second.start();
first.join();
second.join();
assertEquals( 2 , map.get(1).intValue() );
}
}
}
}
該測試與先前的測試相似。 再次,我們使用兩個線程來測試我們的方法是否是線程安全的(第18行和第19行)。再次,我們在兩個線程完成之后測試結(jié)果是否正確(第24行)。運行測試,我們看到以下錯誤:
java.lang.AssertionError: expected:<2> but was:<1>
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.failNotEquals(Assert.java:645)
該錯誤的原因是,兩種原子方法get和put的組合不是原子的。 因此,兩個線程可以覆蓋另一個線程的結(jié)果。 我們可以在vmlens的報告中看到這一點:
在發(fā)生錯誤的情況下,兩個線程首先并行獲取值。 然后,兩個都創(chuàng)建相同的值并將其放入地圖中。 要解決這種競爭狀況,我們需要使用一種方法而不是兩種方法。 在我們的例子中,我們可以使用單個方法而不是兩個方法get和put來進(jìn)行計算:
public void update() {
map.compute(1, (key, value) -> {
if (value == null) {
return 1;
}
return value + 1;
});
}
因為方法計算是原子的,所以這解決了競爭條件。 雖然對ConcurrentHashMap的相同元素進(jìn)行的所有操作都是原子操作,但對整個地圖(如大?。┻M(jìn)行操作的操作都是靜態(tài)的。 因此,讓我們看看靜態(tài)意味著什么。
“靜止”是什么意思?
靜態(tài)意味著當(dāng)我們調(diào)用靜態(tài)方法時,我們需要確保當(dāng)前沒有其他方法在運行。 下面的示例顯示如何使用ConcurrentHashMap的靜態(tài)方法大?。?/p>
ConcurrentHashMap<Integer,Integer> map =
new ConcurrentHashMap<Integer,Integer>();
Thread first = new Thread(() -> { map.put(1,1);});
Thread second = new Thread(() -> { map.put(2,2);});
first.start();
second.start();
first.join();
second.join();
assertEquals( 2 , map.size());
通過等待直到所有線程都使用線程連接完成為止,當(dāng)我們調(diào)用方法大小時,我們確保沒有其他線程正在訪問ConcurrentHashMap。
方法大小使用在java.util.concurrent.atomic.LongAdder,LongAccumulator,DoubleAdder和DoubleAccumulator類中也使用的一種機(jī)制來避免爭用。 與其使用單個變量存儲當(dāng)前大小,不如使用數(shù)組。 不同的線程更新數(shù)組的不同部分,從而避免爭用。 該算法在Striped64的Java文檔中有更詳細(xì)的說明。
靜態(tài)類和靜態(tài)方法對于收集競爭激烈的統(tǒng)計數(shù)據(jù)很有用。 收集數(shù)據(jù)后,可以使用一個線程來評估收集的統(tǒng)計信息。
為什么在Java中沒有其他線程安全方法?
在理論計算機(jī)科學(xué)中,線程安全性意味著數(shù)據(jù)結(jié)構(gòu)滿足正確性標(biāo)準(zhǔn)。 最常用的正確性標(biāo)準(zhǔn)是可線性化的,這意味著數(shù)據(jù)結(jié)構(gòu)的方法是原子的。
對于常見的數(shù)據(jù)結(jié)構(gòu),存在可證明的線性化并發(fā)數(shù)據(jù)結(jié)構(gòu),請參見Maurice Herlihy和Nir Shavit撰寫的《多處理器編程的藝術(shù)》一書。 但是要使數(shù)據(jù)結(jié)構(gòu)線性化,需要使用比較和交換之類的昂貴同步機(jī)制,請參閱論文《定律:無法消除并發(fā)算法中的昂貴同步》以了解更多信息。
因此,研究了其他正確性標(biāo)準(zhǔn)(例如靜態(tài))。 因此,我認(rèn)為問題不在于“為什么Java中沒有其他類型的線程安全方法?” 但是,Java何時將提供其他類型的線程安全性?
結(jié)論
Java中的線程安全性意味著類的方法是原子的或靜態(tài)的。 當(dāng)方法調(diào)用似乎立即生效時,該方法就是原子的。 靜態(tài)意味著當(dāng)我們調(diào)用靜態(tài)方法時,我們需要確保當(dāng)前沒有其他方法在運行。
目前,靜態(tài)方法僅用于收集統(tǒng)計信息,例如ConcurrentHashMap的大小。 對于所有其他用例,使用原子方法。 讓我們拭目以待,未來是否會帶來更多類型的線程安全方法。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。