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 false
dan kemudian menjalankan program untuk waktu yang lama tidak menghasilkan true
output 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
sumber
1234:true
tidak 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).Jawaban:
Dengan tidak adanya sinkronisasi kode ini
dapat menghasilkan
true
. Ini adalah bytecode untuktest()
seperti yang dapat kita lihat, ia memuat field
a
ke vars lokal dua kali, ini adalah operasi non-atom, jikaa
diubah di antaranya dengan perbandingan thread lainfalse
.Juga, masalah visibilitas memori relevan di sini, tidak ada jaminan bahwa perubahan yang
a
dilakukan oleh utas lain akan terlihat oleh utas saat ini.sumber
!=
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 memuata
dua kali.Jika
a
berpotensi diperbarui oleh utas lainnya (tanpa sinkronisasi yang tepat!), Maka Tidak.Itu tidak berarti apa-apa! Masalahnya adalah bahwa jika eksekusi yang
a
diperbarui 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.Ya, secara teori, dalam keadaan tertentu.
Atau,
a != a
bisa kembalifalse
walaupuna
sedang berubah secara bersamaan.Tentang "perilaku aneh":
Perilaku "aneh" ini konsisten dengan skenario eksekusi berikut:
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.
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 ...)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:
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:
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.)
sumber
a != a
bisa kembali benar?Terbukti dengan uji coba:
Saya memiliki 2 gagal pada 10.000 doa. Jadi TIDAK , ini TIDAK aman untuk thread
sumber
Random.nextInt()
bagian itu berlebihan. Anda bisa menguji dengannew Object()
baik juga.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:
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.AtomicInteger
Seperti yang disebutkan oleh Juned Ahasan).Untuk detail tentang keamanan ulir, baca JSR 133 ( Java Memory Model ).
sumber
a
sebagaivolatile
masih akan menyiratkan dua bacaan yang berbeda, dengan kemungkinan perubahan di antara keduanya.Semuanya telah dijelaskan dengan baik oleh Stephen C. Untuk bersenang-senang, Anda dapat mencoba menjalankan kode yang sama dengan parameter JVM berikut:
Ini harus mencegah optimasi yang dilakukan oleh JIT (tidak pada server hotspot 7) dan Anda akan melihat
true
selamanya (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 != a
dan jalur 31 adalah kurung kurawalwhile(true)
).sumber
0x27dccd1
ke0x27dccdf
. Dijmp
dalam loop adalah tanpa syarat (karena loop tidak terbatas). Satu-satunya dua instruksi lain dalam loop adalahadd rbc, 0x1
- yang meningkatcountOfIterations
(terlepas dari kenyataan bahwa loop tidak akan pernah keluar sehingga nilai ini tidak akan dibaca: mungkin diperlukan jika Anda membobolnya dalam debugger), .. .test
instruksi yang tampak aneh , yang sebenarnya hanya ada untuk akses memori (catatan yangeax
bahkan 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.instance. a != instance.a
perbandingan dari loop, dan hanya melakukan sekali, sebelum loop dimasukkan! Ia tahu bahwa itu tidak diperlukan untuk memuat ulanginstance
ataua
karena 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.Tidak,
a != a
bukan utas yang aman. Ungkapan ini terdiri dari tiga bagian: memuata
, memuata
lagi, dan melakukan!=
. Dimungkinkan untuk utas lainnya untuk mendapatkan kunci intrinsik padaa
induk dan mengubah nilaia
di antara 2 operasi pemuatan.Namun faktor lain adalah apakah
a
lokal. Jikaa
bersifat lokal maka tidak ada utas lain yang memiliki akses ke sana dan karenanya harus aman.juga harus selalu dicetak
false
.Mendeklarasikan
a
sepertivolatile
tidak akan menyelesaikan masalah karena jikaa
inistatic
atau contoh. Masalahnya bukan bahwa utas memiliki nilai yang berbedaa
, tetapi satu utas memuata
dua kali dengan nilai yang berbeda. Ini sebenarnya bisa membuat case lebih aman dari thread .. Jikaa
tidakvolatile
makaa
bisa di-cache dan perubahan di thread lain tidak akan mempengaruhi nilai cache.sumber
synchronized
salah: untuk kode yang dijamin untuk dicetakfalse
, semua metode yang ditetapkana
juga harussynchronized
.a
induk saat metode ini dijalankan, perlu untuk mengatur nilainyaa
.Mengenai perilaku aneh:
Karena variabel
a
tidak ditandai sebagaivolatile
, pada titik tertentu mungkin nilaia
cache di-cache. Keduaa
sa != a
kemudian versi cache dan dengan demikian selalu sama (artinyaflag
sekarang selalufalse
).sumber
Bahkan bacaan sederhana bukanlah atom. Jika
a
inilong
dan tidak ditandai sebagaivolatile
kemudian pada 32-bit JVMslong b = a
tidak thread-safe.sumber