Hai! Kami melanjutkan studi kami tentang multithreading. Hari ini kita akan mengenal
Saat kami memanggil
volatile
kata kunci dan yield()
metodenya. Mari selami :)
Kata kunci yang mudah menguap
Saat membuat aplikasi multithreaded, kita dapat mengalami dua masalah serius. Pertama, saat aplikasi multithreaded sedang berjalan, thread yang berbeda dapat meng-cache nilai variabel (kita telah membahasnya dalam pelajaran berjudul 'Menggunakan volatile' ). Anda dapat mengalami situasi di mana satu utas mengubah nilai suatu variabel, tetapi utas kedua tidak melihat perubahannya, karena ia bekerja dengan salinan variabel yang di-cache. Secara alami, konsekuensinya bisa serius. Misalkan itu bukan sembarang variabel lama melainkan saldo rekening bank Anda, yang tiba-tiba mulai melompat-lompat secara acak :) Kedengarannya tidak menyenangkan, bukan? Kedua, di Java, operasi untuk membaca dan menulis semua tipe primitif,long
double
, adalah atom. Nah, misalnya, jika Anda mengubah nilai suatu int
variabel di satu utas, dan di utas lain Anda membaca nilai variabel tersebut, Anda akan mendapatkan nilai lama atau yang baru, yaitu nilai yang dihasilkan dari perubahan tersebut. di utas 1. Tidak ada 'nilai tengah'. Namun, ini tidak bekerja dengan long
s dan double
s. Mengapa? Karena dukungan lintas platform. Ingat pada level awal yang kami katakan bahwa prinsip panduan Java adalah 'menulis sekali, jalankan di mana saja'? Itu berarti dukungan lintas platform. Dengan kata lain, aplikasi Java berjalan di semua jenis platform yang berbeda. Misalnya, pada sistem operasi Windows, versi Linux atau MacOS berbeda. Ini akan berjalan tanpa hambatan pada mereka semua. Menimbang dalam 64 bit,long
double
adalah primitif 'terberat' di Jawa. Dan platform 32-bit tertentu tidak mengimplementasikan pembacaan dan penulisan atom variabel 64-bit. Variabel tersebut dibaca dan ditulis dalam dua operasi. Pertama, 32 bit pertama ditulis ke variabel, dan kemudian 32 bit lainnya ditulis. Akibatnya, masalah mungkin muncul. Satu utas menulis beberapa nilai 64-bit ke X
variabel dan melakukannya dalam dua operasi. Pada saat yang sama, utas kedua mencoba membaca nilai variabel dan melakukannya di antara dua operasi tersebut - ketika 32 bit pertama telah ditulis, tetapi 32 bit kedua belum. Akibatnya, ia membaca nilai perantara yang salah, dan kami memiliki bug. Misalnya, jika pada platform seperti itu kami mencoba menulis nomor ke 9223372036854775809 ke variabel, itu akan menempati 64 bit. Dalam formulir biner, sepertinya ini: 10000000000000000000000000000000000000000000000000000000000000000000000 Utas pertama mulai menulis angka ke variabel. Pada awalnya, ia menulis 32 bit pertama (100000000000000000000000000000000) dan kemudian 32 bit kedua (00000000000000000000000000000001) Dan utas kedua bisa terjepit di antara operasi ini, membaca nilai antara variabel (100000000000000000000000000000000000000000000000), yang merupakan 32 bit pertama yang telah ditulis. Dalam sistem desimal, angka ini adalah 2.147.483.648. Dengan kata lain, kami hanya ingin menulis angka 9223372036854775809 ke sebuah variabel, tetapi karena fakta bahwa operasi ini tidak atomik pada beberapa platform, kami memiliki angka jahat 2.147.483.648, yang muncul entah dari mana dan akan memiliki efek yang tidak diketahui program. Utas kedua hanya membaca nilai variabel sebelum selesai ditulis, yaitu utas melihat 32 bit pertama, tetapi bukan 32 bit kedua. Tentu saja, masalah ini tidak muncul kemarin. Java menyelesaikannya dengan satu kata kunci: volatile
. Jika kita menggunakanvolatile
kata kunci saat mendeklarasikan beberapa variabel dalam program kita…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…artinya:
- Itu akan selalu dibaca dan ditulis secara atomik. Bahkan jika itu 64-bit
double
ataulong
. - Mesin Java tidak akan menyimpannya. Jadi Anda tidak akan mengalami situasi di mana 10 utas bekerja dengan salinan lokalnya sendiri.
Metode hasil()
Kami telah meninjau banyakThread
metode kelas, tetapi ada satu metode penting yang akan menjadi hal baru bagi Anda. Itu yield()
metodenya . Dan itu persis seperti namanya! 
yield
metode di utas, itu sebenarnya berbicara ke utas lainnya: 'Hai, teman-teman. Saya tidak terburu-buru untuk pergi ke mana pun, jadi jika penting bagi salah satu dari Anda untuk mendapatkan waktu prosesor, ambillah — saya bisa menunggu '. Berikut ini contoh sederhana cara kerjanya:
public class ThreadExample extends Thread {
public ThreadExample() {
this.start();
}
public void run() {
System.out.println(Thread.currentThread().getName() + " yields its place to others");
Thread.yield();
System.out.println(Thread.currentThread().getName() + " has finished executing.");
}
public static void main(String[] args) {
new ThreadExample();
new ThreadExample();
new ThreadExample();
}
}
Kami secara berurutan membuat dan memulai tiga utas: Thread-0
, Thread-1
, dan Thread-2
. Thread-0
mulai lebih dulu dan langsung mengalah kepada yang lain. Kemudian Thread-1
dimulai dan juga menghasilkan. Kemudian Thread-2
dimulai, yang juga menghasilkan. Kami tidak memiliki utas lagi, dan setelah Thread-2
memberikan tempatnya terakhir, penjadwal utas mengatakan, 'Hmm, tidak ada lagi utas baru. Siapa yang kita miliki dalam antrian? Siapa yang menyerahkan tempatnya sebelumnya Thread-2
? Sepertinya begitu Thread-1
. Oke, itu artinya kita akan membiarkannya berjalan '. Thread-1
menyelesaikan pekerjaannya dan kemudian penjadwal utas melanjutkan koordinasinya: 'Oke, Thread-1
selesai. Apakah kita memiliki orang lain dalam antrean?'. Thread-0 ada di antrian: itu menghasilkan tempatnya tepat sebelumnyaThread-1
. Sekarang giliran dan berjalan sampai selesai. Kemudian penjadwal selesai mengoordinasikan utas: 'Oke, Thread-2
Anda menyerah pada utas lain, dan semuanya selesai sekarang. Kamu yang terakhir mengalah, jadi sekarang giliranmu'. Kemudian Thread-2
berjalan sampai selesai. Output konsol akan terlihat seperti ini: Thread-0 memberikan tempatnya kepada orang lain Thread-1 memberikan tempatnya kepada orang lain Thread-2 memberikan tempatnya kepada orang lain Thread-1 telah selesai dieksekusi. Thread-0 telah selesai dieksekusi. Thread-2 telah selesai dieksekusi. Tentu saja, penjadwal utas mungkin memulai utas dalam urutan yang berbeda (misalnya, 2-1-0 alih-alih 0-1-2), tetapi prinsipnya tetap sama.
Terjadi-sebelum aturan
Hal terakhir yang akan kita sentuh hari ini adalah konsep ' terjadi sebelum '. Seperti yang sudah Anda ketahui, di Java, penjadwal utas melakukan sebagian besar pekerjaan yang terlibat dalam mengalokasikan waktu dan sumber daya ke utas untuk melakukan tugasnya. Anda juga telah berulang kali melihat bagaimana utas dieksekusi dalam urutan acak yang biasanya tidak dapat diprediksi. Dan secara umum, setelah pemrograman 'berurutan' yang kami lakukan sebelumnya, pemrograman multithread terlihat seperti sesuatu yang acak. Anda sudah percaya bahwa Anda dapat menggunakan sejumlah metode untuk mengontrol aliran program multithreaded. Tetapi multithreading di Java memiliki satu pilar lagi — aturan 4 ' terjadi sebelum '. Memahami aturan ini cukup sederhana. Bayangkan kita memiliki dua utas —A
danB
. Setiap utas ini dapat melakukan operasi 1
dan 2
. Dalam setiap aturan, ketika kami mengatakan ' A terjadi-sebelum B ', kami bermaksud bahwa semua perubahan yang dilakukan oleh utas A
sebelum operasi 1
dan perubahan yang dihasilkan dari operasi ini dapat dilihat oleh utas B
saat operasi 2
dilakukan dan sesudahnya. Setiap aturan menjamin bahwa ketika Anda menulis program multithreaded, peristiwa tertentu akan terjadi sebelum yang lain 100% dari waktu, dan pada saat operasi 2
thread B
akan selalu mengetahui perubahan yang A
dilakukan thread selama operasi 1
. Mari kita tinjau.
Aturan 1.
Melepaskan mutex terjadi sebelum monitor yang sama diakuisisi oleh utas lainnya. Saya pikir Anda mengerti semuanya di sini. Jika mutex objek atau kelas diperoleh dengan satu utas, misalnya, dengan utasA
, utas lain (utas B
) tidak dapat memperolehnya pada saat yang sama. Itu harus menunggu sampai mutex dilepaskan.
Aturan 2.
Thread.start()
Metode ini terjadi sebelumnya Thread.run()
. Sekali lagi, tidak ada yang sulit di sini. Anda sudah tahu bahwa untuk mulai menjalankan kode di dalam run()
metode, Anda harus memanggil start()
metode di utas. Khususnya, metode mulai, bukan run()
metode itu sendiri! Aturan ini memastikan bahwa nilai semua variabel yang ditetapkan sebelum Thread.start()
dipanggil akan terlihat di dalam run()
metode begitu dimulai.
Aturan 3.
Akhir darirun()
metode terjadi sebelum pengembalian dari join()
metode. Mari kembali ke dua utas kita: A
dan B
. Kami memanggil join()
metode sehingga utas B
dijamin menunggu selesainya utas A
sebelum bekerja. Ini berarti bahwa metode objek A run()
dijamin berjalan sampai akhir. Dan semua perubahan pada data yang terjadi dalam run()
metode utas A
dijamin seratus persen akan terlihat di utas B
setelah selesai menunggu utas A
menyelesaikan pekerjaannya sehingga dapat memulai pekerjaannya sendiri.
Aturan 4.
Menulis kevolatile
variabel terjadi sebelum membaca dari variabel yang sama. Saat kami menggunakan volatile
kata kunci, kami sebenarnya selalu mendapatkan nilai saat ini. Bahkan dengan long
atau double
(kami berbicara sebelumnya tentang masalah yang bisa terjadi di sini). Seperti yang sudah Anda pahami, perubahan yang dilakukan pada beberapa utas tidak selalu terlihat oleh utas lainnya. Tapi, tentu saja, sangat sering ada situasi di mana perilaku seperti itu tidak cocok untuk kita. Misalkan kita menetapkan nilai ke variabel di thread A
:
int z;
….
z = 555;
Jika B
utas kami harus menampilkan nilai variabel z
di konsol, itu dapat dengan mudah menampilkan 0, karena tidak tahu tentang nilai yang diberikan. Tetapi Aturan 4 menjamin bahwa jika kita mendeklarasikan z
variabel sebagai volatile
, maka perubahan nilainya di satu utas akan selalu terlihat di utas lainnya. Jika kita menambahkan kata volatile
ke kode sebelumnya...
volatile int z;
….
z = 555;
... lalu kami mencegah situasi di mana utas B
mungkin menampilkan 0. Penulisan ke volatile
variabel terjadi sebelum membacanya.
GO TO FULL VERSION