Operasi sensitif di lab saya hari ini benar-benar salah. Aktuator pada mikroskop elektron melewati batasnya, dan setelah serangkaian peristiwa saya kehilangan $ 12 juta peralatan. Saya mempersempit lebih dari 40 ribu baris dalam modul yang salah untuk ini:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Beberapa sampel dari output yang saya dapatkan:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Karena tidak ada aritmatika floating point di sini, dan kita semua tahu bilangan bulat yang ditandatangani berperilaku baik pada overflow di Jawa, saya pikir tidak ada yang salah dengan kode ini. Namun, meskipun output menunjukkan bahwa program tidak mencapai kondisi keluar, ia mencapai kondisi keluar (keduanya tercapai dan tidak tercapai?). Mengapa?
Saya perhatikan ini tidak terjadi di beberapa lingkungan. Saya menggunakan OpenJDK 6 di Linux 64-bit.
final
kualifikasi (yang tidak berpengaruh pada bytecode yang dihasilkan) ke bidangx
dany
"memecahkan" bug. Meskipun tidak memengaruhi bytecode, bidang ditandai dengan itu, yang membuat saya berpikir ini adalah efek samping dari optimasi JVM.Point
p
dikonstruksi yang memuaskanp.x+1 == p.y
, lalu a referensi dilewatkan ke tempat pemungutan suara. Akhirnya utas pemungutan suara memutuskan untuk keluar karena menganggap kondisinya tidak terpenuhi untuk salah satu yangPoint
diterimanya, tetapi kemudian output konsol menunjukkan bahwa ia seharusnya sudah puas. Kurangnya divolatile
sini berarti bahwa tempat pemungutan suara mungkin macet, tapi itu jelas bukan masalah di sini.synchronized
membuat bug tidak terjadi? Itu karena saya harus menulis kode secara acak sampai saya menemukan kode yang akan mereproduksi perilaku ini secara deterministik.Jawaban:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
melakukan beberapa hal, termasuk menulis nilai default kex
dany
(0) dan kemudian menulis nilai awal mereka di konstruktor. Karena objek Anda tidak diterbitkan dengan aman, 4 operasi penulisan tersebut dapat diatur ulang secara bebas oleh kompiler / JVM.Jadi dari perspektif utas bacaan, itu adalah eksekusi hukum untuk membaca
x
dengan nilai baru tetapiy
dengan nilai default 0 misalnya. Pada saat Anda mencapaiprintln
pernyataan (yang dengan cara disinkronkan dan karena itu mempengaruhi operasi baca), variabel memiliki nilai awal dan program mencetak nilai yang diharapkan.Ditandai
currentPos
sebagaivolatile
akan memastikan publikasi yang aman karena objek Anda tidak dapat diubah secara efektif - jika dalam kasus penggunaan nyata objek tersebut dimutasi setelah konstruksi,volatile
jaminan tidak akan cukup dan Anda dapat melihat objek yang tidak konsisten lagi.Atau, Anda dapat membuat
Point
abadi yang juga akan memastikan publikasi yang aman, bahkan tanpa menggunakanvolatile
. Untuk mencapai kekekalan, Anda hanya perlu menandaix
dany
final.Sebagai catatan tambahan dan sebagaimana telah disebutkan,
synchronized(this) {}
dapat diperlakukan sebagai larangan oleh JVM (saya mengerti Anda memasukkannya untuk mereproduksi perilaku).sumber
Karena
currentPos
sedang diubah di luar utas itu harus ditandai sebagaivolatile
:Tanpa volatile, utas tidak dijamin untuk membaca pembaruan pada currentPos yang sedang dibuat di utas utama. Jadi nilai baru terus ditulis untuk currentPos tetapi utas terus menggunakan versi cache sebelumnya untuk alasan kinerja. Karena hanya satu utas yang memodifikasi currentPos Anda bisa lolos tanpa kunci yang akan meningkatkan kinerja.
Hasilnya terlihat jauh berbeda jika Anda membaca nilai hanya sekali dalam utas untuk digunakan dalam perbandingan dan selanjutnya menampilkannya. Ketika saya melakukan hal berikut ini
x
selalu ditampilkan sebagai1
dany
bervariasi antara0
dan beberapa bilangan bulat besar. Saya pikir perilaku itu pada saat ini agak tidak terdefinisi tanpavolatile
kata kunci dan mungkin saja kompilasi kode JIT berkontribusi untuk bertindak seperti ini. Juga jika saya mengomentarisynchronized(this) {}
blok kosong maka kodenya juga berfungsi dan saya menduga itu karena penguncian menyebabkan penundaan yang cukupcurrentPos
dan bidangnya dibaca ulang daripada digunakan dari cache.sumber
volatile
.Anda memiliki memori biasa, referensi 'currentpos' dan objek Point dan bidangnya di belakangnya, dibagi antara 2 utas, tanpa sinkronisasi. Jadi, tidak ada urutan yang pasti antara penulisan yang terjadi pada memori ini di utas utama dan pembacaan di utas yang dibuat (sebut saja T).
Utas utama melakukan penulisan berikut (mengabaikan pengaturan awal titik, akan menghasilkan px dan py memiliki nilai default):
Karena tidak ada yang istimewa dari penulisan ini dalam hal sinkronisasi / penghalang, runtime bebas untuk membiarkan T thread melihatnya terjadi dalam urutan apa pun (utas utama tentu saja selalu melihat tulis dan bacaan dipesan sesuai dengan urutan program), dan terjadi di setiap titik antara bacaan di T.
Jadi T sedang melakukan:
Karena tidak ada hubungan pemesanan antara penulisan di main, dan membaca di T, jelas ada beberapa cara ini dapat menghasilkan hasil Anda, karena T dapat melihat main menulis ke currentpos sebelum menulis ke currentpos.y atau currentpos.x:
dan seterusnya ... Ada sejumlah perlombaan data di sini.
Saya menduga asumsi cacat di sini adalah berpikir bahwa tulisan yang dihasilkan dari baris ini dibuat terlihat di semua utas dalam urutan program utas yang menjalankannya:
Java tidak membuat jaminan seperti itu (itu akan mengerikan untuk kinerja). Sesuatu yang lebih harus ditambahkan jika program Anda membutuhkan pemesanan tulisan yang relatif terjamin untuk dibaca di utas lainnya. Yang lain menyarankan agar x, y bidang final, atau sebagai alternatif membuat currentpos volatile.
Menggunakan final memiliki keuntungan bahwa itu membuat bidang tidak berubah, dan dengan demikian memungkinkan nilai di-cache. Menggunakan volatile lead untuk sinkronisasi pada setiap penulisan dan pembacaan currentpos, yang dapat merusak kinerja.
Lihat bab 17 dari Spesifikasi Bahasa Jawa untuk detail berdarah: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(Jawaban awal mengasumsikan model memori yang lebih lemah, karena saya tidak yakin JLS dijamin volatilnya cukup. Jawaban diedit untuk mencerminkan komentar dari para pengguna, menunjukkan bahwa model Java lebih kuat - terjadi-sebelum transitif - dan karenanya volatile pada currentpos juga cukup. ).
sumber
currentPos
dibuat tidak stabil, penugasan memastikan publikasi yang aman daricurrentPos
objek serta anggotanya, bahkan jika mereka tidak mudah menguap sendiri.Point currentPos = new Point(x, y)
, Anda memiliki 3 tulisan: (w1)this.x = x
, (w2)this.y = y
dan (w3)currentPos = the new point
. Urutan program menjamin bahwa hb (w1, w3) dan hb (w2, w3). Kemudian di program yang Anda baca (r1)currentPos
. JikacurrentPos
tidak volatile, tidak ada hb antara r1 dan w1, w2, w3, sehingga r1 dapat mengamati (atau tidak ada) dari mereka. Dengan volatile, Anda memperkenalkan hb (w3, r1). Dan hubungan hb adalah transitif sehingga Anda juga memperkenalkan hb (w1, r1) dan hb (w2, r1). Ini dirangkum dalam Java Concurrency in Practice (3.5.3. Publikasi Aman Idiom).Anda bisa menggunakan objek untuk menyinkronkan tulisan dan bacaan. Kalau tidak, seperti yang orang lain katakan sebelumnya, menulis ke currentPos akan terjadi di tengah dua kali dibaca p.x + 1 dan py
sumber
sem
tidak dibagikan dan memperlakukan pernyataan yang disinkronkan sebagai no-op ... Fakta bahwa itu memecahkan masalah adalah keberuntungan murni.Anda mengakses currentPos dua kali, dan tidak memberikan jaminan bahwa itu tidak diperbarui di antara kedua akses tersebut.
Sebagai contoh:
Anda pada dasarnya membandingkan dua Poin yang berbeda .
Perhatikan bahwa bahkan membuat currentPos volatile tidak akan melindungi Anda dari ini, karena ini adalah dua bacaan terpisah oleh utas pekerja.
Tambahkan
metode ke kelas poin Anda. Ini akan memastikan bahwa hanya satu nilai currentPos yang digunakan ketika memeriksa x + 1 == y.
sumber