! = Kontrol ipliği güvenli mi?


140

Birden fazla işlem i++içerdiğinden, bu tür bileşik işlemlerin iş parçacığı için güvenli olmadığını biliyorum .

Ancak referansın kendisiyle kontrol edilmesi güvenli bir işlem midir?

a != a //is this thread-safe

Bunu programlamaya ve birden çok iş parçacığı kullanmaya çalıştım ama başarısız olmadı. Sanırım makinemde yarışı simüle edemedim.

DÜZENLE:

public class TestThreadSafety {
    private Object a = new Object();

    public static void main(String[] args) {

        final TestThreadSafety instance = new TestThreadSafety();

        Thread testingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                long countOfIterations = 0L;
                while(true){
                    boolean flag = instance.a != instance.a;
                    if(flag)
                        System.out.println(countOfIterations + ":" + flag);

                    countOfIterations++;
                }
            }
        });

        Thread updatingReferenceThread = new Thread(new Runnable() {

            @Override
            public void run() {
                while(true){
                    instance.a = new Object();
                }
            }
        });

        testingReferenceThread.start();
        updatingReferenceThread.start();
    }

}

Bu, iplik güvenliğini test etmek için kullandığım program.

Tuhaf davranış

Programım bazı yinelemeler arasında başladığında, çıkış bayrağı değerini alıyorum, bu da referans !=kontrolünün aynı referansta başarısız olduğu anlamına geliyor . ANCAK bazı tekrarlamalardan sonra çıktı sabit değer haline gelir falseve daha sonra programı uzun süre yürütmek tek bir trueçıktı üretmez .

Çıktının bazı n (sabit olmayan) yinelemelerden sonra önerdiği gibi, çıktı sabit bir değer gibi görünür ve değişmez.

Çıktı:

Bazı iterasyonlar için:

1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true

2
Bu bağlamda "iş parçacığı açısından güvenli" ile ne demek istiyorsun? Her zaman yanlış döndürülmesinin garanti edilip edilmediğini mi soruyorsun?
JB Nizet

@JBNizet evet. Ben de öyle düşünüyordum.
Narendra Pathai

5
Tek iş parçacıklı bir bağlamda her zaman yanlış döndürmez. Bir NaN olabilir ..
harold

4
Olası açıklama: kod tam zamanında derlenmiş ve derlenmiş kod değişkeni yalnızca bir kez yükler. Bu bekleniyor.
Marko Topolnik

3
Bireysel sonuçları yazdırmak, yarışları test etmenin kötü bir yoludur. Yazdırma (hem biçimlendirme hem de sonuçları yazma) testinizle karşılaştırıldığında nispeten maliyetlidir (ve bazen terminal veya terminale bağlantının bant genişliği yavaş olduğunda programınız yazma sırasında engellenir). Ayrıca, IO genellikle dişlerinizin yürütme sırasına izin verecek olan kendi mutekslerini içerir (bireysel çizgilerinizi 1234:trueasla birbirinizi parçalamayın ). Bir yarış testi daha sıkı bir iç döngüye ihtiyaç duyar. Sonunda bir özet yazdırın (birisinin aşağıda birim test çerçevesiyle yaptığı gibi).
Ben Jackson

Yanıtlar:


124

Senkronizasyon olmadığında bu kod

Object a;

public boolean test() {
    return a != a;
}

üretebilir true. Bu bayt kodutest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

aGörebildiğimiz gibi, alanı iki kez yerel değişkenlere yükler , bu atomik olmayan bir işlemdir, eğer abaşka bir iş parçacığı karşılaştırması arasında değişebilirse üretebilir false.

Ayrıca, burada bellek görünürlük sorunu söz konusudur, abaşka bir evre tarafından yapılan değişikliklerin mevcut evre tarafından görüleceği garantisi yoktur .


22
Güçlü kanıtlar olmasına rağmen bayt kodu aslında bir kanıt değildir.
JLS'de

10
@Marko Düşüncelerinize katılıyorum, ancak sonuca mutlaka uymuyorum. Bana göre yukarıdaki bayt kodu !=, LHS ve RHS'nin ayrı olarak yüklenmesini içeren bariz / kanonik uygulama yoludur . Dolayısıyla, JLS , LHS ve RHS sözdizimsel olarak özdeş olduğunda optimizasyon hakkında spesifik bir şeyden bahsetmezse, genel kural geçerli olacaktır, bu da aiki kez yükleme anlamına gelir .
Andrzej Doyle

20
Aslında, oluşturulan bayt kodunun JLS'ye uygun olduğunu varsayarsak, bu bir kanıttır!
proskor

6
@Adrian: Öncelikle: bu varsayım geçersiz olsa bile, "true" olarak değerlendirebileceği tek bir derleyicinin varlığı , bazen "true" olarak değerlendirilebildiğini göstermek için yeterlidir (spec onu yasaklasa bile - ki ) buna gerek yoktur. İkinci olarak: Java iyi tanımlanmıştır ve çoğu derleyici buna çok uygundur. Bunları referans olarak kullanmak mantıklıdır. Üçüncüsü: "JRE" terimini kullanıyorsunuz, ama bunun ne anlama geldiğini düşündüğünüzü sanmıyorum. . .
ruakh

2
@AlexanderTorstling - " Yine de bunun tek okuma optimizasyonunu engellemek için yeterli olup olmadığından emin değilim." Yeterli değil. Aslında, senkronizasyon yokluğunda (ve empoze edilen ekstra "daha önce olur" ilişkileri), optimizasyon geçerlidir,
Stephen C

47

a != aİpliği kontrol et güvenli mi?

Eğer apotansiyel (doğru senkronizasyon olmadan!) Başka bir iş parçacığı tarafından güncellenebilir, daha sonra No.

Bunu programlamaya ve birden çok iş parçacığı kullanmaya çalıştım ama başarısız olmadı. Sanırım makinemdeki yarışı simüle edemedi.

Bu hiçbir şey ifade etmiyor! Sorun, abaşka bir iş parçacığı tarafından güncelleştirilen bir yürütme JLS tarafından izin verilirse , kod iş parçacığı için güvenli değil olmasıdır. Belirli bir makinede belirli bir test senaryosunda ve belirli bir Java uygulamasında yarış koşulunun gerçekleşmesine neden olamamanız, diğer durumlarda gerçekleşmesini engellemez.

! = Bir geri dönebilirler bir o anlamına mı geliyor true.

Evet, teorik olarak, belirli koşullar altında.

Alternatif olarak, aynı anda değişmesine rağmen a != ageri dönebilir .falsea


"Tuhaf davranış" ile ilgili olarak:

Programım bazı yinelemeler arasında başladığında, çıkış bayrağı değerini alıyorum, yani başvuru! = Check aynı başvuruda başarısız oluyor. ANCAK bazı tekrarlamalardan sonra çıktı sabit değer yanlış olur ve daha sonra programı uzun süre yürütmek tek bir gerçek çıktı üretmez.

Bu "garip" davranış, aşağıdaki yürütme senaryosuyla tutarlıdır:

  1. Program yüklenir ve JVM bayt kodlarını yorumlamaya başlar . (Javap çıkışından gördüğümüz gibi) bayt kodu iki yük yaptığından, (görünüşe göre) bazen yarış durumunun sonuçlarını görürsünüz.

  2. Bir süre sonra, kod JIT derleyicisi tarafından derlenir. JIT optimizer a, birbirine yakın aynı bellek yuvasının ( ) iki yükünün olduğunu fark eder ve ikincisini uzaklaştırır. (Aslında, testi tamamen optimize etme şansı var ...)

  3. Artık yarış koşulu artık tezahür etmiyor, çünkü artık iki yük yok.

Tüm bunların , JLS'nin Java uygulamasının yapmasına izin verdiği şeyle tutarlı olduğunu unutmayın .


@kriss şöyle yorumladı:

Bu, C veya C ++ programcılarının "Tanımsız Davranış" (uygulamaya bağlı) olarak adlandırdığı gibi olabilir. Bu gibi köşe durumlarda java birkaç UB olabilir gibi görünüyor.

Java Bellek Modeli ( JLS 17.4'te belirtilmiştir), bir iş parçacığının başka bir iş parçacığı tarafından yazılan bellek değerlerini göreceği garanti edilen bir dizi ön koşulu belirtir. Bir iş parçacığı başka bir değişken tarafından yazılan bir değişkeni okumaya çalışırsa ve bu önkoşullar sağlanamazsa, bir dizi olası yürütme olabilir ... bazıları muhtemelen yanlıştır (uygulamanın gereksinimleri açısından). Başka bir deyişle, olası davranışlar kümesi (yani "iyi biçimlendirilmiş yürütmeler" kümesi) tanımlanır, ancak bu davranışlardan hangisinin gerçekleşeceğini söyleyemeyiz.

Kodun son etkisi aynı olduğu sürece, derleyicinin yükleri birleştirmesine ve yeniden düzenlemesine ve kaydetmesine (ve başka şeyler yapmasına) izin verilir:

  • tek bir iş parçacığı tarafından yürütüldüğünde ve
  • doğru şekilde senkronize edilen farklı iş parçacıkları tarafından yürütüldüğünde (Bellek Modeline göre).

Ancak kod düzgün şekilde senkronize edilmezse (ve dolayısıyla "önce gerçekleşir" ilişkileri yeterince iyi yapılandırılmış yürütme kümesini yeterince kısıtlamıyorsa), derleyicinin yükleri ve depoları "yanlış" sonuçlar verecek şekilde yeniden sıralamasına izin verilir. (Ama bu sadece programın yanlış olduğunu söylüyor.)


Bu a != a, bunun doğru olabileceği anlamına mı geliyor ?
proskor

Belki benim makinemde yukarıdaki kodun iplik güvenli olmadığını simüle edemezdim demekti. Belki de arkasında teorik bir akıl vardır.
Narendra Pathai

@NarendraPathai - Gösterememenizin teorik bir nedeni yok. Muhtemelen pratik bir sebep var ... ya da belki şanslı değilsiniz.
Stephen C

Lütfen kullandığım programla güncel cevabımı kontrol et. Kontrol bazen doğru olur, ancak çıktıda garip bir davranış var gibi görünüyor.
Narendra Pathai

1
@NarendraPathai - Açıklamamı görün.
Stephen C

27

Test ile kanıtlanmıştır:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

10.000 çağrıda 2 başarısızlık var. Yani NO , bu edilmektedir DEĞİL parçacığı güvenli


6
Eşitliği bile kontrol etmiyorsun ... Random.nextInt()kısım gereksiz. Siz de test edebilirsiniz new Object().
Marko Topolnik

@MarkoTopolnik Lütfen kullandığım programla güncel cevabımı kontrol edin. Kontrol bazen doğru olur, ancak çıktıda garip bir davranış var gibi görünüyor.
Narendra Pathai

1
Yan not, Rastgele nesneler genellikle yeniden kullanılmalıdır, her yeni int'e ihtiyacınız olduğunda oluşturulmaz.
Simon Forsberg

15

Hayır öyle değil. Karşılaştırma için Java VM iki değeri yığın üzerinde karşılaştırmalı ve karşılaştırma komutunu çalıştırmalıdır (ki bu "a" türüne bağlıdır).

Java VM şunları yapabilir:

  1. "A" yı iki kez okuyun, her birini yığına koyun ve ardından sonuçları karşılaştırın
  2. "A" yı sadece bir kez okuyun, yığına koyun, çoğaltın ("dup" komutu) ve karşılaştırmayı çalıştırın
  3. İfadeyi tamamen ortadan kaldırın ve onunla değiştirin false

İlk durumda, başka bir iş parçacığı iki okuma arasında "a" değerini değiştirebilir.

Hangi strateji seçilirse Java derleyicisine ve Java Runtime'a (özellikle JIT derleyicisine) bağlıdır. Programınızın çalışması sırasında bile değişebilir.

Değişkenin nasıl erişildiğinden emin olmak istiyorsanız, volatile("yarı bellek bariyeri") yapmalısınız veya tam bir bellek bariyeri ( synchronized) eklemelisiniz . Daha yüksek seviyeli bir API da kullanabilirsiniz (örneğin AtomicIntegerJuned Ahasan'ın belirttiği gibi).

İş parçacığı güvenliği hakkında ayrıntılar için, JSR 133'ü ( Java Bellek Modeli ) okuyun.


Bildirmek aolarak volatilehala arada bir değişim imkanı ile, ayrı iki okur ima edecektir.
Holger

6

Her şey Stephen C tarafından iyi açıklanmıştır. Eğlenmek için, aşağıdaki JVM parametreleriyle aynı kodu çalıştırmayı deneyebilirsiniz:

-XX:InlineSmallCode=0

Bu, JIT tarafından yapılan optimizasyonu önlemelidir (hotspot 7 sunucusunda yapar) ve truesonsuza kadar göreceksiniz (2.000.000'da durdum, ancak bundan sonra devam ediyor).

Bilgi için aşağıda JIT kodu verilmiştir. Dürüst olmak gerekirse, montajın testin gerçekten yapılıp yapılmadığını veya iki yükün nereden geldiğini bilecek kadar akıcı okumam. (satır 26 testtir flag = a != ave satır 31, kapama köşebentidir while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   

1
Bu, sonsuz bir döngüye sahip olduğunuzda JVM'nin üreteceği kod türünün iyi bir örneğidir ve her şey az çok kaldırılabilir. Burada asıl "döngü" den üç talimatlar verilmiştir 0x27dccd1için 0x27dccdf. jmp(Döngü sonsuz olduğundan) döngüde mutlak gereklidir. Döngüdeki diğer iki yönerge add rbc, 0x1- artmaktadır countOfIterations(bu değer okunmayacak şekilde döngüden asla çıkılmayacak olmasına rağmen: hata ayıklayıcıya girmeniz durumunda belki de gereklidir), .. .
BeeOnRope

... ve testaslında sadece bellek erişimi için var olan garip görünümlü talimat ( eaxasla yöntemde ayarlanmadığını unutmayın !): JVM tüm iş parçacıklarını tetiklemek istediğinde okunamayacak şekilde ayarlanmış özel bir sayfa bir güvenli noktaya ulaşmak için gc ya da tüm iş parçacıklarının bilinen bir durumda olmasını gerektiren başka bir işlem yapabilir.
BeeOnRope

Daha da önemlisi, JVM instance. a != instance.akarşılaştırmayı döngüden tamamen kaldırdı ve döngü girmeden önce sadece bir kez gerçekleştirdi! Yeniden yüklemenin gerekli olmadığını instanceveya auçucu olarak bildirilmediklerini ve bunları aynı iş parçacığında değiştirebilecek başka bir kod olmadığını bilir, bu nedenle bellek tarafından izin verilen tüm döngü boyunca aynı olduklarını varsayar. modeli.
BeeOnRope

5

Hayır, a != aiş parçacığı için güvenli değildir. Bu ifade üç bölümden oluşur: yükleme a, atekrar yükleme ve gerçekleştirme !=. Başka bir iş parçacığının aebeveyni üzerinde içsel kilit kazanması ave 2 yükleme işlemi arasındaki değeri değiştirmesi mümkündür .

Bir başka faktör aise yerel olup olmadığıdır . aYerel ise , başka hiçbir iş parçacığının buna erişimi olmamalıdır ve bu nedenle iş parçacığı için güvenli olmalıdır.

void method () {
    int a = 0;
    System.out.println(a != a);
}

ayrıca her zaman yazdırmalıdır false.

Bildirmek aolarak volatileeğer için sorunu çözmek olmaz aise staticveya örneği. Sorun, iş parçacıklarının farklı değerleri olması adeğil, bir iş parçacığının afarklı değerlerle iki kez yüklenmesi olmasıdır . Aslında yapabilir durumda daha az iş parçacığı güvenli .. Eğer adeğil volatiledaha sonra aönbelleğe alınmış olabilir ve başka bir iş parçacığı bir değişiklik önbelleğe değerini etkilemez.


İle örneğiniz synchronizedyanlış: Bu kodun yazdırılması garanti edilecekse , ayarlananfalse tüm yöntemlerin de olması gerekir . asynchronized
ruakh

Neden öyle? Yöntem senkronize edilirse, yöntem ayürütülürken başka bir iş parçacığının ebeveyni üzerinde nasıl kilit kazanacağı değeri ayarlamak için gereklidir a.
DoubleMx2

1
Tesisleriniz yanlış. Bir nesnenin alanını kendi iç kilidini almadan ayarlayabilirsiniz. Java, alanlarını ayarlamadan önce bir nesnenin gerçek kilidini elde etmek için bir iş parçacığı gerektirmez.
ruakh

3

Tuhaf davranışlarla ilgili olarak:

Değişken aolarak işaretlenmediğinden volatile, bir noktada değeri aiş parçacığı tarafından önbelleğe alınabilir. Hem as a != asonra önbelleğe sürümü ve böylece her zaman aynı (anlam vardır flagher zaman şimdi false).


0

Basit okuma bile atomik değildir. Eğer aedilir longve olarak işaretlenmemiş volatile32 bit JVMs daha sonra long b = aevreli değil.


uçucu ve atomisite ilgisizdir. Bir uçuculuğu işaretlesem bile atomik olmayacak
Narendra Pathai

Uçucu uzun bir alanın atanması her zaman atomiktir. ++ gibi diğer işlemler değildir.
ZhekaKozlov
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.