Hai! Kami meneruskan kajian kami tentang multithreading. Hari ini kita akan mengenali
Apabila kita memanggil
volatile
kata kunci dan yield()
kaedahnya. Jom terjun :)
Kata kunci yang tidak menentu
Apabila mencipta aplikasi berbilang benang, kita boleh menghadapi dua masalah serius. Pertama, apabila aplikasi berbilang benang sedang berjalan, benang yang berbeza boleh menyimpan nilai pembolehubah (kita telah membincangkan perkara ini dalam pelajaran bertajuk 'Menggunakan volatile' ). Anda boleh mengalami situasi di mana satu utas mengubah nilai pembolehubah, tetapi utas kedua tidak melihat perubahan itu, kerana ia berfungsi dengan salinan pembolehubahnya yang dicache. Sememangnya, akibatnya boleh menjadi serius. Katakan bahawa ia bukan sebarang pembolehubah lama tetapi baki akaun bank anda, yang tiba-tiba mula melonjak naik dan turun secara rawak :) Itu tidak terdengar seperti menyeronokkan, bukan? Kedua, dalam Java, operasi untuk membaca dan menulis semua jenis primitif,long
double
, adalah atom. Contohnya, jika anda menukar nilai pembolehubah int
pada satu utas, dan pada utas lain anda membaca nilai pembolehubah itu, anda sama ada akan mendapat nilai lama atau yang baharu, iaitu nilai yang terhasil daripada perubahan itu. dalam benang 1. Tiada 'nilai perantaraan'. Walau bagaimanapun, ini tidak berfungsi dengan long
s dan double
s. kenapa? Kerana sokongan merentas platform. Ingat pada peringkat permulaan yang kami katakan bahawa prinsip panduan Java ialah 'tulis sekali, jalankan di mana-mana'? Ini bermakna sokongan merentas platform. Dengan kata lain, aplikasi Java berjalan pada semua jenis platform yang berbeza. Contohnya, pada sistem pengendalian Windows, versi Linux atau MacOS yang berbeza. Ia akan berjalan tanpa halangan pada mereka semua. Dengan berat dalam 64 bit,long
double
adalah primitif 'paling berat' di Jawa. Dan platform 32-bit tertentu hanya tidak melaksanakan pembacaan dan penulisan atom pembolehubah 64-bit. Pembolehubah sedemikian dibaca dan ditulis dalam dua operasi. Pertama, 32 bit pertama ditulis kepada pembolehubah, dan kemudian 32 bit lagi ditulis. Akibatnya, masalah mungkin timbul. Satu benang menulis beberapa nilai 64-bit kepada X
pembolehubah dan melakukannya dalam dua operasi. Pada masa yang sama, benang kedua cuba membaca nilai pembolehubah dan melakukannya di antara kedua-dua operasi tersebut — apabila 32 bit pertama telah ditulis, tetapi 32 bit kedua tidak. Akibatnya, ia membaca nilai pertengahan, salah, dan kami mempunyai pepijat. Sebagai contoh, jika pada platform sedemikian kami cuba menulis nombor ke 9223372036854775809 kepada pembolehubah, ia akan menduduki 64 bit. Dalam bentuk binari, ia kelihatan seperti ini: 10000000000000000000000000000000000000000000000000000000001 Benang pertama mula menulis nombor kepada pembolehubah. Pada mulanya, ia menulis 32 bit pertama (100000000000000000000000000000) dan kemudian 32 bit kedua (000000000000000000000000000001) Dan benang kedua boleh tersepit di antara operasi ini, membaca nilai perantaraan pembolehubah (100000000000000000000000000000000), yang merupakan 32 bit pertama yang telah ditulis. Dalam sistem perpuluhan, nombor ini ialah 2,147,483,648. Dalam erti kata lain, kami hanya mahu menulis nombor 9223372036854775809 kepada pembolehubah, tetapi disebabkan oleh fakta bahawa operasi ini bukan atom pada beberapa platform, kami mempunyai nombor jahat 2,147,483,648, yang datang entah dari mana dan akan mempunyai kesan yang tidak diketahui. program. Benang kedua hanya membaca nilai pembolehubah sebelum ia selesai ditulis, iaitu benang melihat 32 bit pertama, tetapi bukan 32 bit kedua. Sudah tentu, masalah ini tidak timbul semalam. Java menyelesaikannya dengan satu kata kunci: volatile
. Jika kita menggunakanvolatile
kata kunci apabila mengisytiharkan beberapa pembolehubah dalam program kami…
public class Main {
public volatile long x = 2222222222222222222L;
public static void main(String[] args) {
}
}
…maksudnya:
- Ia akan sentiasa dibaca dan ditulis secara atom. Walaupun ia adalah 64-bit
double
ataulong
. - Mesin Java tidak akan menyimpannya. Oleh itu, anda tidak akan mengalami situasi di mana 10 utas berfungsi dengan salinan tempatan mereka sendiri.
Kaedah hasil().
Kami telah menyemak banyakThread
kaedah kelas, tetapi terdapat kaedah penting yang akan menjadi baharu kepada anda. Ia adalah yield()
kaedahnya . Dan ia melakukan apa yang tersirat dari namanya! 
yield
kaedah pada benang, ia sebenarnya bercakap dengan benang lain: 'Hei, kawan-kawan. Saya tidak terlalu tergesa-gesa untuk pergi ke mana-mana, jadi jika penting bagi mana-mana daripada anda untuk mendapatkan masa pemproses, ambil masa — saya boleh menunggu'. Berikut ialah contoh mudah tentang cara ini berfungsi:
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 membuat dan memulakan tiga urutan secara berurutan: Thread-0
, Thread-1
, dan Thread-2
. Thread-0
bermula dahulu dan serta-merta menghasilkan kepada yang lain. Kemudian Thread-1
dimulakan dan juga menghasilkan. Kemudian Thread-2
dimulakan, yang juga menghasilkan. Kami tidak mempunyai apa-apa benang lagi dan selepas Thread-2
menghasilkan tempat terakhir, penjadual benang berkata, 'Hmm, tiada lagi benang baharu. Siapa yang kita ada dalam barisan? Siapa yang memberikan tempatnya sebelum ini Thread-2
? Nampaknya memang begitu Thread-1
. Okay, itu bermakna kita akan biarkan ia berjalan'. Thread-1
menyelesaikan kerjanya dan kemudian penjadual benang meneruskan penyelarasannya: 'Baiklah, Thread-1
selesai. Adakah kita mempunyai orang lain dalam barisan?'. Thread-0 berada dalam baris gilir: ia memberikan tempatnya sebelum iniThread-1
. Ia kini mendapat gilirannya dan berjalan hingga siap. Kemudian penjadual selesai menyelaraskan urutan: 'Baiklah, Thread-2
, anda telah menyerah kepada urutan lain, dan semuanya telah selesai sekarang. Anda adalah yang terakhir mengalah, jadi kini giliran anda'. Kemudian Thread-2
berjalan hingga selesai. Output konsol akan kelihatan seperti ini: Thread-0 menghasilkan tempatnya kepada orang lain Thread-1 menghasilkan tempatnya kepada orang lain Thread-2 menghasilkan tempatnya kepada orang lain Thread-1 telah selesai dilaksanakan. Thread-0 telah selesai dilaksanakan. Thread-2 telah selesai dilaksanakan. Sudah tentu, penjadual benang mungkin memulakan benang dalam susunan yang berbeza (contohnya, 2-1-0 dan bukannya 0-1-2), tetapi prinsipnya tetap sama.
Berlaku-sebelum peraturan
Perkara terakhir yang akan kita sentuh hari ini ialah konsep ' berlaku sebelum '. Seperti yang anda sedia maklum, dalam Java penjadual benang melaksanakan sebahagian besar kerja yang terlibat dalam memperuntukkan masa dan sumber kepada benang untuk melaksanakan tugas mereka. Anda juga telah berulang kali melihat cara urutan dilaksanakan dalam susunan rawak yang biasanya mustahil untuk diramalkan. Dan secara umum, selepas pengaturcaraan 'berurutan' yang kami lakukan sebelum ini, pengaturcaraan berbilang benang kelihatan seperti sesuatu yang rawak. Anda sudah mula percaya bahawa anda boleh menggunakan pelbagai kaedah untuk mengawal aliran program berbilang benang. Tetapi multithreading di Jawa mempunyai satu tiang lagi — peraturan 4 ' berlaku-sebelum '. Memahami peraturan ini agak mudah. Bayangkan kita mempunyai dua utas —A
danB
. Setiap utas ini boleh melakukan operasi 1
dan 2
. Dalam setiap peraturan, apabila kami menyebut ' A berlaku-sebelum B ', kami maksudkan bahawa semua perubahan yang dibuat oleh benang A
sebelum operasi 1
dan perubahan yang terhasil daripada operasi ini kelihatan kepada benang B
apabila operasi 2
dijalankan dan selepas itu. Setiap peraturan menjamin bahawa apabila anda menulis atur cara berbilang benang, peristiwa tertentu akan berlaku sebelum yang lain 100% sepanjang masa, dan pada masa operasi 2
benang B
akan sentiasa menyedari perubahan yang A
dibuat semasa operasi 1
. Mari kita semak mereka.
Peraturan 1.
Melepaskan mutex berlaku sebelum monitor yang sama diperoleh oleh utas lain. Saya fikir anda memahami segala-galanya di sini. Jika mutex objek atau kelas diperoleh oleh satu utas., sebagai contoh, dengan benangA
, satu lagi benang (benang B
) tidak boleh memperolehnya pada masa yang sama. Ia mesti menunggu sehingga mutex dilepaskan.
Peraturan 2.
Thread.start()
Kaedah itu berlaku sebelum Thread.run()
. Sekali lagi, tiada yang sukar di sini. Anda sudah tahu bahawa untuk mula menjalankan kod di dalam run()
kaedah, anda mesti memanggil start()
kaedah pada benang. Secara khusus, kaedah permulaan, bukan run()
kaedah itu sendiri! Peraturan ini memastikan bahawa nilai semua pembolehubah yang ditetapkan sebelum Thread.start()
dipanggil akan kelihatan di dalam run()
kaedah sebaik sahaja dimulakan.
Peraturan 3.
Penghujung kaedahrun()
berlaku sebelum pulangan daripada join()
kaedah. Mari kembali ke dua utas kami: A
dan B
. Kami memanggil join()
kaedah tersebut supaya benang B
dijamin menunggu selesainya benang A
sebelum ia berfungsi. Ini bermakna kaedah objek A run()
dijamin berjalan hingga ke penghujungnya. Dan semua perubahan pada data yang berlaku dalam run()
kaedah utas A
adalah seratus peratus dijamin akan kelihatan dalam utas B
sebaik sahaja ia selesai menunggu utas A
untuk menyelesaikan kerjanya supaya ia boleh memulakan kerjanya sendiri.
Peraturan 4.
Menulis kepadavolatile
pembolehubah berlaku sebelum membaca daripada pembolehubah yang sama. Apabila kita menggunakan volatile
kata kunci, kita sebenarnya sentiasa mendapat nilai semasa. Walaupun dengan long
atau double
(kami bercakap sebelum ini tentang masalah yang boleh berlaku di sini). Seperti yang telah anda fahami, perubahan yang dibuat pada beberapa utas tidak selalu kelihatan kepada utas lain. Tetapi, sudah tentu, terdapat situasi yang sangat kerap di mana tingkah laku sedemikian tidak sesuai dengan kita. Katakan kita memberikan nilai kepada pembolehubah pada benang A
:
int z;
….
z = 555;
Jika B
utas kami harus memaparkan nilai pembolehubah z
pada konsol, ia boleh memaparkan 0 dengan mudah, kerana ia tidak tahu tentang nilai yang diberikan. Tetapi Peraturan 4 menjamin bahawa jika kami mengisytiharkan pembolehubah z
sebagai volatile
, maka perubahan kepada nilainya pada satu urutan akan sentiasa kelihatan pada urutan lain. Jika kita menambah perkataan volatile
pada kod sebelumnya...
volatile int z;
….
z = 555;
...maka kami menghalang situasi di mana benang B
mungkin memaparkan 0. Menulis kepada volatile
pembolehubah berlaku sebelum membaca daripadanya.
GO TO FULL VERSION