Apakah variabel statis dibagikan di antara utas?

95

Guru saya di kelas Java tingkat atas tentang threading mengatakan sesuatu yang saya tidak yakin.

Dia menyatakan bahwa kode berikut belum tentu memperbarui readyvariabel. Menurutnya, kedua utas tidak harus berbagi variabel statis, khususnya dalam kasus ketika setiap utas (utas utama versus ReaderThread) berjalan pada prosesornya sendiri dan oleh karena itu tidak berbagi register / cache / dll yang sama dan satu CPU tidak akan memperbarui yang lain.

Pada dasarnya, dia mengatakan itu mungkin yang readydiperbarui di utas utama, tetapi BUKAN di ReaderThread, sehingga ReaderThreadakan berulang tanpa batas.

Dia juga mengklaim program itu mungkin untuk dicetak 0atau 42. Saya mengerti bagaimana 42bisa dicetak, tapi tidak 0. Dia menyebutkan ini akan menjadi kasus ketika numbervariabel disetel ke nilai default.

Saya pikir mungkin tidak ada jaminan bahwa variabel statis diperbarui di antara utas, tetapi ini menurut saya sangat aneh untuk Java. Apakah membuat readyvolatile memperbaiki masalah ini?

Dia menunjukkan kode ini:

public class NoVisibility {  
    private static boolean ready;  
    private static int number;  
    private static class ReaderThread extends Thread {   
        public void run() {  
            while (!ready)   Thread.yield();  
            System.out.println(number);  
        }  
    }  
    public static void main(String[] args) {  
        new ReaderThread().start();  
        number = 42;  
        ready = true;  
    }  
}
dontocsata
sumber
Visibilitas variabel non-lokal tidak bergantung pada apakah itu variabel statis, kolom objek, atau elemen array, semuanya memiliki pertimbangan yang sama. (Dengan masalah bahwa elemen array tidak dapat dibuat volatil.)
Paŭlo Ebermann
1
tanyakan pada guru Anda jenis arsitektur apa yang dia anggap mungkin untuk melihat '0'. Namun, secara teori dia benar.
bestsss
4
@bestsss Mengajukan pertanyaan semacam itu akan mengungkapkan kepada guru bahwa dia telah melewatkan inti dari apa yang dia katakan. Intinya adalah programmer yang kompeten memahami apa yang dijamin dan apa yang tidak dan tidak bergantung pada hal-hal yang tidak dijamin, setidaknya tidak tanpa memahami dengan tepat apa yang tidak dijamin dan mengapa.
David Schwartz
Mereka dibagi di antara semua yang dimuat oleh loader kelas yang sama. Termasuk utas.
Marquis dari Lorne
Guru Anda (dan jawaban yang diterima) 100% benar, tetapi saya akan menyebutkan bahwa itu jarang terjadi - ini adalah jenis masalah yang akan bersembunyi selama bertahun-tahun dan hanya muncul dengan sendirinya ketika hal itu paling berbahaya. Bahkan tes singkat yang mencoba untuk mengungkap masalah cenderung bertindak seolah-olah semuanya baik-baik saja (Mungkin karena mereka tidak punya waktu untuk JVM melakukan banyak pengoptimalan), jadi ini adalah masalah yang sangat bagus untuk diperhatikan.
Bill K

Jawaban:

75

Tidak ada yang istimewa tentang variabel statis dalam hal visibilitas. Jika mereka dapat diakses, utas apa pun bisa mendapatkannya, jadi Anda lebih cenderung melihat masalah konkurensi karena mereka lebih terbuka.

Ada masalah visibilitas yang diberlakukan oleh model memori JVM. Berikut adalah artikel yang membahas tentang model memori dan bagaimana penulisan dapat dilihat oleh utas . Anda tidak dapat mengandalkan perubahan yang dibuat satu utas agar terlihat oleh utas lain secara tepat waktu (sebenarnya JVM tidak berkewajiban untuk membuat perubahan itu terlihat oleh Anda sama sekali, dalam kerangka waktu apa pun), kecuali Anda membuat terjadi sebelum .

Berikut kutipan dari tautan itu (disediakan dalam komentar oleh Jed Wesley-Smith):

Bab 17 dari Spesifikasi Bahasa Java mendefinisikan hubungan terjadi-sebelum pada operasi memori seperti membaca dan menulis variabel bersama. Hasil tulis oleh satu utas dijamin dapat dilihat oleh utas lain hanya jika operasi tulis terjadi-sebelum operasi baca. Konstruksi tersinkronisasi dan mudah menguap, serta metode Thread.start () dan Thread.join (), dapat membentuk hubungan yang terjadi sebelum. Khususnya:

  • Setiap tindakan di utas terjadi-sebelum setiap tindakan di utas itu yang datang kemudian dalam urutan program.

  • Buka kunci (blok tersinkronisasi atau keluar metode) monitor terjadi-sebelum setiap kunci berikutnya (blok tersinkronisasi atau entri metode) dari monitor yang sama. Dan karena hubungan terjadi-sebelum bersifat transitif, semua tindakan utas sebelum pembukaan kunci terjadi-sebelum semua tindakan setelah utas apa pun yang mengunci monitor itu.

  • Penulisan ke bidang volatil terjadi sebelum setiap pembacaan berikutnya dari bidang yang sama. Penulisan dan pembacaan kolom volatil memiliki efek konsistensi memori yang sama seperti saat masuk dan keluar monitor, tetapi tidak memerlukan penguncian pengecualian bersama.

  • Panggilan untuk memulai thread terjadi-sebelum tindakan apa pun di thread yang dimulai.

  • Semua tindakan di utas terjadi-sebelum utas lainnya berhasil kembali dari gabungan di utas itu.

Nathan Hughes
sumber
3
Dalam praktiknya, "pada waktu yang tepat" dan "selalu" adalah sinonim. Sangat mungkin kode di atas tidak pernah berhenti.
POHON
4
Juga, ini menunjukkan anti-pola lain. Jangan gunakan volatile untuk melindungi lebih dari satu bagian dari status bersama. Di sini, angka dan siap adalah dua bagian dari status, dan untuk memperbarui / membaca keduanya secara konsisten, Anda memerlukan sinkronisasi aktual.
POHON
5
Bagian tentang akhirnya menjadi terlihat adalah salah. Tanpa hubungan kejadian -sebelum yang eksplisit, tidak ada jaminan bahwa tulisan apa pun akan dilihat oleh utas lain, karena JIT memiliki hak yang cukup untuk memasukkan pembacaan ke dalam register dan kemudian Anda tidak akan pernah melihat pembaruan apa pun. Beban akhir apa pun adalah keberuntungan dan tidak boleh diandalkan.
Jed Wesley-Smith
2
"kecuali Anda menggunakan kata kunci yang mudah menguap atau sinkronkan." harus membaca "kecuali ada hubungan terjadi-sebelum yang relevan antara penulis dan pembaca" dan tautan ini: download.oracle.com/javase/6/docs/api/java/util/concurrent/…
Jed Wesley-Smith
2
@bestss terlihat bagus. Sayangnya ThreadGroup rusak dalam banyak hal.
Jed Wesley-Smith
37

Dia berbicara tentang visibilitas dan tidak untuk dipahami terlalu harfiah.

Variabel statis memang dibagi di antara utas, tetapi perubahan yang dibuat di satu utas mungkin tidak langsung terlihat ke utas lain, membuatnya tampak seperti ada dua salinan variabel.

Artikel ini menyajikan pandangan yang sejalan dengan cara dia menyajikan info:

Pertama, Anda harus memahami sedikit tentang model memori Java. Saya telah berjuang sedikit selama bertahun-tahun untuk menjelaskannya dengan singkat dan baik. Saat ini, cara terbaik yang dapat saya pikirkan untuk menggambarkannya adalah jika Anda membayangkannya seperti ini:

  • Setiap utas di Java berlangsung di ruang memori terpisah (ini jelas tidak benar, jadi bersabarlah dengan saya yang satu ini).

  • Anda perlu menggunakan mekanisme khusus untuk menjamin bahwa komunikasi terjadi di antara utas ini, seperti yang Anda lakukan pada sistem penerusan pesan.

  • Penulisan memori yang terjadi di satu thread dapat "bocor" dan dilihat oleh thread lain, tetapi ini sama sekali tidak dijamin. Tanpa komunikasi eksplisit, Anda tidak dapat menjamin penulisan mana yang dilihat oleh utas lain, atau bahkan urutan penulisannya.

...

model benang

Tapi sekali lagi, ini hanyalah model mental untuk berpikir tentang threading dan volatile, tidak secara harfiah bagaimana JVM bekerja.

Bert F
sumber
12

Pada dasarnya memang benar, tetapi sebenarnya masalahnya lebih kompleks. Visibilitas data bersama dapat dipengaruhi tidak hanya oleh cache CPU, tetapi juga oleh eksekusi instruksi yang tidak teratur.

Oleh karena itu, Java mendefinisikan Model Memori , yang menyatakan dalam situasi apa thread dapat melihat status konsisten dari data bersama.

Dalam kasus khusus Anda, menambahkan volatilejaminan visibilitas.

axtavt
sumber
8

Mereka tentu saja "dibagi" dalam arti bahwa keduanya merujuk ke variabel yang sama, tetapi mereka tidak selalu melihat pembaruan satu sama lain. Ini berlaku untuk variabel apa pun, tidak hanya statis.

Dan secara teori, penulisan yang dibuat oleh thread lain dapat tampak dalam urutan yang berbeda, kecuali jika variabel dideklarasikan volatileatau penulisan disinkronkan secara eksplisit.

biziclop
sumber
4

Dalam satu classloader, kolom statis selalu dibagikan. Untuk secara eksplisit mencakup data ke utas, Anda ingin menggunakan fasilitas seperti ThreadLocal.

Kirk Woll
sumber
2

Saat Anda menginisialisasi variabel tipe primitif statis, java default memberikan nilai untuk variabel statis

public static int i ;

ketika Anda mendefinisikan variabel seperti ini nilai default i = 0; itulah mengapa ada kemungkinan untuk mendapatkan Anda 0. maka utas utama memperbarui nilai boolean siap menjadi true. karena ready adalah variabel statis, utas utama, dan utas lainnya merujuk ke alamat memori yang sama sehingga variabel siap berubah. sehingga benang sekunder keluar dari loop sementara dan mencetak nilai. saat mencetak nilai nilai inisialisasi angka adalah 0. jika proses utas telah berlalu sementara loop sebelum utas utama memperbarui variabel angka. maka ada kemungkinan untuk mencetak 0

NuOne
sumber
-2

@dontocsata Anda dapat kembali ke guru Anda dan menyekolahkannya sedikit :)

sedikit catatan dari dunia nyata dan apa pun yang Anda lihat atau ceritakan. Harap DICATAT, kata-kata di bawah ini berkaitan dengan kasus khusus ini dalam urutan persis seperti yang ditunjukkan.

2 variabel berikut akan berada di baris cache yang sama di bawah hampir semua arsitektur yang diketahui.

private static boolean ready;  
private static int number;  

Thread.exit (utas utama) dijamin keluar dan exit dijamin menyebabkan pagar memori, karena penghapusan utas grup utas (dan banyak masalah lainnya). (ini adalah panggilan tersinkronisasi, dan saya tidak melihat cara tunggal untuk diimplementasikan tanpa bagian sinkronisasi karena ThreadGroup harus diakhiri juga jika tidak ada untaian daemon yang tersisa, dll).

Utas yang dimulai ReaderThreadakan menjaga proses tetap hidup karena ini bukan daemon! Jadi readydan numberakan disiram bersama-sama (atau nomor sebelumnya jika sakelar konteks terjadi) dan tidak ada alasan nyata untuk mengurutkan ulang dalam kasus ini setidaknya saya bahkan tidak dapat memikirkannya. Anda akan membutuhkan sesuatu yang benar-benar aneh untuk melihat apa pun kecuali 42. Sekali lagi saya menganggap kedua variabel statis akan berada di baris cache yang sama. Saya tidak bisa membayangkan panjang baris cache 4 byte ATAU JVM yang tidak akan menempatkan mereka di area berkelanjutan (baris cache).

bestsss
sumber
3
@bestsss sementara itu semua benar hari ini, itu bergantung pada implementasi JVM saat ini dan arsitektur perangkat keras untuk kebenarannya, bukan pada semantik program. Artinya, program tersebut masih rusak, meskipun dapat berfungsi. Sangat mudah untuk menemukan varian sepele dari contoh ini yang benar-benar gagal dengan cara yang ditentukan.
Jed Wesley-Smith
1
Saya memang mengatakan bahwa itu tidak mengikuti spesifikasi, namun sebagai seorang guru setidaknya menemukan contoh yang sesuai yang benar-benar dapat gagal pada beberapa arsitektur komoditas, jadi contoh tersebut agak nyata.
bestsss
6
Mungkin saran terburuk dalam menulis kode aman-utas yang pernah saya lihat.
Lawrence Dol
4
@Bestsss: Jawaban sederhana untuk pertanyaan Anda adalah: "Kode untuk spesifikasi dan dokumen, bukan untuk efek samping dari sistem atau implementasi tertentu". Hal ini sangat penting dalam platform mesin virtual yang dirancang untuk menjadi agnostik perangkat keras yang mendasarinya.
Lawrence Dol
1
@Bestsss: Poin guru adalah bahwa (a) kode mungkin berfungsi dengan baik saat Anda mengujinya, dan (b) kode rusak karena bergantung pada operasi perangkat keras dan bukan jaminan dari spesifikasi. Intinya adalah sepertinya tidak apa-apa, tetapi tidak juga.
Lawrence Dol