Java中的線程安全性是什麼意思?

“線程安全”實際上意味着什麼?

通過優銳課的學習分享,討論了關於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技術很感興趣也可以一起交流學習,共同學習進步!

Java中的線程安全性是什麼意思?

最近get了很多新知識,希望能幫到大家。需要詳細的java架構思維導圖路線也可以評論獲取!

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