Pemrograman bersamaan adalah salah satu keterampilan terpenting bagi pemrogram Java dan salah satu keterampilan paling sulit untuk dikuasai. Dibutuhkan programmer untuk memiliki pemahaman yang mendalam tentang prinsip operasi terendah komputer, dan pada saat yang sama, itu membutuhkan programmer untuk memiliki logika yang jelas dan pemikiran yang cermat, sehingga mereka dapat menulis program bersamaan multi-threaded yang efisien, aman dan andal. Seri ini akan dimulai dari sifat koordinasi antar-thread (tunggu, beri tahu, memberi tahu semua), disinkronkan dan mudah menguap, dan menjelaskan secara rinci setiap alat konkurensi dan mekanisme implementasi yang mendasari yang disediakan oleh JDK. Atas dasar ini, kami akan lebih menganalisis kelas alat dari paket java.util.concurrent, termasuk penggunaannya, implementasi kode sumber, dan prinsip -prinsip di baliknya. Artikel ini adalah artikel pertama dalam seri ini dan merupakan bagian teoritis paling inti dari seri ini. Artikel selanjutnya akan dianalisis dan dijelaskan berdasarkan ini.
1. Berbagi
Berbagi data adalah salah satu alasan utama keamanan utas. Jika semua data hanya valid di utas, tidak ada masalah keamanan utas, yang merupakan salah satu alasan utama mengapa kita sering tidak perlu mempertimbangkan keamanan utas saat pemrograman. Namun, dalam pemrograman multithreaded, berbagi data tidak dapat dihindari. Skenario yang paling khas adalah data dalam database. Untuk memastikan konsistensi data, kita biasanya perlu berbagi data dalam database yang sama. Bahkan dalam kasus master dan slave, data yang sama diakses. Master dan Slave hanya menyalin data yang sama untuk efisiensi akses dan keamanan data. Kami sekarang menunjukkan masalah yang disebabkan oleh berbagi data di bawah beberapa utas melalui contoh sederhana:
Cuplikan Kode 1:
paket com.paddx.test.concurrent; kelas publik sharedata {public static int count = 0; public static void main (string [] args) {final sharedata data = new sharedata (); untuk (int i = 0; i <10; i ++) {thread baru (runnable baru () {@Override public void run () {coba {// jeda untuk 1 milidetik saat masuk untuk meningkatkan kemungkinan masalah konkurensi. data.addcount ();} System.out.print (Count + ""); } coba {// Program utama dijeda selama 3 detik untuk memastikan bahwa eksekusi program di atas diselesaikan thread.sleep (3000); } catch (InterruptedException e) {E.PrintStackTrace (); } System.out.println ("count =" + count); } public void addCount () {count ++; }}Tujuan dari kode di atas adalah untuk menambahkan satu operasi untuk menghitung dan mengeksekusi 1.000 kali, tetapi di sini diimplementasikan melalui 10 utas, setiap utas mengeksekusi 100 kali, dan dalam keadaan normal, 1.000 harus output. Namun, jika Anda menjalankan program di atas, Anda akan menemukan bahwa hasilnya tidak terjadi. Berikut adalah hasil eksekusi dari waktu tertentu (hasil dari setiap proses mungkin tidak sama, dan kadang -kadang hasil yang benar dapat diperoleh):
Dapat dilihat bahwa untuk operasi variabel bersama, berbagai hasil yang tidak terduga mudah terlihat di lingkungan multi-utas.
2. Pengecualian bersama
Pengecualian Sumber Daya Mutual berarti bahwa hanya satu pengunjung yang diizinkan untuk mengaksesnya secara bersamaan, yang unik dan eksklusif. Kami biasanya mengizinkan beberapa utas membaca data secara bersamaan, tetapi hanya satu utas yang dapat menulis data secara bersamaan. Jadi kami biasanya membagi kunci menjadi kunci bersama dan kunci eksklusif, juga disebut kunci baca dan menulis kunci. Jika sumber daya tidak saling eksklusif, kita tidak perlu khawatir tentang keamanan utas bahkan jika mereka adalah sumber daya bersama. Misalnya, untuk berbagi data yang tidak dapat diubah, semua utas hanya dapat membacanya, jadi masalah keselamatan utas tidak diperlukan. Namun, operasi penulisan untuk data bersama umumnya memerlukan pengecualian timbal balik. Dalam contoh di atas, masalah modifikasi data terjadi karena kurangnya pengecualian timbal balik. Java menyediakan banyak mekanisme untuk memastikan pengecualian timbal balik, cara termudah adalah dengan menggunakan sinkronisasi. Sekarang kami menambahkan disinkronkan ke program di atas dan mengeksekusi:
Cuplikan Kode Dua:
paket com.paddx.test.concurrent; kelas publik sharedata {public static int count = 0; public static void main (string [] args) {final sharedata data = new sharedata (); untuk (int i = 0; i <10; i ++) {thread baru (runnable baru () {@Override public void run () {coba {// jeda untuk 1 milidetik saat masuk untuk meningkatkan kemungkinan masalah konkurensi. data.addcount ();} System.out.print (Count + ""); } coba {// Program utama dijeda selama 3 detik untuk memastikan bahwa eksekusi program di atas diselesaikan thread.sleep (3000); } catch (InterruptedException e) {E.PrintStackTrace (); } System.out.println ("count =" + count); } / *** Tambahkan kata kunci yang disinkronkan* / public disinkronkan void addCount () {count ++; }}Sekarang kode di atas dieksekusi, Anda akan menemukan bahwa tidak peduli berapa kali Anda mengeksekusi, hasil akhirnya adalah 1000.
AKU AKU AKU. Atomisitas
Atomisitas mengacu pada pengoperasian data sebagai keseluruhan yang independen dan tidak dapat dipisahkan. Dengan kata lain, ini adalah operasi yang kontinu dan tidak terputus. Setengah dari eksekusi data tidak dimodifikasi oleh utas lain. Cara termudah untuk memastikan atomisitas adalah instruksi sistem operasi, yaitu, jika satu operasi sesuai dengan satu instruksi sistem operasi sekaligus, itu pasti akan memastikan atomisitas. Namun, banyak operasi tidak dapat diselesaikan dengan satu instruksi. Misalnya, untuk operasi tipe panjang, banyak sistem perlu dibagi menjadi beberapa instruksi untuk beroperasi pada posisi tinggi dan rendah masing-masing. Misalnya, pengoperasian Integer I ++ yang sering kita gunakan sebenarnya perlu dibagi menjadi tiga langkah: (1) membaca nilai bilangan bulat I; (2) tambahkan satu operasi ke i; (3) Tulis hasilnya kembali ke memori. Proses ini dapat terjadi dalam multithreading:
Ini juga alasan mengapa hasil eksekusi segmen kode salah. Untuk operasi kombinasi ini, cara paling umum untuk memastikan atomisitas adalah mengunci, seperti disinkronkan atau kunci di Java dapat diimplementasikan, dan Segmen Kode 2 diimplementasikan melalui disinkronkan. Selain kunci, ada cara lain untuk CAS (bandingkan dan bertukar), yaitu, sebelum memodifikasi data, membandingkan apakah nilai yang dibaca sebelum yang sebelumnya konsisten. Jika mereka konsisten, modifikasi mereka, dan jika mereka tidak konsisten, mereka akan dieksekusi lagi. Ini juga merupakan prinsip mengoptimalkan implementasi kunci. Namun, CAS mungkin tidak efektif dalam beberapa skenario. Misalnya, utas lain pertama kali memodifikasi nilai tertentu dan kemudian mengubahnya kembali ke nilai asli. Dalam hal ini, CAS tidak dapat menilai.
4. Visibilitas
Untuk memahami visibilitas, Anda perlu memiliki pemahaman tertentu tentang model memori JVM. Model memori JVM mirip dengan sistem operasi, seperti yang ditunjukkan pada gambar:
Dari gambar ini, kita dapat melihat bahwa setiap utas memiliki memori kerjanya sendiri (setara dengan buffer canggih CPU. Tujuannya adalah untuk semakin mempersempit perbedaan kecepatan antara sistem penyimpanan dan CPU dan meningkatkan kinerja). Untuk variabel bersama, setiap kali utas membaca salinan variabel bersama dalam memori kerja. Saat menulis, itu secara langsung memodifikasi nilai salinan di memori kerja, dan kemudian menyinkronkan memori kerja dengan nilai pada memori utama pada titik waktu tertentu. Masalah yang menyebabkan ini adalah bahwa jika Thread 1 memodifikasi variabel tertentu, Thread 2 mungkin tidak melihat modifikasi yang dibuat oleh Thread 1 ke variabel bersama. Melalui program berikut, kita dapat menunjukkan masalah yang tidak terlihat:
paket com.paddx.test.concurrent; Visibilitas kelas publik {private static boolean siap; nomor int statis pribadi; Private Static Class ReaderRthread memperluas utas {public void run () {coba {thread.sleep (10); } catch (InterruptedException e) {E.PrintStackTrace (); } if (! Ready) {System.out.println (siap); } System.out.println (angka); }} private static class writerThread memperluas utas {public void run () {try {thread.sleep (10); } catch (InterruptedException e) {E.PrintStackTrace (); } angka = 100; siap = true; }} public static void main (string [] args) {new writerThread (). start (); baru readerthread (). start (); }}Secara intuitif, program ini hanya boleh menghasilkan 100, dan nilai siap tidak akan dicetak. Bahkan, jika Anda menjalankan kode di atas beberapa kali, mungkin ada banyak hasil yang berbeda. Berikut adalah hasil dari dua run:
Tentu saja, hasil ini hanya dapat dikatakan dimungkinkan karena visibilitas. Ketika utas tulis (WriterThread) ditetapkan siap = true, readerthread tidak dapat melihat hasil yang dimodifikasi, jadi false akan dicetak. Untuk hasil kedua, yaitu, hasil dari utas tulis belum dibaca saat mengeksekusi IF (! Siap), tetapi hasil eksekusi utas tulis dibaca saat mengeksekusi System.out.println (siap). Namun, hasil ini juga mungkin disebabkan oleh eksekusi utas alternatif. Visibilitas dapat dipastikan melalui sinkronisasi atau volatil di Java, dan detail spesifik akan dianalisis dalam artikel selanjutnya.
5. Urutan
Untuk meningkatkan kinerja, kompiler dan prosesor dapat memesan ulang instruksi. Ada tiga jenis pemesanan ulang:
(1) Penataan ulang yang dioptimalkan kompiler. Kompiler dapat menjadwal ulang urutan eksekusi pernyataan tanpa mengubah semantik dari program tunggal.
(2) Penataan ulang paralelisme tingkat instruksi. Prosesor modern menggunakan teknologi paralel tingkat instruksi (ICP) untuk tumpang tindih eksekusi beberapa instruksi. Jika tidak ada ketergantungan data, prosesor dapat mengubah urutan eksekusi pernyataan yang sesuai dengan instruksi mesin.
(3) Pemesanan ulang sistem memori. Karena prosesor menggunakan cache dan buffer baca/tulis, ini membuat operasi pemuatan dan penyimpanan tampaknya dieksekusi rusak.
Kami dapat secara langsung merujuk pada deskripsi masalah pemesanan ulang di JSR 133:
(1) (2)
Mari pertama -tama lihat bagian kode sumber (1) pada gambar di atas. Dari kode sumber, instruksi 1 dieksekusi terlebih dahulu atau instruksi 3 dieksekusi terlebih dahulu. Jika Instruksi 1 dieksekusi terlebih dahulu, R2 tidak boleh melihat nilai yang ditulis dalam Instruksi 4. Jika Instruksi 3 dieksekusi terlebih dahulu, R1 tidak boleh melihat nilai yang ditulis oleh Instruksi 2. Namun, hasil yang berjalan mungkin memiliki R2 == 2 dan R1 == 1, yang merupakan hasil dari "penataan ulang". Gambar di atas (2) adalah hasil kompilasi hukum yang mungkin. Setelah kompilasi, urutan instruksi 1 dan instruksi 2 dapat dipertukarkan. Oleh karena itu, hasil R2 == 2 dan R1 == 1 akan muncul. Sinkronisasi atau volatil juga dapat digunakan di Java untuk memastikan pesanan.
Enam ringkasan
Artikel ini menjelaskan dasar teoritis dari pemrograman bersamaan Java, dan beberapa hal akan dibahas secara lebih rinci dalam analisis selanjutnya, seperti visibilitas, ketertiban, dll. Artikel selanjutnya akan dibahas berdasarkan konten bab ini. Jika Anda dapat memahami konten di atas dengan baik, saya percaya bahwa itu akan sangat membantu Anda apakah itu untuk memahami artikel pemrograman bersamaan lainnya atau dalam pekerjaan pemrograman bersamaan harian Anda.