簡體   English   中英

原始數組寫入的Java並發可見性

[英]Java concurrent visibility of primitive array writes

我最近在我的代碼庫中找到了這個gem:

/** This class is used to "publish" changes to a non-volatile variable.
 *
 * Access to non-volatile and volatile variables cannot be reordered,
 * so if you make changes to a non-volatile variable before calling publish,
 * they are guaranteed to be visible to a thread which calls syncChanges
 *
 */
private static class Publisher {
    //This variable may not look like it's doing anything, but it really is.
    //See the documentaion for this class.
    private volatile AtomicInteger sync = new AtomicInteger(0);

    void publish() {
        sync.incrementAndGet();
    }

    /**
     *
     * @return the return value of this function has no meaning.
     * You should not make *any* assumptions about it.
     */
    int syncChanges() {
        return sync.get();
    }
}

這樣使用:

線程1

float[][] matrix;
matrix[x][y] = n;
publisher.publish();

線程2

publisher.syncChanges();
myVar = matrix[x][y];

線程1是一個連續運行的后台更新線程。 線程2是一個HTTP工作線程,它不關心它讀取的內容是以任何方式一致的還是原子的,只是寫入“最終”到達並且不會作為並發神的提供而丟失。

現在,這會觸發我所有的警告鈴聲。 自定義並發算法深入寫入無關代碼。

不幸的是,修復代碼並非易事。 Java對並發原始矩陣的支持並不好。 看起來最明確的解決方法是使用ReadWriteLock ,但這可能會對性能產生負面影響。 顯而易見,正確性更為重要,但似乎我應該在將其從性能敏感區域中剝離之前證明這是正確的。

根據java.util.concurrent文檔 ,以下創建happens-before關系:

線程中的每個動作都發生在該線程中的每個動作之前,該動作在程序的順序中稍后出現。

在每次后續讀取同一字段之前,會發生對易失性字段的寫入。 易失性字段的寫入和讀取具有與進入和退出監視器類似的內存一致性效果,但不需要互斥鎖定。

所以聽起來像:

  • 矩陣寫在發布之前發生()(規則1)
  • publish()發生在syncChanges()之前(規則2)
  • syncChanges()發生在矩陣讀取之前(規則1)

所以代碼確實已經建立了矩陣的先發條件鏈。

但我不相信。 並發很難,我不是域專家。 我錯過了什么? 這確實安全嗎?

在可見性方面,您需要的是在任何易失性字段上的易讀寫入。 這會奏效

final    float[][] matrix  = ...;
volatile float[][] matrixV = matrix;

線程1

matrix[x][y] = n;
matrixV = matrix; // volatile write

線程2

float[][] m = matrixV;  // volatile read
myVar = m[x][y];

or simply
myVar = matrixV[x][y];

但這只適用於更新一個變量。 如果編寫器線程正在寫入多個變量並且讀取線程正在讀取它們,則讀者可能會看到不一致的圖片。 通常它由讀寫鎖處理。 寫時復制可能適用於某些使用模式。

Doug Lea有一個新的“StampedLock” http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/StampedLock.html for Java8,它是一個讀寫鎖的版本,讀取便宜得多鎖。 但它也很難使用。 基本上讀者獲取當前版本,然后繼續讀取一堆變量,然后再次檢查版本; 如果版本沒有更改,則在讀取會話期間沒有並發寫入。

這對於向矩陣發布單個更新看起來確實安全,但當然它不提供任何原子性。 這是否可行取決於您的應用程序,但它應該記錄在這樣的實用程序類中。

但是,它包含一些冗余,可以通過使sync字段final來改進。 該字段的volatile訪問是兩個內存屏障中的第一個; 通過契約,調用incrementAndGet()對內存的影響與寫入和讀取volatile變量相同,並且調用get()與讀取具有相同的效果。

因此,代碼可以單獨依賴這些方法提供的同步,並使字段本身final

使用volatile不是同步所有內容的靈丹妙葯。 保證如果另一個線程讀取volatile變量的更新值,它們也會看到在此之前對非volatile變量進行的每個更改。 但是沒有什么能保證其他線程會讀取更新的值

在示例代碼中,如果您對matrix進行多次寫入然后調用publish() ,而另一個線程調用synch()然后讀取矩陣,則另一個線程可能會看到一些,全部或沒有任何更改:

  • 所有更改,如果它從publish()讀取更新的值
  • 沒有任何更改,如果它讀取舊的已發布值並且沒有任何更改泄漏
  • 一些變化,如果它讀取以前發布的值,但一些變化已經泄露

看到這篇文章

你正確地提到了之前發生過關系的規則#2

在每次后續讀取同一字段之前,會發生對易失性字段的寫入。

但是,它不保證在絕對時間軸上的syncChanges()之前調用publish()。 讓我們稍微改變你的例子。

線程1:

matrix[0][0] = 42.0f;
Thread.sleep(1000*1000); // assume the thread was preempted here
publisher.publish(); //assume initial state of sync is 0 

線程2:

int a = publisher.syncChanges();
float b = matrix[0][0];

a和b變量的選項有哪些?

  • a為0,b可以為0或42
  • a是1,b是42,因為之前發生過關系
  • a大於1(線程2由於某種原因很慢而且線程1幸運地多次發布更新),b的值取決於業務邏輯和矩陣的處理方式 - 它是否依賴於以前的狀態?

怎么處理呢? 這取決於業務邏輯。

  • 如果線程2不時地輪詢矩陣的狀態,並且在它們之間有一些過時的值是完全正確的,如果最終將處理正確的值,則保持原樣。
  • 如果線程2不關心錯過的更新,但它總是想要觀察最新的矩陣,那么使用copy-on-write集合或使用上面提到的ReaderWriteLock。
  • 如果線程2確實關心單個更新,那么它應該以更智能的方式處理,您可能需要考慮wait()/ notify()模式並在更新矩陣時通知線程2。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM