Pertama -tama mari kita baca penjelasan terperinci tentang sinkronisasi:
Sinkronisasi adalah kata kunci dalam bahasa Java. Ketika digunakan untuk memodifikasi metode atau blok kode, itu dapat memastikan bahwa paling banyak satu utas menjalankan kode secara bersamaan.
1. Ketika dua utas bersamaan mengakses blok kode sinkronisasi yang disinkronkan ini dalam objek objek yang sama, hanya satu utas yang dapat dieksekusi dalam satu waktu. Utas lain harus menunggu utas saat ini untuk menjalankan blok kode ini sebelum dapat menjalankan blok kode.
2. Namun, ketika satu utas mengakses blok kode sinkronisasi yang disinkronkan (ini) dari suatu objek, utas lain masih dapat mengakses blok kode sinkronisasi yang tidak disinkronkan (ini) di objek itu.
3. Sangat penting bahwa ketika utas mengakses blok kode sinkronisasi yang disinkronkan (ini) dari suatu objek, utas lain akan diblokir dari mengakses semua blok kode sinkronisasi yang disinkronkan (ini) pada objek.
4. Contoh ketiga juga berlaku untuk blok kode sinkron lainnya. Artinya, ketika utas mengakses blok kode sinkronisasi yang disinkronkan (ini) dari suatu objek, ia memperoleh kunci objek objek ini. Akibatnya, utas lain akses ke semua bagian kode sinkron dari objek objek diblokir sementara.
5. Aturan di atas juga berlaku untuk kunci objek lainnya.
Sederhananya, disinkronkan menyatakan kunci untuk utas saat ini. Utas dengan kunci ini dapat menjalankan instruksi di blok, dan utas lainnya hanya dapat menunggu kunci untuk mendapatkannya sebelum operasi yang sama.
Ini sangat berguna, tetapi saya menemukan situasi aneh lainnya.
1. Di kelas yang sama, ada dua metode: menggunakan deklarasi kata kunci yang disinkronkan
2. Saat mengeksekusi salah satu metode, Anda harus menunggu metode lain (callback utas asinkron) dieksekusi, sehingga Anda menggunakan COUNTDownLatch untuk menunggu.
3. Kode ini didekonstruksi sebagai berikut:
Sinkronisasi void a () {countDownlatch = countDownlatch baru (1); // Lakukan beberapa countdownlatch.aWait ();} disinkronkan void b () {countDownlatch.countdown ();} di dalam
Metode A dieksekusi oleh utas utama, metode B dieksekusi oleh utas asinkron dan hasil eksekusi panggilan balik adalah:
Utas utama mulai macet setelah menjalankan metode A, dan tidak lagi melakukannya, dan tidak ada gunanya bagi Anda untuk menunggu berapa lama berapa lama.
Ini adalah masalah kebuntuan klasik
A Menunggu B untuk dieksekusi, tetapi pada kenyataannya, jangan berpikir bahwa B adalah panggilan balik, B juga menunggu A untuk dieksekusi. Mengapa? Sinkronisasi berperan.
Secara umum, ketika kita ingin menyinkronkan blok kode, kita perlu menggunakan variabel bersama untuk menguncinya, misalnya:
byte [] mutex = byte baru [0]; void a1 () {disinkronkan (mutex) {// dosomething}} void b1 () {disinkronkan (mutex) {// dosomething}} Jika konten metode A dan metode B dimigrasi ke blok yang disinkronkan dari metode A1 dan B1 masing -masing, akan mudah dimengerti.
Setelah A1 dieksekusi, ia secara tidak langsung akan menunggu metode (Countdownlatch) B1 untuk dieksekusi.
Namun, karena mutex di A1 tidak dirilis, kami mulai menunggu B1. Pada saat ini, bahkan jika metode Callback B1 asinkron perlu menunggu Mutex untuk melepaskan kunci, metode B tidak akan dieksekusi.
Ini menyebabkan kebuntuan!
Kata kunci yang disinkronkan di sini ditempatkan di depan metode, dan fungsinya sama. Hanya saja bahasa Java membantu Anda menyembunyikan deklarasi dan penggunaan mutex. Metode sinkronisasi yang digunakan dalam objek yang sama adalah sama, jadi bahkan panggilan balik asinkron akan menyebabkan kebuntuan, jadi perhatikan masalah ini. Tingkat kesalahan ini adalah bahwa kata kunci yang disinkronkan digunakan secara tidak benar. Jangan gunakan secara acak, dan gunakan dengan benar.
Jadi apa sebenarnya objek Mutex yang tidak terlihat?
Contohnya sendiri mudah dipikirkan. Karena dengan cara ini, tidak perlu mendefinisikan objek baru dan membuat kunci. Untuk membuktikan ide ini, Anda dapat menulis program untuk membuktikannya.
Idenya sangat sederhana. Tentukan kelas dan ada dua metode. Satu dinyatakan disinkronkan, dan yang lainnya digunakan disinkronkan (ini) dalam badan metode. Kemudian mulai dua utas untuk memanggil dua metode ini secara terpisah. Jika kompetisi kunci terjadi antara kedua metode (menunggu), dapat dijelaskan bahwa mutex yang tidak terlihat dalam disinkronkan yang dinyatakan oleh metode ini sebenarnya adalah instance itu sendiri.
Public Class Multithreadsync {public disinkronkan void m1 () melempar interruptedException {System. out.println ("panggilan m1"); Benang. Sleep (2000); Sistem. out.println ("M1 Call Done"); } public void m2 () melempar InterruptedException {disinkronkan (this) {System. out.println ("m2 call"); Benang. Sleep (2000); Sistem. out.println ("m2 call done"); }} public static void main (string [] args) {final multithreadsync thisobj = new MulithReadSync (); Thread t1 = thread baru () {@Override public void run () {coba {thisobj.m1 (); } catch (InterruptedException e) {E.PrintStackTrace (); }}}}; Thread t2 = thread baru () {@Override public void run () {coba {thisobj.m2 (); } catch (InterruptedException e) {E.PrintStackTrace (); }}}; t1.start (); t2.start (); }} Output hasilnya adalah:
m1 callm1 hubungi donem2 callm2 panggilan selesai
Dijelaskan bahwa blok sinkronisasi metode M2 sedang menunggu eksekusi M1. Ini dapat mengkonfirmasi konsep di atas.
Perlu dicatat bahwa ketika sinkronisasi ditambahkan ke metode statis, karena ini adalah metode tingkat kelas, objek yang terkunci adalah contoh kelas dari kelas saat ini. Anda juga dapat menulis program untuk membuktikannya. Di sini dihilangkan.
Oleh karena itu, kata kunci yang disinkronkan dari metode ini dapat secara otomatis diganti dengan sinkronisasi (ini) {} saat membacanya, yang mudah dimengerti.
void method () {void Method Sinkronisasi () {disinkronkan (this) {// biz code // biz code} ------ >>>}} Visibilitas memori dari sinkronisasi
Di Java, kita semua tahu bahwa kata kunci yang disinkronkan dapat digunakan untuk menerapkan pengecualian bersama antara utas, tetapi kita sering lupa bahwa ia memiliki fungsi lain, yaitu untuk memastikan visibilitas variabel dalam memori - yaitu, ketika dua utas membaca dan menulis akses ke variabel yang sama pada waktu yang sama, disinkronkan digunakan untuk memastikan bahwa utas menulis memperbarui variabel, dan variabel yang dibaca.
Misalnya, contoh berikut:
Novisibilitas kelas publik {private static boolean ready = false; Nomor int statis pribadi = 0; Private Static Class ReaderRthread memperluas thread {@Override public void run () {while (! Ready) {thread.yield (); // Mendukung CPU untuk membiarkan utas lain berfungsi} system.out.println (angka); }} public static void main (string [] args) {baru readerthread (). start (); angka = 42; siap = true; }}Menurut Anda, apa yang akan dibaca utas? 42? Dalam keadaan normal, 42 akan menjadi output. Namun, karena masalah pemesanan ulang, utas baca dapat menghasilkan 0 atau tidak menghasilkan apa -apa.
Kita tahu bahwa kompiler dapat memesan ulang kode saat menyusun kode Java ke dalam bytecode, dan CPU juga dapat memesan ulang instruksinya saat menjalankan instruksi mesin. Selama pemesanan ulang tidak menghancurkan semantik program-
Dalam satu utas, selama pemesanan ulang tidak mempengaruhi hasil eksekusi program, tidak dapat dijamin bahwa operasi di dalamnya harus dieksekusi dalam urutan yang ditentukan oleh program, bahkan jika pemesanan ulang mungkin memiliki dampak yang signifikan pada utas lain.
Ini berarti bahwa eksekusi pernyataan "Ready = True" dapat diutamakan atas eksekusi pernyataan "Number = 42". Dalam hal ini, utas baca dapat menghasilkan nilai default angka 0.
Di bawah model memori Java, masalah pemesanan ulang akan menyebabkan masalah visibilitas memori tersebut. Di bawah model memori Java, masing -masing utas memiliki memori kerjanya sendiri (terutama cache atau register CPU), dan operasinya pada variabel dilakukan dalam memori kerjanya sendiri, sementara komunikasi antara utas dicapai melalui sinkronisasi antara memori utama dan memori kerja utas.
Misalnya, untuk contoh di atas, utas tulis telah berhasil memperbarui nomor ke 42 dan siap menjadi true, tetapi sangat mungkin bahwa utas tulis hanya menyinkronkan angka ke memori utama (mungkin karena buffer tulis CPU), yang menghasilkan nilai siap dibaca oleh utas baca berikutnya selalu salah, sehingga kode di atas tidak akan menghasilkan nilai numerikal.
Jika kita menggunakan kata kunci yang disinkronkan untuk disinkronkan, tidak akan ada masalah seperti itu.
Novisibilitas kelas publik {private static boolean ready = false; Nomor int statis pribadi = 0; kunci objek statis pribadi = objek baru (); Private Static Class ReaderRthread memperluas thread {@Override public void run () {disinkronkan (lock) {while (! Ready) {thread.yield (); } System.out.println (angka); }} public static void main (string [] args) {disinkronkan (lock) {new readerthread (). start (); angka = 42; siap = true; }}} Ini karena model memori Java memberikan jaminan berikut untuk semantik yang disinkronkan.
Artinya, ketika Threada melepaskan kunci M, variabel yang telah ditulisnya (seperti x dan y, yang ada dalam memori kerjanya) akan disinkronkan ke memori utama. Ketika ThreadB berlaku untuk kunci M yang sama, memori kerja ThreadB akan diatur ke tidak valid, dan kemudian ThreadB akan memuat ulang variabel yang ingin diakses dari memori utama ke dalam memori kerjanya (saat ini, x = 1, y = 1, adalah nilai terbaru yang dimodifikasi dalam threada). Dengan cara ini, komunikasi antara utas dari Threada ke ThreadB tercapai.
Ini sebenarnya adalah salah satu aturan yang terjadi sebelum yang ditentukan oleh JSR133. JSR133 mendefinisikan serangkaian aturan yang terjadi sebelumnya untuk model memori Java.
Faktanya, rangkaian aturan yang terjadi sebelum ini menentukan visibilitas memori antara operasi. Jika A beroperasi terjadi sebelum operasi B, maka hasil eksekusi suatu operasi (seperti menulis ke variabel) harus terlihat saat melakukan operasi B.
Untuk mendapatkan pemahaman yang lebih dalam tentang aturan ini terjadi sebelumnya, mari kita ambil contoh:
// Kode dibagikan berdasarkan utas A dan B objek kunci = objek baru (); int a = 0; int b = 0; int c = 0; // Thread a, panggil kode berikut disinkronkan (kunci) {a = 1; // 1 b = 2; // 2} // 3c = 3; // 4 // Thread B, hubungi kode berikut disinkronkan (lock) {// 5 System.out.println (a); // 6 System.out.println (b); // 7 System.out.println (c); // 8}Kami berasumsi bahwa utas A berjalan terlebih dahulu, menetapkan nilai ke tiga variabel A, B, dan C masing -masing (Catatan: Penugasan variabel A, B dilakukan di blok pernyataan sinkron), dan kemudian Thread B berjalan lagi, membaca nilai -nilai dari ketiga variabel ini dan mencetaknya. Jadi apa nilai variabel A, B, dan C yang dicetak oleh Thread B?
Menurut aturan utamanya, dalam pelaksanaan utas A, kita dapat memperoleh bahwa 1 operasi terjadi sebelum 2 operasi, 2 operasi terjadi sebelum 3 operasi, dan 3 operasi terjadi sebelum 4 operasi. Demikian pula, dalam pelaksanaan Operasi Thread B, 5 terjadi sebelum 6 operasi, 6 operasi terjadi sebelum 7 operasi, dan 7 operasi terjadi sebelum 8 operasi. Menurut prinsip -prinsip membuka dan mengunci monitor, 3 operasi (operasi membuka kunci) terjadi sebelum 5 operasi (operasi penguncian). Menurut aturan transitif, kita dapat menyimpulkan bahwa Operasi 1 dan 2 terjadi sebelum operasi 6, 7, dan 8.
Menurut semantik memori yang terjadi sebelum, hasil eksekusi Operasi 1 dan 2 terlihat untuk Operasi 6, 7 dan 8, jadi dalam Thread B, A dan B dicetak harus 1 dan 2. Untuk Operasi 4 dan Operasi 8 variabel c. Kami tidak dapat menyimpulkan Operasi 4 terjadi sebelum Operasi 8 menurut yang ada terjadi sebelum aturan. Oleh karena itu, dalam utas B, variabel yang diakses ke C mungkin masih 0, bukan 3.