Apakah! = Periksa utas aman?

140

Saya tahu bahwa operasi gabungan seperti i++tidak aman karena melibatkan beberapa operasi.

Tetapi apakah memeriksa referensi dengan sendirinya operasi yang aman?

a != a //is this thread-safe

Saya mencoba memprogram ini dan menggunakan beberapa utas tetapi tidak gagal. Saya kira saya tidak bisa mensimulasikan balapan di mesin saya.

EDIT:

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();
    }

}

Ini adalah program yang saya gunakan untuk menguji keamanan thread.

Perilaku aneh

Ketika program saya mulai di antara beberapa iterasi saya mendapatkan nilai flag output, yang berarti bahwa !=pemeriksaan referensi gagal pada referensi yang sama. TETAPI setelah beberapa iterasi output menjadi nilai konstan falsedan kemudian menjalankan program untuk waktu yang lama tidak menghasilkan trueoutput tunggal .

Seperti yang disarankan oleh output setelah beberapa iterasi n (tidak tetap), output tersebut tampaknya bernilai konstan dan tidak berubah.

Keluaran:

Untuk beberapa iterasi:

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
Narendra Pathai
sumber
2
Apa yang Anda maksud dengan "thread-safe" dalam konteks ini? Apakah Anda bertanya apakah dijamin selalu kembali salah?
JB Nizet
@JBNizet ya. Itulah yang saya pikirkan.
Narendra Pathai
5
Bahkan tidak selalu menghasilkan false dalam konteks single-threaded. Bisa jadi NaN ..
harold
4
Penjelasan yang mungkin: kode hanya dikompilasi tepat waktu dan kode yang dikompilasi memuat variabel hanya sekali. Ini diharapkan.
Marko Topolnik
3
Mencetak hasil individu adalah cara yang buruk untuk menguji ras. Mencetak (baik memformat dan menulis hasilnya) relatif mahal dibandingkan dengan tes Anda (dan kadang-kadang program Anda akhirnya akan memblokir penulisan ketika bandwidth koneksi ke terminal atau terminal itu sendiri lambat). Juga, IO sering berisi mutex-nya sendiri yang akan mengubah urutan eksekusi utas Anda (perhatikan jalur individual Anda untuk 1234:truetidak pernah saling menghancurkan ). Tes perlombaan membutuhkan loop dalam yang lebih ketat. Cetak ringkasan di bagian akhir (seperti yang dilakukan seseorang di bawah ini dengan kerangka uji unit).
Ben Jackson

Jawaban:

124

Dengan tidak adanya sinkronisasi kode ini

Object a;

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

dapat menghasilkan true. Ini adalah bytecode untuktest()

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

seperti yang dapat kita lihat, ia memuat field ake vars lokal dua kali, ini adalah operasi non-atom, jika adiubah di antaranya dengan perbandingan thread lain false.

Juga, masalah visibilitas memori relevan di sini, tidak ada jaminan bahwa perubahan yang adilakukan oleh utas lain akan terlihat oleh utas saat ini.

Evgeniy Dorofeev
sumber
22
Meski bukti kuat, bytecode sebenarnya bukan bukti. Pasti ada di suatu tempat di JLS juga ...
Marko Topolnik
10
@ Marko Saya setuju dengan pemikiran Anda, tetapi belum tentu kesimpulan Anda. Bagi saya bytecode di atas adalah cara implementasi !=yang jelas / kanonik , yang melibatkan pemuatan LHS dan RHS secara terpisah. Dan jika JLS tidak menyebutkan sesuatu yang spesifik tentang optimisasi ketika LHS dan RHS identik secara sintaksis, maka aturan umum akan berlaku, yang berarti memuat adua kali.
Andrzej Doyle
20
Sebenarnya, dengan asumsi bahwa bytecode yang dihasilkan sesuai dengan JLS, itu adalah buktinya!
proskor
6
@Adrian: Pertama: bahkan jika asumsi itu tidak valid, keberadaan kompiler tunggal di mana ia dapat mengevaluasi ke "true" sudah cukup untuk menunjukkan bahwa kadang-kadang dapat mengevaluasi ke "true" (bahkan jika spesifikasi melarangnya - yang mana ia tidak). Kedua: Java dispesifikasikan dengan baik, dan sebagian besar kompiler sangat cocok dengannya. Masuk akal untuk menggunakannya sebagai referensi dalam hal ini. Ketiga: Anda menggunakan istilah "JRE", tapi saya tidak berpikir itu berarti apa yang Anda pikirkan. . .
ruakh
2
@AlexanderTorstling - "Saya tidak yakin apakah itu cukup untuk menghalangi optimasi baca-tunggal." Itu tidak cukup. Bahkan, dengan tidak adanya sinkronisasi (dan hubungan "terjadi sebelum" tambahan yang memaksakan), optimasi itu valid,
Stephen C
47

Apakah a != athread check -aman?

Jika aberpotensi diperbarui oleh utas lainnya (tanpa sinkronisasi yang tepat!), Maka Tidak.

Saya mencoba memprogram ini dan menggunakan beberapa utas tetapi tidak gagal. Saya kira tidak bisa mensimulasikan balapan di mesin saya.

Itu tidak berarti apa-apa! Masalahnya adalah bahwa jika eksekusi yang adiperbarui oleh utas lain diizinkan oleh JLS, maka kode tersebut tidak aman-utas. Fakta bahwa Anda tidak dapat menyebabkan kondisi balapan terjadi dengan test-case tertentu pada mesin tertentu dan implementasi Java tertentu, tidak menghalangi hal itu terjadi dalam keadaan lain.

Apakah ini berarti bahwa a! = A dapat kembali true.

Ya, secara teori, dalam keadaan tertentu.

Atau, a != abisa kembali falsewalaupun asedang berubah secara bersamaan.


Tentang "perilaku aneh":

Ketika program saya mulai di antara beberapa iterasi saya mendapatkan nilai flag output, yang berarti bahwa referensi! = Cek gagal pada referensi yang sama. TETAPI setelah beberapa iterasi output menjadi nilai konstan salah dan kemudian menjalankan program untuk waktu yang lama tidak menghasilkan satu output benar tunggal.

Perilaku "aneh" ini konsisten dengan skenario eksekusi berikut:

  1. Program dimuat dan JVM mulai menafsirkan bytecodes. Karena (seperti yang telah kita lihat dari output javap) bytecode melakukan dua beban, Anda (tampaknya) melihat hasil dari kondisi balapan, kadang-kadang.

  2. Setelah beberapa saat, kode tersebut dikompilasi oleh kompiler JIT. Pengoptimal JIT memperhatikan bahwa ada dua beban slot memori yang sama ( a) saling berdekatan, dan mengoptimalkan yang kedua. (Bahkan, ada kemungkinan bahwa tes ini mengoptimalkan tes sepenuhnya ...)

  3. Sekarang kondisi balapan tidak lagi terwujud, karena tidak ada lagi dua beban.

Perhatikan bahwa ini semua konsisten dengan apa yang JLS memungkinkan implementasi Java lakukan.


@ Kriss berkomentar sebagai berikut:

Ini terlihat seperti ini yang bisa disebut oleh pemrogram C atau C ++ "Perilaku Tidak Terdefinisi" (tergantung implementasi). Sepertinya mungkin ada beberapa UB di java dalam kasus sudut seperti ini.

Java Memory Model (ditentukan dalam JLS 17.4 ) menentukan serangkaian prasyarat di mana satu utas dijamin untuk melihat nilai memori yang ditulis oleh utas lain. Jika satu utas mencoba membaca variabel yang ditulis oleh yang lain, dan prasyarat tersebut tidak terpenuhi, maka mungkin ada sejumlah eksekusi yang mungkin ... beberapa di antaranya kemungkinan tidak benar (dari perspektif persyaratan aplikasi). Dengan kata lain, himpunan perilaku yang mungkin (yaitu himpunan "eksekusi yang dilakukan dengan baik") didefinisikan, tetapi kita tidak bisa mengatakan perilaku mana yang akan terjadi.

Kompiler diperbolehkan untuk menggabungkan dan menyusun ulang beban dan menyimpan (dan melakukan hal-hal lain) asalkan efek akhir kode sama:

  • ketika dieksekusi oleh utas tunggal, dan
  • ketika dijalankan oleh utas berbeda yang disinkronkan dengan benar (sesuai Model Memori).

Tetapi jika kode tidak disinkronkan dengan benar (dan karena itu hubungan "terjadi sebelum" tidak cukup membatasi set eksekusi yang dilakukan dengan baik) kompiler diperbolehkan untuk menyusun ulang beban dan menyimpan dengan cara yang akan memberikan hasil "salah". (Tapi itu hanya mengatakan bahwa programnya salah.)

Stephen C
sumber
Apakah ini berarti itu a != abisa kembali benar?
proskor
Maksud saya, mungkin pada mesin saya, saya tidak dapat mensimulasikan bahwa kode di atas adalah non-thread safe. Jadi mungkin ada alasan teoretis di baliknya.
Narendra Pathai
@NarendraPathai - Tidak ada alasan teoritis mengapa Anda tidak dapat menunjukkannya. Mungkin ada alasan praktis ... atau mungkin Anda tidak beruntung.
Stephen C
Silakan periksa jawaban saya yang diperbarui dengan program yang saya gunakan. Kadang-kadang cek mengembalikan true tetapi tampaknya ada perilaku aneh dalam output.
Narendra Pathai
1
@NarendraPathai - Lihat penjelasan saya.
Stephen C
27

Terbukti dengan uji coba:

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);
  }

}

Saya memiliki 2 gagal pada 10.000 doa. Jadi TIDAK , ini TIDAK aman untuk thread

Arnaud Denoyelle
sumber
6
Anda bahkan tidak memeriksa kesetaraan ... Random.nextInt()bagian itu berlebihan. Anda bisa menguji dengan new Object()baik juga.
Marko Topolnik
@MarkoTopolnik Silakan periksa jawaban saya yang diperbarui dengan program yang saya gunakan. Kadang-kadang cek mengembalikan true tetapi tampaknya ada perilaku aneh dalam output.
Narendra Pathai
1
Catatan tambahan, Objek acak biasanya dimaksudkan untuk digunakan kembali, tidak dibuat setiap kali Anda membutuhkan int baru.
Simon Forsberg
15

Tidak, bukan. Untuk membandingkan Java VM harus meletakkan dua nilai untuk membandingkan pada stack dan menjalankan instruksi bandingkan (yang mana tergantung pada jenis "a").

Java VM dapat:

  1. Baca "a" dua kali, letakkan masing-masing di tumpukan lalu, lalu bandingkan hasilnya
  2. Baca "a" hanya satu kali, letakkan di tumpukan, duplikat ("dup" instruksi) dan jalankan membandingkan
  3. Hilangkan ekspresi sepenuhnya dan ganti dengan false

Dalam kasus pertama, utas lain dapat mengubah nilai "a" di antara keduanya.

Strategi mana yang dipilih tergantung pada kompiler Java dan Java Runtime (terutama kompiler JIT). Bahkan mungkin berubah selama runtime program Anda.

Jika Anda ingin memastikan bagaimana variabel diakses, Anda harus membuatnya volatile(yang disebut "penghalang setengah memori") atau menambahkan penghalang memori penuh ( synchronized). Anda juga dapat menggunakan beberapa API tingkat lebih tinggi (mis. AtomicIntegerSeperti yang disebutkan oleh Juned Ahasan).

Untuk detail tentang keamanan ulir, baca JSR 133 ( Java Memory Model ).

stefan.schwetschke
sumber
Menyatakan asebagai volatilemasih akan menyiratkan dua bacaan yang berbeda, dengan kemungkinan perubahan di antara keduanya.
Holger
6

Semuanya telah dijelaskan dengan baik oleh Stephen C. Untuk bersenang-senang, Anda dapat mencoba menjalankan kode yang sama dengan parameter JVM berikut:

-XX:InlineSmallCode=0

Ini harus mencegah optimasi yang dilakukan oleh JIT (tidak pada server hotspot 7) dan Anda akan melihat trueselamanya (saya berhenti di 2.000.000 tapi saya kira itu terus berlanjut setelah itu).

Untuk informasi, di bawah ini adalah kode JIT'ed. Sejujurnya, saya tidak membaca perakitan dengan cukup lancar untuk mengetahui apakah tes benar-benar dilakukan atau dari mana dua beban itu berasal. (jalur 26 adalah tes flag = a != adan jalur 31 adalah kurung kurawal 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   
assylias
sumber
1
Ini adalah contoh yang baik dari jenis kode yang JVM akan benar-benar hasilkan ketika Anda memiliki loop tak terbatas dan semuanya dapat lebih atau kurang dapat diangkat. "Loop" yang sebenarnya di sini adalah tiga instruksi dari 0x27dccd1ke 0x27dccdf. Di jmpdalam loop adalah tanpa syarat (karena loop tidak terbatas). Satu-satunya dua instruksi lain dalam loop adalah add rbc, 0x1- yang meningkat countOfIterations(terlepas dari kenyataan bahwa loop tidak akan pernah keluar sehingga nilai ini tidak akan dibaca: mungkin diperlukan jika Anda membobolnya dalam debugger), .. .
BeeOnRope
... dan testinstruksi yang tampak aneh , yang sebenarnya hanya ada untuk akses memori (catatan yang eaxbahkan tidak pernah diatur dalam metode!): ini adalah halaman khusus yang diatur untuk tidak dapat dibaca ketika JVM ingin memicu semua utas untuk mencapai titik aman, sehingga dapat melakukan gc atau operasi lain yang mengharuskan semua utas berada dalam kondisi yang diketahui.
BeeOnRope
Lebih penting lagi, JVM sepenuhnya mengangkat instance. a != instance.aperbandingan dari loop, dan hanya melakukan sekali, sebelum loop dimasukkan! Ia tahu bahwa itu tidak diperlukan untuk memuat ulang instanceatau akarena mereka tidak dinyatakan volatile dan tidak ada kode lain yang dapat mengubahnya pada utas yang sama, jadi itu hanya menganggap mereka sama selama seluruh loop, yang diizinkan oleh memori model.
BeeOnRope
5

Tidak, a != abukan utas yang aman. Ungkapan ini terdiri dari tiga bagian: memuat a, memuat alagi, dan melakukan !=. Dimungkinkan untuk utas lainnya untuk mendapatkan kunci intrinsik pada ainduk dan mengubah nilai adi antara 2 operasi pemuatan.

Namun faktor lain adalah apakah alokal. Jika abersifat lokal maka tidak ada utas lain yang memiliki akses ke sana dan karenanya harus aman.

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

juga harus selalu dicetak false.

Mendeklarasikan aseperti volatiletidak akan menyelesaikan masalah karena jika aini staticatau contoh. Masalahnya bukan bahwa utas memiliki nilai yang berbeda a, tetapi satu utas memuat adua kali dengan nilai yang berbeda. Ini sebenarnya bisa membuat case lebih aman dari thread .. Jika atidak volatilemaka abisa di-cache dan perubahan di thread lain tidak akan mempengaruhi nilai cache.

DoubleMx2
sumber
Contoh Anda dengan synchronizedsalah: untuk kode yang dijamin untuk dicetak false, semua metode yang ditetapkan a juga harus synchronized.
ruakh
Kenapa begitu? Jika metode ini disinkronkan bagaimana utas lain akan mendapatkan kunci intrinsik pada ainduk saat metode ini dijalankan, perlu untuk mengatur nilainya a.
DoubleMx2
1
Tempatmu salah. Anda dapat mengatur bidang objek tanpa memperoleh kunci intrinsiknya. Java tidak memerlukan utas untuk mendapatkan kunci intrinsik objek sebelum mengatur bidangnya.
ruakh
3

Mengenai perilaku aneh:

Karena variabel atidak ditandai sebagai volatile, pada titik tertentu mungkin nilai acache di-cache. Kedua as a != akemudian versi cache dan dengan demikian selalu sama (artinya flagsekarang selalu false).

Walter Laan
sumber
0

Bahkan bacaan sederhana bukanlah atom. Jika aini longdan tidak ditandai sebagai volatilekemudian pada 32-bit JVMs long b = atidak thread-safe.

ZhekaKozlov
sumber
volatile dan atomicity tidak berhubungan. bahkan jika saya menandai volatile itu akan non-atomik
Narendra Pathai
Penugasan bidang panjang yang mudah menguap selalu atom. Operasi lain seperti ++ tidak.
ZhekaKozlov