“線程安全”實際上意味着什麼?
通過優銳課的學習分享,討論了關於Java中的線程安全性意味着類的方法是原子的或靜態的。 那麼原子是什麼,靜止是什麼意思呢? 爲什麼在Java中沒有其他類型的線程安全方法?
“原子”是什麼意思?
當方法調用似乎立即生效時,該方法就是原子的。 因此,其他線程在方法調用之前或之後只能看到狀態,而沒有中間狀態。 讓我們看一下非原子方法,看看原子方法如何使類具有線程安全性。
public class UniqueIdNotAtomic {
private volatile long counter = 0;
public long nextId() {
return counter++;
}
}
類UniqueIdNotAtomic通過使用易失性變量計數器創建唯一的ID。 我在第2行使用了volatile字段,以確保線程始終看到當前值,如此處更詳細的說明。 要查看此類是否是線程安全的,我們使用以下測試:
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 );
}
}
}
}
爲了測試計數器是否是線程安全的,我們需要在第16和17行中創建兩個線程。我們啓動這兩個線程(第18和19行)。然後,我們等待直到兩個線程都通過第20和21行結束。 在兩個線程都停止之後,我們檢查兩個ID是否唯一,如第22行所示。
爲了測試所有線程交織,我們使用來自vmlens第15行的AllInterleavings類,將完整的測試放在while循環中迭代所有線程交織。
運行測試,我們看到以下錯誤:
java.lang.AssertionError:
at org.junit.Assert.fail(Assert.java:91)
at org.junit.Assert.assertTrue(Assert.java:43)
發生該錯誤的原因是,由於操作++不是原子操作,因此兩個線程可以覆蓋另一個線程的結果。 我們可以在vmlens的報告中看到這一點:
在發生錯誤的情況下,兩個線程首先並行讀取變量計數器。 然後,兩個都創建相同的ID。 爲了解決這個問題,我們通過使用同步塊使方法原子化:
private final Object LOCK = new Object();
public long nextId() {
synchronized(LOCK) {
return counter++;
}
}
現在,該方法是原子的。 同步塊可確保其他線程無法看到該方法的中間狀態。
不訪問共享狀態的方法是自動原子的。 具有隻讀狀態的類也是如此。 因此,無狀態和不可變的類是實現線程安全類的簡便方法。 他們所有的方法都是自動的。
並非原子方法的所有用法都是自動線程安全的。 將多個原子方法組合爲相同的值通常會導致爭用條件。 讓我們看看從ConcurrentHashMap獲取和放置的原子方法以瞭解原因。 當以前的映射不存在時,讓我們使用這些方法在映射中插入一個值:
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行)。再次,我們在兩個線程完成之後測試結果是否正確(第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的組合不是原子的。 因此,兩個線程可以覆蓋另一個線程的結果。 我們可以在vmlens的報告中看到這一點:
在發生錯誤的情況下,兩個線程首先並行獲取值。 然後,兩個都創建相同的值並將其放入地圖中。 要解決這種競爭狀況,我們需要使用一種方法而不是兩種方法。 在我們的例子中,我們可以使用單個方法而不是兩個方法get和put來進行計算:
public void update() {
map.compute(1, (key, value) -> {
if (value == null) {
return 1;
}
return value + 1;
});
}
因爲方法計算是原子的,所以這解決了競爭條件。 雖然對ConcurrentHashMap的相同元素進行的所有操作都是原子操作,但對整個地圖(如大小)進行操作的操作都是靜態的。 因此,讓我們看看靜態意味着什麼。
“靜止”是什麼意思?
靜態意味着當我們調用靜態方法時,我們需要確保當前沒有其他方法在運行。 下面的示例顯示如何使用ConcurrentHashMap的靜態方法大小:
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());
通過等待直到所有線程都使用線程連接完成爲止,當我們調用方法大小時,我們確保沒有其他線程正在訪問ConcurrentHashMap。
方法大小使用在java.util.concurrent.atomic.LongAdder,LongAccumulator,DoubleAdder和DoubleAccumulator類中也使用的一種機制來避免爭用。 與其使用單個變量存儲當前大小,不如使用數組。 不同的線程更新數組的不同部分,從而避免爭用。 該算法在Striped64的Java文檔中有更詳細的說明。
靜態類和靜態方法對於收集競爭激烈的統計數據很有用。 收集數據後,可以使用一個線程來評估收集的統計信息。
爲什麼在Java中沒有其他線程安全方法?
在理論計算機科學中,線程安全性意味着數據結構滿足正確性標準。 最常用的正確性標準是可線性化的,這意味着數據結構的方法是原子的。
對於常見的數據結構,存在可證明的線性化併發數據結構,請參見Maurice Herlihy和Nir Shavit撰寫的《多處理器編程的藝術》一書。 但是要使數據結構線性化,需要使用比較和交換之類的昂貴同步機制,請參閱論文《定律:無法消除併發算法中的昂貴同步》以瞭解更多信息。
因此,研究了其他正確性標準(例如靜態)。 因此,我認爲問題不在於“爲什麼Java中沒有其他類型的線程安全方法?” 但是,Java何時將提供其他類型的線程安全性?
結論
Java中的線程安全性意味着類的方法是原子的或靜態的。 當方法調用似乎立即生效時,該方法就是原子的。 靜態意味着當我們調用靜態方法時,我們需要確保當前沒有其他方法在運行。
目前,靜態方法僅用於收集統計信息,例如ConcurrentHashMap的大小。 對於所有其他用例,使用原子方法。 讓我們拭目以待,未來是否會帶來更多類型的線程安全方法。
文章寫道這裏,如有不足之處,歡迎補充評論。
如果你對java技術很感興趣也可以一起交流學習,共同學習進步!
最近get了很多新知識,希望能幫到大家。需要詳細的java架構思維導圖路線也可以評論獲取!