Java 101: Memahami thread Java, Bagian 2: Sinkronisasi thread

Bulan lalu saya menunjukkan Anda bagaimana mudahnya untuk membuat objek benang, mulai benang yang diasosiasikan dengan benda-benda dengan memanggil Thread's start()metode, dan melakukan operasi benang sederhana dengan memanggil lain Threadmetode seperti tiga kelebihan beban join()metode. Bulan ini kami mengambil program Java multithread, bagaimanapun, yang lebih kompleks.

Memahami utas Java - baca keseluruhan seri

  • Bagian 1: Memperkenalkan utas dan runnable
  • Bagian 2: Sinkronisasi benang
  • Bagian 3: Penjadwalan thread, tunggu / beri tahu, dan gangguan thread
  • Bagian 4: Grup thread, volatilitas, variabel thread-lokal, timer, dan kematian thread

Program multithread sering berfungsi tidak menentu atau menghasilkan nilai yang salah karena kurangnya sinkronisasi utas . Sinkronisasi adalah tindakan serialisasi (atau memesan satu per satu) akses utas ke urutan kode tersebut yang memungkinkan beberapa utas memanipulasi kelas dan variabel bidang contoh, dan sumber daya bersama lainnya. Saya menyebut urutan kode itu sebagai bagian kode penting. . Kolom bulan ini adalah tentang penggunaan sinkronisasi untuk membuat serial akses thread ke bagian kode penting dalam program Anda.

Saya mulai dengan contoh yang menggambarkan mengapa beberapa program multithread harus menggunakan sinkronisasi. Saya selanjutnya mengeksplorasi mekanisme sinkronisasi Java dalam hal monitor dan kunci, dan synchronizedkata kunci. Karena salah menggunakan mekanisme sinkronisasi meniadakan manfaatnya, saya menyimpulkan dengan menyelidiki dua masalah yang diakibatkan oleh penyalahgunaan tersebut.

Tip: Tidak seperti variabel kelas dan bidang instance, utas tidak dapat membagikan variabel dan parameter lokal. Alasan: Variabel dan parameter lokal dialokasikan pada tumpukan panggilan metode thread. Akibatnya, setiap utas menerima salinannya sendiri dari variabel tersebut. Sebaliknya, thread dapat berbagi kolom kelas dan kolom instance karena variabel tersebut tidak dialokasikan pada tumpukan panggilan metode thread. Sebaliknya, mereka mengalokasikan dalam memori heap bersama — sebagai bagian dari kelas (bidang kelas) atau objek (bidang contoh).

Perlunya sinkronisasi

Mengapa kita membutuhkan sinkronisasi? Sebagai jawaban, pertimbangkan contoh ini: Anda menulis program Java yang menggunakan sepasang utas untuk mensimulasikan penarikan / penyetoran transaksi keuangan. Dalam program itu, satu utas melakukan setoran sementara utas lainnya melakukan penarikan. Setiap utas memanipulasi sepasang variabel bersama, kelas dan variabel bidang contoh, yang mengidentifikasi nama dan jumlah transaksi keuangan. Untuk transaksi keuangan yang benar, setiap utas harus menyelesaikan pemberian nilai ke variabel namedan amount(dan mencetak nilai tersebut, untuk mensimulasikan penyimpanan transaksi) sebelum utas lainnya mulai menetapkan nilai ke namedan amount(dan juga mencetak nilai tersebut). Setelah beberapa pekerjaan, Anda akan mendapatkan kode sumber yang menyerupai Kode 1:

Kode 1. NeedForSynchronizationDemo.java

// NeedForSynchronizationDemo.java class NeedForSynchronizationDemo { public static void main (String [] args) { FinTrans ft = new FinTrans (); TransThread tt1 = new TransThread (ft, "Deposit Thread"); TransThread tt2 = new TransThread (ft, "Withdrawal Thread"); tt1.start (); tt2.start (); } } class FinTrans { public static String transName; public static double amount; } class TransThread extends Thread { private FinTrans ft; TransThread (FinTrans ft, String name) { super (name); // Save thread's name this.ft = ft; // Save reference to financial transaction object } public void run () { for (int i = 0; i < 100; i++) { if (getName ().equals ("Deposit Thread")) { // Start of deposit thread's critical code section ft.transName = "Deposit"; try { Thread.sleep ((int) (Math.random () * 1000)); } catch (InterruptedException e) { } ft.amount = 2000.0; System.out.println (ft.transName + " " + ft.amount); // End of deposit thread's critical code section } else { // Start of withdrawal thread's critical code section ft.transName = "Withdrawal"; try { Thread.sleep ((int) (Math.random () * 1000)); } catch (InterruptedException e) { } ft.amount = 250.0; System.out.println (ft.transName + " " + ft.amount); // End of withdrawal thread's critical code section } } } }

NeedForSynchronizationDemoKode sumber memiliki dua bagian kode penting: satu dapat diakses oleh utas setoran, dan yang lainnya dapat diakses oleh utas penarikan. Di dalam bagian kode kritis utas setoran, utas itu memberikan DepositStringreferensi objek ke variabel bersama transNamedan menetapkan 2000.0ke variabel bersama amount. Demikian pula, dalam bagian kode kritis penarikan thread, thread yang menugaskan WithdrawalStringreferensi obyek untuk transNamedan penerima haknya 250.0untuk amount. Mengikuti setiap tugas utas, konten variabel itu dicetak. Saat Anda menjalankan NeedForSynchronizationDemo, Anda mungkin mengharapkan keluaran yang mirip dengan daftar garis Withdrawal 250.0dan Deposit 2000.0garis yang diselingi . Sebaliknya, Anda menerima keluaran yang menyerupai berikut ini:

Withdrawal 250.0 Withdrawal 2000.0 Deposit 2000.0 Deposit 2000.0 Deposit 250.0

Programnya pasti bermasalah. Utas penarikan tidak boleh mensimulasikan penarikan $ 2000, dan utas setoran tidak boleh mensimulasikan setoran $ 250. Setiap utas menghasilkan keluaran yang tidak konsisten. Apa yang menyebabkan ketidakkonsistenan tersebut? Pertimbangkan hal berikut:

  • Pada mesin prosesor tunggal, thread berbagi prosesor. Akibatnya, satu utas hanya dapat dieksekusi untuk jangka waktu tertentu. Pada saat itu, JVM / sistem operasi menjeda eksekusi thread tersebut dan mengizinkan thread lain untuk mengeksekusi — manifestasi penjadwalan thread, topik yang saya bahas di Bagian 3. Di mesin multiprosesor, bergantung pada jumlah thread dan prosesor, setiap thread dapat memiliki prosesor sendiri.
  • Pada mesin prosesor tunggal, periode eksekusi thread mungkin tidak bertahan cukup lama untuk thread tersebut menyelesaikan eksekusi bagian kode kritisnya sebelum thread lain mulai menjalankan bagian kode kritisnya sendiri. Pada mesin multiprosesor, thread dapat secara bersamaan mengeksekusi kode di bagian kode kritisnya. Namun, mereka mungkin memasukkan bagian kode penting mereka pada waktu yang berbeda.
  • Pada mesin prosesor tunggal atau multiprosesor, skenario berikut dapat terjadi: Thread A memberikan nilai ke variabel X bersama di bagian kode kritisnya dan memutuskan untuk melakukan operasi input / output yang memerlukan 100 milidetik. Thread B kemudian memasuki bagian kode kritisnya, memberikan nilai yang berbeda ke X, melakukan operasi input / output 50 milidetik, dan memberikan nilai ke variabel bersama Y dan Z. Operasi input / output Thread A selesai, dan thread tersebut menetapkan nilainya sendiri nilai ke Y dan Z. Karena X berisi nilai yang ditetapkan B, sedangkan Y dan Z berisi nilai yang ditetapkan A, hasil yang tidak konsisten.

Bagaimana inkonsistensi muncul NeedForSynchronizationDemo? Misalkan utas deposit dijalankan ft.transName = "Deposit";dan kemudian panggilan Thread.sleep(). Pada titik itu, utas setoran menyerahkan kendali prosesor untuk periode waktu itu harus tidur, dan utas penarikan dijalankan. Asumsikan utas setoran tidur selama 500 milidetik (nilai yang dipilih secara acak, berkat Math.random(), dari kisaran inklusif 0 hingga 999 milidetik; Saya menjelajahi Mathdan random()metodenya di artikel mendatang). Selama waktu tidur utas setoran, utas penarikan dijalankan ft.transName = "Withdrawal";, tidur selama 50 milidetik (nilai tidur utas penarikan yang dipilih secara acak), bangun, dieksekusi ft.amount = 250.0;, dan dieksekusi System.out.println (ft.transName + " " + ft.amount);—semuanya sebelum utas setoran aktif. Hasilnya, benang penarikan tercetakWithdrawal 250.0, yang mana yang benar. Ketika utas setoran aktif, itu dijalankan ft.amount = 2000.0;, diikuti oleh System.out.println (ft.transName + " " + ft.amount);. Kali ini, Withdrawal 2000.0hasil cetak, yang salah. Meskipun thread deposit sebelumnya menetapkan "Deposit"referensi ke transName, referensi tersebut kemudian menghilang saat thread penarikan menetapkan "Withdrawal"referensi ke variabel bersama tersebut. Saat utas setoran aktif, untaian tersebut gagal memulihkan referensi yang benar ke transName, tetapi melanjutkan eksekusinya dengan menetapkan 2000.0ke amount. Meskipun tidak ada variabel yang memiliki nilai yang tidak valid, nilai gabungan dari kedua variabel menunjukkan ketidakkonsistenan. Dalam hal ini, nilainya mewakili upaya untuk menarik, 000.

Dahulu kala, ilmuwan komputer menemukan istilah untuk menggambarkan perilaku gabungan dari beberapa utas yang menyebabkan ketidakkonsistenan. Istilah tersebut adalah kondisi balapan — tindakan setiap utas berlomba untuk menyelesaikan bagian kode kritisnya sebelum beberapa utas lainnya memasuki bagian kode kritis yang sama. SebagaiNeedForSynchronizationDemomendemonstrasikan, perintah eksekusi thread tidak dapat diprediksi. Tidak ada jaminan bahwa utas dapat menyelesaikan bagian kode kritisnya sebelum utas lain memasuki bagian itu. Karenanya, kami memiliki kondisi ras yang menyebabkan inkonsistensi. Untuk mencegah kondisi balapan, setiap utas harus menyelesaikan bagian kode kritisnya sebelum utas lain memasuki bagian kode kritis yang sama atau bagian kode kritis terkait lainnya yang memanipulasi variabel atau sumber daya bersama yang sama. Tanpa sarana akses serialisasi — yaitu, mengizinkan akses hanya ke satu utas pada satu waktu —ke bagian kode kritis, Anda tidak dapat mencegah kondisi balapan atau inkonsistensi. Untungnya, Java menyediakan cara untuk membuat serial akses thread: melalui mekanisme sinkronisasi.

Catatan : Dari jenis Java, hanya variabel bilangan bulat panjang dan titik mengambang presisi ganda yang rentan terhadap ketidakkonsistenan. Mengapa? JVM 32-bit biasanya mengakses variabel integer panjang 64-bit atau variabel floating-point presisi ganda 64-bit dalam dua langkah 32-bit yang berdekatan. Satu utas mungkin menyelesaikan langkah pertama dan kemudian menunggu sementara utas lain menjalankan kedua langkah tersebut. Kemudian, utas pertama mungkin aktif dan menyelesaikan langkah kedua, menghasilkan variabel dengan nilai yang berbeda dari nilai utas pertama atau kedua. Akibatnya, jika setidaknya satu thread dapat memodifikasi variabel integer panjang atau variabel floating-point presisi ganda, semua thread yang membaca dan / atau mengubah variabel tersebut harus menggunakan sinkronisasi untuk membuat serial akses ke variabel.

Mekanisme sinkronisasi Java

Java menyediakan mekanisme sinkronisasi untuk mencegah lebih dari satu utas mengeksekusi kode di satu atau beberapa bagian kode penting kapan saja. Mekanisme itu mendasarkan dirinya pada konsep monitor dan kunci. Pikirkan monitor sebagai pembungkus pelindung di sekitar bagian kode kritis dan kuncisebagai entitas perangkat lunak yang digunakan monitor untuk mencegah beberapa utas memasuki monitor. Idenya adalah ini: Ketika utas ingin memasuki bagian kode kritis yang dilindungi monitor, utas itu harus mendapatkan kunci yang terkait dengan objek yang terkait dengan monitor. (Setiap objek memiliki kuncinya sendiri.) Jika beberapa thread lain menahan kunci itu, JVM memaksa thread yang meminta untuk menunggu di area tunggu yang terkait dengan monitor / kunci. Saat utas di monitor melepaskan kunci, JVM menghapus utas menunggu dari area tunggu monitor dan memungkinkan utas tersebut mendapatkan kunci dan melanjutkan ke bagian kode kritis monitor.

Untuk bekerja dengan monitor / kunci, JVM menyediakan monitorenterdan monitorexitinstruksi. Untungnya, Anda tidak perlu bekerja pada level rendah seperti itu. Sebagai gantinya, Anda dapat menggunakan synchronizedkata kunci Java dalam konteks synchronizedpernyataan dan metode tersinkronisasi.

Pernyataan tersinkronisasi

Beberapa bagian kode penting menempati sebagian kecil dari metode penutupnya. Untuk menjaga akses beberapa utas ke bagian kode kritis seperti itu, Anda menggunakan synchronizedpernyataan itu. Pernyataan itu memiliki sintaks berikut:

'synchronized' '(' objectidentifier ')' '{' // Critical code section '}'

The synchronizedpernyataan dimulai dengan kata kunci synchronizeddan berlanjut dengan objectidentifier, yang muncul antara sepasang kurung bulat. The objectidentifier referensi obyek yang kunci rekan dengan monitor bahwa synchronizedpernyataan mewakili. Akhirnya, bagian kode kritis pernyataan Java muncul di antara sepasang karakter kurung kurawal. Bagaimana Anda menafsirkan synchronizedpernyataan itu? Pertimbangkan fragmen kode berikut:

synchronized ("sync object") { // Access shared variables and other shared resources }