簡體   English   中英

當數組變量是volatile時,我們是否需要同步對數組的訪問?

[英]Do we need to synchronize access to an array when the array variable is volatile?

我有一個包含對數組的volatile引用的類:

private volatile Object[] objects = new Object[100];

現在,我可以保證,只有一個線程(稱之為writer )可以寫入到陣列。 例如,

objects[10] = new Object();

所有其他線程將只讀取被寫入的值writer線程。

問題:我是否需要同步這樣的讀寫以確保內存一致性?

我認為,是的,我應該。 因為從性能角度來看,如果JVM在寫入數組時提供某種內存一致性保證,那么它就沒有用處。 但我不確定。 沒有找到任何有用的文檔。

您可以使用AtomicReferenceArray

final AtomicReferenceArray<Object> objects = new AtomicReferenceArray<>(100);

// writer
objects.set(10, new Object());

// reader
Object obj = objects.get(10);

這將確保原子更新並在讀/寫操作的一致性之前發生,就像每個數組項都是volatile

 private volatile Object[] objects = new Object[100]; 

您只能通過這種方式將objects引用為volatile 不是關聯的數組實例內容。

問題:我是否需要同步這樣的讀寫以確保內存一致性?

是。

如果JVM在寫入數組時提供某種內存一致性保證,那么從性能角度來看它是沒有用的

考慮使用像CopyOnWriteArrayList這樣的集合(或者你自己的數組包裝器,在mutator和read方法中有一些Lock實現)。

Java平台還有Vector (過時的有缺陷設計)和synchronized List (很多場景都很慢),但我不建議使用它們。

PS:來自@SashaSalauyou的 另一個好主意

根據JLS§17.4.5 - 在訂單之前發生

可以通過先發生關系來排序兩個動作。 如果一個動作發生在另一個動作之前 ,那么第一個動作第二個動作之前可見並且在第

[...]

對該字段的每次后續讀取之前發生volatile字段的寫入。

事先發生的關系非常強烈。 這意味着如果線程A寫入volatile變量,並且任何線程B稍后讀取變量,則線程B保證在設置volatile變量之前看到對volatile變量本身的更改以及所有其他更改線程A. ,包括任何其他對象,無論它們是否volatile

但是,這還不夠!

元素賦值objects[10] = new Object(); 不是變量objects 的寫入 它只是讀取變量以確定它指向的數組,然后寫入包含在位於內存中其他位置的數組對象中的不同變量。 沒有發生 - 在通過僅讀取volatile變量建立關系之前 ,因此代碼不安全。

正如@DimitarDimitrov指出的那樣,你可以通過對objects變量進行虛擬寫入來解決這個問題。 每對操作 - objects = objects; 由作者線程重新分配加上foo = objects[x]; 讀取器線程查找 - 定義更新的先發生關系,因此將“發布”作者線程對讀取器線程所做的所有最新更改。 這可行,但它需要紀律,而且不優雅。

但是有一個更微妙的問題:即使讀者線程看到數組元素的更新值仍然不能保證它正確地看到該元素引用的對象的字段,因為以下順序是可能的:

  1. Writer創建了一些對象foo
  2. Writer設置objects[x] = foo;
  3. Reader檢查objects[x]並查看對新對象foo的引用(它可以做到 ,但不能保證這樣做,因為之前沒有發生過關系)。
  4. Writer做objects = objects;

不幸的是,這並沒有定義正式發生在之前的關系,因為volatile變量read(3)來自volatile變量write(4)。 雖然讀者可以偶然地看到objects[x]是對象foo ,但這並不意味着foo字段被安全地發布 ,因此理論上讀者可能會看到新對象,但錯誤的值! 要解決這個問題,使用這種技術在線程之間共享的對象需要將所有字段設置為finalvolatile或以其他方式同步。 例如,如果對象都是String ,那么你會沒事的,但除此之外,它很容易犯這個錯誤。 (感謝@Holger指出這一點。)


以下是一些不太明顯的替代品:

  • AtomicReferenceArray這樣的並發數組類用於提供數組,其中每個元素的行為都像volatile 這更容易正確使用,因為它確保如果讀者看到更新的數組元素值,它還可以正確地看到該元素引用的對象。

  • 您可以在synchronized塊中包裝對數組的所有訪問,並在某些共享對象上進行synchronized

     // writer synchronized (aSharedObject) { objects[x] = foo; } 
     // reader synchronized (aSharedObject) { bar = objects[x]; } 

    volatile一樣,使用synchronized會創建一個先發生過的關系。 (在釋放對象的同步鎖之前,線程所做的一切都發生在任何其他線程獲取同一對象的同步鎖之前 。)如果這樣做,則陣列不需要是volatile

  • 考慮一下陣列是否真的是你需要的。 您還沒有說出這些編寫器和讀者線程的用途,但如果您需要某種生產者 - 消費者隊列,那么您真正需要的類是BlockingQueueExecutor 您應該查看Java並發類,看​​看其中一個是否已經滿足您的需求,因為如果有的話,它肯定比volatile更容易正確使用。

是的,您需要同步訪問易失性數組的元素。

其他人已經解決了你如何使用CopyOnWriteArrayListAtomicReferenceArray ,所以我將轉向一個稍微不同的方向。 我還建議由JMM的一大貢獻者Jeremy Manson閱讀Java中Volatile Arrays

現在,我可以保證只有一個線程(稱為編寫器)可以寫入數組,如下所示:

是否可以提供單個寫入器保證與volatile關鍵字無關。 我認為你沒有考慮到這一點,但我只是澄清,以便其他讀者不會得到錯誤的印象( 我認為有一個數據競爭雙關語可以用這句話來做)。

所有其他線程只讀取編寫器線程寫入的值。

是的,但是就像你的直覺正確引導你一樣,這只適用於數組引用的值。 這意味着除非您正在向volatile變量寫入數組引用,否則您將無法獲得volatile寫 - 讀合同的寫入部分。

這意味着要么你想要做的事情

objects[i] = newObj;
objects = objects;

這在許多方面都是丑陋可怕的。 或者你想在每次作家進行更新時發布一個全新的數組,例如

Object[] newObjects = new Object[100];

// populate values in newObjects, make sure that newObjects IS NOT published yet

// publish newObjects through the volatile variable
objects = newObjects;

這不是一個非常常見的用例。

請注意,與設置不提供volatile -write語義的數組元素不同,獲取數組元素(使用newObj = objects[i]; )確實提供了volatile -read語義,因為您正在取消引用數組:)

因為如果JVM在寫入數組時提供某種內存一致性保證,從性能角度來看是沒有用的。 但我不確定。

就像你所暗示的那樣,確保volatile語義所需的內存防護成本非常高,如果你在混合中添加錯誤共享,它就會變得更糟。

沒有找到任何有用的文檔。

您可以安全地假定,然后將volatile的數組引用語義是完全一樣的volatile語義非數組引用,這並不奇怪可言,考慮如何陣列(甚至是原始的)仍然對象。

暫無
暫無

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

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