it-swarm-tr.com

Bir Java.util.HashMap'ten birden fazla iş parçacığından değerler almak güvenli midir (değişiklik yok)?

Bir haritanın inşa edileceği bir durum var ve bir kez başlatıldığında, bir daha asla değiştirilmeyecek. Bununla birlikte, çoklu iş parçacıklarından (sadece get (anahtar) ile) erişilebilir. Bu şekilde bir Java.util.HashMap kullanmak güvenli midir?

(Şu anda, mutlu bir Java.util.concurrent.ConcurrentHashMap kullanıyorum ve performansı artırmaya gerek duymadım, ancak basit bir HashMap yeterli olup olmadığını merak ediyorum. Dolayısıyla, bu soru değil "Hangisini kullanmalıyım? "ne de bir performans sorusu değil. Aksine, soru" Güvenli olur mu? "

118
Dave L.

Deyiminiz güvenlidir if ve sadeceHashMap referansı güvenli bir şekilde yayınlandıysa. HashMap 'in kendi içindekilerine ilişkin her şeyden ziyade, safe yayın, yapıcı iş parçacığının haritaya referansı diğer iş parçacıkları için nasıl görünür kıldığı ile ilgilidir.

Temel olarak, buradaki tek olası yarış HashMap yapımı ile tamamen inşa edilmeden önce erişebilecek herhangi bir okuma dizisi arasındadır. Tartışmanın çoğu, harita nesnesinin durumuna ne olacağı ile ilgilidir, ancak onu hiçbir zaman değiştiremediğinizden bu konuyla ilgisizdir - bu nedenle tek ilginç kısım HashMap referansının yayınlanmasıdır. 

Örneğin, haritayı şöyle yayınladığınızı hayal edin:

class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}

... ve bir noktada setMap() bir harita ile çağrılır ve diğer iş parçacıkları, haritaya erişmek için SomeClass.MAP kullanıyor ve aşağıdaki gibi null değerini kontrol ediyor:

HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}

Bu güvenli değildir muhtemelen olduğu gibi görünmesine rağmen. Buradaki sorun, SomeObject.MAP kümesi ile daha sonra başka bir iş parçacığı üzerinde okunan okuma arasında olmasından önce ilişki olmamasıdır, bu nedenle okuma iş parçacığı kısmen inşa edilmiş bir harita görmekte özgürdür. Bu, herhangi bir şeyi yapabilir ve pratikte bile, { okuma dizisini sonsuz bir döngüye koy gibi şeyler yapar.

Haritayı güvenli bir şekilde yayınlamak için, [] referansının [] yazısının HashMap (yani, yayın) ile sonraki referansları (yayın arasında bir olmasından önce ilişki kurmanız gerekir. yani tüketim). Elverişli bir şekilde, başarmak ’i hatırlamanın sadece birkaç kolay yolu vardır.[1]:

  1. Referansı uygun şekilde kilitlenmiş bir alandan değiştirin ( JLS 17.4.5 )
  2. Hazırlama depolarını yapmak için statik başlatıcı kullanın ( JLS 12.4 )
  3. Referansı geçici bir alan ( JLS 17.4.5 ) veya bu kuralın bir sonucu olarak AtomicX sınıfları aracılığıyla paylaşın
  4. Değeri son bir alana sıfırlayın ( JLS 17.5 ).

Senaryonuz için en ilginç olanlar (2), (3) ve (4). Özellikle, (3) doğrudan benim üstümde bulunan koda uygulanır: MAP bildirgesini şuna dönüştürürseniz:

public static volatile HashMap<Object, Object> MAP;

o zaman her şey koşuttur: null olmayan değeri gören okuyucular mutlaka MAP ile mağaza ile bir olur-önce ilişki kurar ve bu nedenle harita başlatma ile ilgili tüm mağazaları görür.

Diğer yöntemler, yönteminizin anlamını değiştirir, çünkü hem (2) (statik başlatıcıyı kullanarak) hem de (4) (final kullanarak) çalışma zamanında MAP öğesini dinamik olarak ayarlayamazsınız anlamına gelir. Bunu yapmak için gerekmiyorsa, o zaman MAP öğesini static final HashMap<> olarak bildirmeniz yeterlidir;.

Uygulamada, kurallar "asla değiştirilmemiş nesnelere" güvenli erişim için basittir:

Doğal olarak değişmez bir nesne yayınlıyorsanız (final tarafından bildirilen tüm alanlarda olduğu gibi) ve:

  • Bildirim anında atanacak nesneyi zaten oluşturabilirsinizbir: sadece bir final alanı kullanın (statik üyeler için static final dahil).
  • Daha önce başvuru yapıldıktan sonra nesneyi atamak istediğinizde: geçici bir alan kullanınb.

Bu kadar!

Uygulamada çok etkilidir. Örneğin bir static final alanının kullanılması, JVM'nin, programın ömrü boyunca değerin değişmediğini varsaymasına ve yoğun olarak optimize etmesine izin verir. final üyesi bir alanın kullanılması, çoğu mimarinin alanı normal alan okumasına eşdeğer bir şekilde okumasına izin verir ve daha fazla optimizasyonu engellemezc

Son olarak, volatile kullanımının bir etkisi vardır: birçok mimaride (örneğin, okumaların geçmesine izin vermeyenler için x86 gibi) hiçbir donanım engeli gerekmez, ancak derleme zamanında bazı optimizasyon ve yeniden sıralama işlemleri gerçekleşmeyebilir - ancak bu etki genellikle küçüktür. Buna karşılık, aslında istediğinizden daha fazlasını elde edersiniz - yalnızca bir HashMap güvenli bir şekilde yayınlayamazsınız, aynı referans için istediğiniz kadar değiştirilmemiş HashMaps saklayabilir ve tüm okuyucuların güvenli bir şekilde yayınlanmış olarak göreceğinden emin olabilirsiniz. haritası.

Daha fazla kanlı ayrıntı için Shipilev veya bu FAQ Manson ve Goetz kısmına bakın.


[1] Doğrudan shipilev 'den alıntı yapmak.


bir Kulağa karmaşık geliyor, ancak demek istediğim, referansı yapım zamanında atayabilirsiniz - bildirim noktasında veya yapıcıda (üye alanları) veya statik başlatıcıda (statik alanlar).

b İsteğe bağlı olarak, almak/ayarlamak için bir synchronized yöntemi veya bir AtomicReference veya başka bir şey kullanabilirsiniz, ancak yapabileceğiniz minimum işden bahsediyoruz.

c Çok zayıf bellek modellerine sahip bazı mimariler (you, Alpha'ya bakıyorum) final okumadan önce bir tür okuma engeline ihtiyaç duyabilir - ancak bunlar bugün çok nadir görülür.

31
BeeOnRope

Java Bellek Modeli'ne gelince tanrı Jeremy Manson'un bu konuyla ilgili üç bölümden oluşan bir blogu var - çünkü özünde "Değişmez bir HashMap'e erişmek güvenli mi" sorusunu soruyorsun - bunun cevabı evet. Fakat bu soruyu açıklamak zorundasınız - "HashMap'ım değişmez mi". Cevap sizi şaşırtabilir - Java değişmezliği belirlemek için nispeten karmaşık kurallara sahiptir.

Konuyla ilgili daha fazla bilgi için Jeremy'nin blog yazılarını okuyun:

Java'da İmkansızlık Üzerine 1. Bölüm: http://jeremymanson.blogspot.com/2008/04/immutability-in-Java.html

Java'da İmkansızlık Üzerine 2. Bölüm: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-2.html

Java'da İmkansızlık Üzerine 3. Bölüm: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-3.html

70
Taylor Gautier

Okumalar senkronizasyon açısından güvenlidir ancak bellek açısından güvenli değildir. Bu, Stackoverflow da dahil olmak üzere Java geliştiricileri arasında yaygın olarak yanlış anlaşılan bir şey. (Kanıt için bu cevabın derecesine dikkat edin.)

Çalışan başka bir iş parçacığınız varsa, geçerli iş parçacığından herhangi bir bellek yazmak yoksa HashMap'in güncellenmiş bir kopyasını görmeyebilir. Bellek yazıları, senkronize edilmiş veya geçici anahtar kelimeler kullanılarak veya bazı Java eşzamanlılık yapıları kullanılarak gerçekleşir.

Ayrıntılar için Brian Goetz'in yeni Java Bellek Modeli ile ilgili makalesine bakın. 

34
Heath Borders

Biraz daha baktıktan sonra, bunu Java doc (vurgu madeninde) de buldum:

Bu uygulamanın .__ olmadığını unutmayın. Senkronize. Eğer çoklu iş parçacığı eşzamanlı olarak bir karma haritasına erişin ve at. yivlerden en az bir tanesi .__ değiştirir. yapısal olarak harita, olması gerekir. harici olarak senkronize edilir. (Yapısal Modifikasyonu, Bir veya daha fazla harita ekleyen veya silen herhangi bir işlemdir; değişiklik.)

Bu, ifadenin tersine doğru olduğunu varsayarak, güvenli olacağını ima ediyor gibi görünüyor.

10
Dave L.

Bir not, bazı durumlarda, senkronize edilmemiş bir HashMap’ten bir get () öğesinin sonsuz bir döngüye neden olabileceğidir. Bu, eşzamanlı bir put (), Haritanın yeniden şekillenmesine neden olursa oluşabilir.

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

9
Alex Miller

Yine de önemli bir bükülme var. Haritaya erişmek güvenlidir, ancak genel olarak tüm iş parçacıklarının HashMap’in aynı durumunu (ve dolayısıyla değerlerini) göreceği garanti edilmez. Bu, bir iş parçacığı tarafından yapılan HashMap'taki değişikliklerin (örneğin, onu dolduranın) yapılan işlemcinin önbelleğinde oturabildiği ve bir bellek çiti işlemi gerçekleşene kadar diğer CPU'larda çalışan iş parçacıkları tarafından görülmeyeceği çok işlemcili sistemlerde olabilir. önbellek tutarlılığını sağlamak için gerçekleştirildi. Java Language Specification bu konuda açık: çözüm, bellek çit işlemi yayan bir kilit (senkronize (...)) elde etmektir. Bu nedenle, eğer HashMap'i doldurduktan sonra, her bir iş parçacığının HERHANGİ bir kilit edindiğinden eminseniz, o zamandan itibaren HashMap'e tekrar herhangi bir iş parçasından HashMap'a erişmek için tamam mı?.

8
Alexander

http://www.ibm.com/developerworks/Java/library/j-jtp03304/ #'e göre Başlatma emniyeti, HashMap'inizi nihai bir alan haline getirebilir ve yapıcı bittikten sonra güvenle yayınlanacaktır.

Yeni bellek modelinde, bir kurucudaki son alanın yazılması ile başka bir iş parçacığındaki bu nesneye paylaşılan bir referansın ilk yükü arasında bir önceki-bir ilişkiye benzer bir şey var. ...

4
bodrin

Yani tarif ettiğiniz senaryo bir Haritaya bir sürü veri koymanız gerektiği, ardından doldurmayı tamamladığınızda değişmez olarak kabul etmenizdir. "Güvenli" olan (yaklaşımı gerçekten değişmez olarak kabul ettiğiniz için zorladığınız anlamına gelen) bir yaklaşım, değişmez hale getirmeye hazır olduğunuzda referansı Collections.unmodifiableMap(originalMap) ile değiştirmektir.

Eşzamanlı olarak kullanılırsa haritaların ne kadar kötü sonuç verebileceğinin bir örneği ve bahsettiğim önerilen geçici çözüm için, bu hata geçit girişine göz atın: bug_id = 6423457

3
Will

Tek iş parçacıklı kodda bile, bir ConcurrentHashMap'i HashMap ile değiştirmenin güvenli olmayabileceği konusunda uyarılırsınız. ConcurrentHashMap null'u anahtar veya değer olarak yasaklar. HashMap onları yasaklamaz (sormaz).

Bu nedenle, olası kodunuzda kurulum sırasında (muhtemelen bir tür arıza durumunda) koleksiyona boş bir değer eklenebilir, koleksiyonun açıklandığı şekilde değiştirilmesi işlevsel davranışı değiştirir.

Bununla birlikte, başka hiçbir şey yapmazsanız, HashMap'ten eşzamanlı olarak okunan bilgiler güvenlidir. 

[Düzenle: "eşzamanlı okuma" ile, aynı zamanda eşzamanlı değişiklik olmadığını da kastediyorum.

Diğer cevaplar bunun nasıl sağlanacağını açıklar. Bir yol haritayı değiştirilemez kılmaktır, ancak gerekli değildir. Örneğin, JSR133 bellek modeli, bir iş parçacığının başlatılmasını açık bir şekilde senkronize edilmiş bir işlem olarak tanımlar; bu, iş parçacığı B'de iş parçacığı B başlamadan önce yapılan değişikliklerin iş parçacığı B'de görülebileceği anlamına gelir.

Niyetim, Java Bellek Modeli ile ilgili daha ayrıntılı cevaplarla çelişmemek. Bu yanıt, eşzamanlılık sorunlarının yanı sıra, ConcurrentHashMap ve HashMap arasında, biriyle değiştirilen tek bir iş parçacıklı programı bile kaplayabilecek en az bir API farkı olduğunu belirtmek için tasarlanmıştır.

1
Steve Jessop

Başlatma ve her koyma senkronize edilirse, tasarruf edilir.

Sınıf yükleyici senkronizasyon ile ilgileneceği için aşağıdaki kod kaydedilir:

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

Aşağıdaki kod saklanır çünkü uçucu bir yazı senkronizasyon ile ilgilenecektir.

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

Bu aynı zamanda, eğer üye değişken nihai ise, final de geçici olduğu için işe yarayacaktır. Ve eğer yöntem bir kurucu ise.

0
TomWolk

http://www.docjar.com/html/api/Java/util/HashMap.Java.html

hashMap için kaynak burada. Söyleyebileceğiniz gibi, kesinlikle hiçbir kilitleme/muteks kodu yoktur.

Bu, çok iş parçacıklı bir durumda bir HashMap'ten okumaya hazır olsa da, birden fazla yazma olsaydı kesinlikle bir ConcurrentHashMap kullanacağım anlamına gelir.

İlginç olan, hem .NET HashTable hem de Sözlük <K, V> senkronizasyon kodunda yerleşik olmalarıdır.

0
FlySwat