線程安全在Java中意味着什麼?

Java中的線程安全意味着類的方法要麼是原子的,要麼是靜態的。那麼原子和靜止意味着什麼?爲什麼Java中沒有其他類型的線程安全方法?

“原子”是什麼意思?

當方法調用顯示立即生效時,方法是原子的。因此,其他線程可以在方法調用之前或之後看到狀態,但沒有看到中間狀態。讓我們看一看非原子方法,看看原子方法是如何使類線程安全的。您可以從以下所有示例下載源代碼GitHub.

public class UniqueIdNotAtomic { private volatile long counter = 0; public long nextId() { return counter++; } }

全班UniqueIdNotAtomic使用易失變量計數器創建唯一ID。我使用了一個易失性字段第2行,以確保線程始終看到當前值,要查看這個類是否線程安全,我們使用以下測試:

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行所示。

爲了測試所有線程交織,我們將完整的測試放在一個while循環中,使用這個類對所有線程交織進行迭代。AllInterleavings從…vm透鏡,第15行。

運行測試時,我們會看到以下錯誤:

`java.lang.AssertionError: at org.junit.Assert.fail(Assert.java:91) at org.junit.Assert.assertTrue`

造成此錯誤的原因是,由於操作++不是原子操作,這兩個線程可以覆蓋另一個線程的結果。我們可以從vm透鏡的報告中看到這一點:

Report from 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行。運行測試時,我們會看到以下錯誤:

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這兩種原子方法的組合並不是原子的。因此,這兩個線程可以覆蓋另一個線程的結果。我們可以從vm透鏡的報告中看到這一點:

Report from 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以避免爭論。它使用數組,而不是使用單個變量來存儲當前大小。不同的線程更新數組的不同部分,從而避免爭用。該算法將在更多內容中得到解釋。

靜態類和方法對於在高爭用下收集統計信息非常有用。收集數據後,可以使用單個線程計算收集的統計信息。

爲什麼Java中沒有其他線程安全方法?

在理論計算機科學中,線程安全是指數據結構滿足正確性標準。最常用的正確性準則是線性化的,這意味着數據結構的方法是原子的。

有關常見的數據結構存在可證明的線性化併發數據結構,請參閱本書。Maurice Herlihy和Nir Shavit的多處理器編程藝術…但是要使數據結構線性化,需要一種昂貴的同步機制,如比較和交換,參見本文。秩序法則:並行算法中昂貴的同步是無法消除的。瞭解更多。

因此,本文研究了靜態等其它正確性準則。因此,我認爲問題不是“爲什麼Java中沒有其他類型的線程安全方法?”但是,Java中什麼時候會有其他類型的線程安全可用呢?

結語

Java中的線程安全意味着類的方法要麼是原子的,要麼是靜態的。當方法調用顯示立即生效時,方法是原子的。靜態意味着當我們調用靜態方法時,我們需要確保當前沒有其他方法正在運行。

目前,靜態方法僅用於收集統計數據,例如ConcurrentHashMap…對於所有其他用例,都使用原子方法。讓我們拭目以待,看看未來是否會帶來更多類型的線程安全方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章