Java 101: Memahami utas Java, Bagian 3: Penjadwalan utas dan tunggu / beri tahu

Bulan ini, saya melanjutkan pengantar empat bagian saya ke utas Java dengan berfokus pada penjadwalan utas, mekanisme tunggu / beri tahu, dan interupsi utas. Anda akan menyelidiki bagaimana JVM atau penjadwal utas sistem operasi memilih utas berikutnya untuk dieksekusi. Seperti yang akan Anda temukan, prioritas penting untuk pilihan penjadwal utas. Anda akan memeriksa bagaimana sebuah thread menunggu hingga menerima notifikasi dari thread lain sebelum melanjutkan eksekusi dan mempelajari cara menggunakan mekanisme wait / notify untuk mengoordinasikan eksekusi dua thread dalam hubungan produsen-konsumen. Terakhir, Anda akan belajar bagaimana membangunkan sebelum waktunya baik thread yang sedang tidur atau menunggu untuk penghentian thread atau tugas lainnya. Saya juga akan mengajari Anda bagaimana utas yang tidak sedang tidur atau menunggu mendeteksi permintaan interupsi dari utas lain.

Perhatikan bahwa artikel ini (bagian dari arsip JavaWorld) diperbarui dengan daftar kode baru dan kode sumber yang dapat diunduh pada Mei 2013.

Memahami utas Java - baca keseluruhan seri

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

Penjadwalan benang

Di dunia yang ideal, semua utas program akan memiliki prosesor sendiri untuk dijalankan. Hingga saatnya tiba ketika komputer memiliki ribuan atau jutaan prosesor, seringkali thread harus berbagi satu atau lebih prosesor. Baik JVM atau sistem operasi platform yang mendasarinya menguraikan cara berbagi sumber daya prosesor di antara utas — tugas yang dikenal sebagai penjadwalan utas . Bagian JVM atau sistem operasi yang melakukan penjadwalan utas itu adalah penjadwal utas .

Catatan: Untuk menyederhanakan diskusi penjadwalan utas saya, saya fokus pada penjadwalan utas dalam konteks prosesor tunggal. Anda dapat mengekstrapolasi diskusi ini menjadi beberapa prosesor; Saya serahkan tugas itu kepada Anda.

Ingat dua poin penting tentang penjadwalan utas:

  1. Java tidak memaksa VM untuk menjadwalkan utas dengan cara tertentu atau berisi penjadwal utas. Itu menyiratkan penjadwalan utas yang bergantung pada platform. Oleh karena itu, Anda harus berhati-hati saat menulis program Java yang perilakunya bergantung pada bagaimana thread dijadwalkan dan harus beroperasi secara konsisten di berbagai platform.
  2. Untungnya, saat menulis program Java, Anda perlu memikirkan tentang bagaimana Java menjadwalkan utas hanya ketika setidaknya satu utas program Anda banyak menggunakan prosesor untuk jangka waktu yang lama dan hasil antara dari eksekusi utas itu terbukti penting. Misalnya, applet berisi utas yang secara dinamis membuat gambar. Secara berkala, Anda ingin utas lukisan menggambar konten gambar saat ini sehingga pengguna dapat melihat bagaimana kemajuan gambar. Untuk memastikan bahwa thread kalkulasi tidak memonopoli prosesor, pertimbangkan penjadwalan thread.

Periksa program yang membuat dua utas intensif prosesor:

Daftar 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemomembuat dua utas yang masing-masing menghitung nilai pi (lima kali) dan mencetak setiap hasil. Bergantung pada bagaimana implementasi JVM menjadwalkan thread, Anda mungkin melihat keluaran yang menyerupai berikut ini:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Menurut output di atas, penjadwal utas berbagi prosesor di antara kedua utas. Namun, Anda bisa melihat keluaran yang serupa dengan ini:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Output di atas menunjukkan penjadwal utas yang mengutamakan satu utas di atas utas lainnya. Dua keluaran di atas menggambarkan dua kategori umum penjadwal utas: hijau dan asli. Saya akan mengeksplorasi perbedaan perilaku mereka di bagian selanjutnya. Saat membahas setiap kategori, saya mengacu pada status utas, yang mana ada empat:

  1. Keadaan awal: Sebuah program telah membuat objek utas, tetapi utas belum ada karena metode objek utas start()belum dipanggil.
  2. Status yang dapat dijalankan: Ini adalah status default utas. Setelah panggilan start()selesai, utas menjadi dapat dijalankan apakah utas itu berjalan atau tidak, yaitu, menggunakan prosesor. Meskipun banyak utas mungkin dapat dijalankan, saat ini hanya satu yang berjalan. Penjadwal utas menentukan utas mana yang dapat dijalankan untuk ditetapkan ke prosesor.
  3. Diblokir negara: Ketika sebuah thread mengeksekusi sleep(), wait()atau join()metode, ketika upaya thread untuk membaca data belum tersedia dari jaringan, dan ketika thread menunggu untuk memperoleh kunci, benang yang ada di negara diblokir: itu yang tidak berjalan atau dalam posisi untuk berlari. (Anda mungkin dapat memikirkan di lain waktu ketika sebuah utas akan menunggu sesuatu terjadi.) Ketika utas yang diblokir membuka blokir, utas itu pindah ke status dapat dijalankan.
  4. Status penghentian: Setelah eksekusi meninggalkan metode utas run(), utas itu berada dalam status penghentian. Dengan kata lain, utas tidak ada lagi.

Bagaimana penjadwal utas memilih utas mana yang dapat dijalankan untuk dijalankan? Saya mulai menjawab pertanyaan itu sambil membahas penjadwalan benang hijau. Saya menyelesaikan jawabannya sambil mendiskusikan penjadwalan utas asli.

Penjadwalan benang hijau

Tidak semua sistem operasi, sistem operasi Microsoft Windows 3.1 kuno, misalnya, mendukung utas. Untuk sistem seperti itu, Sun Microsystems dapat merancang JVM yang membagi satu-satunya rangkaian eksekusi menjadi beberapa rangkaian. JVM (bukan sistem operasi platform yang mendasarinya) memasok logika threading dan berisi penjadwal utas. Utas JVM adalah utas hijau, atau utas pengguna .

Penjadwal utas JVM menjadwalkan utas hijau sesuai dengan prioritas — kepentingan relatif utas, yang Anda ekspresikan sebagai integer dari rentang nilai yang ditentukan dengan baik. Biasanya, penjadwal utas JVM memilih utas dengan prioritas tertinggi dan memungkinkan utas itu berjalan hingga berakhir atau diblokir. Pada saat itu, penjadwal utas memilih utas dengan prioritas tertinggi berikutnya. Utas itu (biasanya) berjalan sampai berakhir atau diblokir. Jika, saat utas berjalan, utas dengan prioritas lebih tinggi membuka blokir (mungkin waktu tidur utas dengan prioritas lebih tinggi kedaluwarsa), penjadwal utas mendahului, atau menyela, utas dengan prioritas lebih rendah dan menetapkan utas prioritas tinggi yang tidak diblokir ke prosesor.

Catatan: Utas yang dapat dijalankan dengan prioritas tertinggi tidak akan selalu berjalan. Berikut ini Spesifikasi Bahasa Java yang diprioritaskan:

Setiap utas memiliki prioritas. Ketika ada persaingan untuk memproses sumber daya, utas dengan prioritas lebih tinggi biasanya dieksekusi daripada utas dengan prioritas lebih rendah. Namun, preferensi tersebut bukan jaminan bahwa utas dengan prioritas tertinggi akan selalu berjalan, dan prioritas utas tidak dapat digunakan untuk mengimplementasikan pengecualian bersama secara andal.

Pengakuan itu menunjukkan banyak hal tentang penerapan JVM benang hijau. JVM tersebut tidak dapat membiarkan utas diblokir karena itu akan mengikat satu-satunya utas eksekusi JVM. Oleh karena itu, ketika utas harus memblokir, seperti ketika utas itu membaca data dengan lambat untuk tiba dari file, JVM mungkin menghentikan eksekusi utas dan menggunakan mekanisme polling untuk menentukan kapan data tiba. Sementara utas tetap berhenti, penjadwal utas JVM mungkin menjadwalkan utas dengan prioritas lebih rendah untuk dijalankan. Misalkan data tiba saat utas dengan prioritas lebih rendah sedang berjalan. Meskipun utas dengan prioritas lebih tinggi harus berjalan segera setelah data tiba, itu tidak terjadi sampai JVM selanjutnya memeriksa sistem operasi dan menemukan kedatangannya. Karenanya, utas dengan prioritas lebih rendah tetap berjalan meskipun utas dengan prioritas lebih tinggi harus dijalankan.Anda perlu mengkhawatirkan situasi ini hanya jika Anda memerlukan perilaku real-time dari Java. Tetapi Java bukanlah sistem operasi waktu nyata, jadi mengapa khawatir?

Untuk memahami thread hijau yang dapat dijalankan yang menjadi thread hijau yang sedang berjalan, pertimbangkan hal berikut. Misalkan aplikasi Anda terdiri dari tiga utas: utas utama yang menjalankan main()metode, utas kalkulasi, dan utas yang membaca input keyboard. Jika tidak ada input keyboard, utas pembacaan akan terhalang. Asumsikan utas pembacaan memiliki prioritas tertinggi dan utas kalkulasi memiliki prioritas terendah. (Demi kesederhanaan, asumsikan juga bahwa tidak ada thread JVM internal lain yang tersedia.) Gambar 1 mengilustrasikan eksekusi ketiga thread ini.

Saat T0, utas utama mulai berjalan. Saat T1, utas utama memulai utas kalkulasi. Karena utas kalkulasi memiliki prioritas lebih rendah daripada utas utama, utas kalkulasi menunggu prosesor. Pada saat T2, utas utama memulai utas pembacaan. Karena utas pembacaan memiliki prioritas lebih tinggi daripada utas utama, utas utama menunggu prosesor sementara utas pembacaan berjalan. Pada saat T3, blok utas pembacaan dan utas utama berjalan. Pada saat T4, utas pembacaan dibuka dan dijalankan; utas utama menunggu. Akhirnya, pada waktu T5, blok thread pembacaan dan thread utama berjalan. Pergantian eksekusi antara pembacaan dan utas utama ini terus berlanjut selama program berjalan. Utas kalkulasi tidak pernah berjalan karena memiliki prioritas terendah dan dengan demikian kekurangan perhatian prosesor,situasi yang dikenal sebagaikelaparan prosesor .

Kita dapat mengubah skenario ini dengan memberikan utas kalkulasi prioritas yang sama dengan utas utama. Gambar 2 menunjukkan hasilnya, dimulai dengan waktu T2. (Sebelum T2, Gambar 2 identik dengan Gambar 1.)

Pada saat T2, utas pembacaan berjalan sementara utas utama dan kalkulasi menunggu prosesor. Pada saat T3, thread pembacaan memblokir dan thread kalkulasi berjalan, karena thread utama berjalan tepat sebelum thread pembacaan. Pada saat T4, utas pembacaan dibuka dan dijalankan; utas utama dan kalkulasi menunggu. Pada saat T5, thread pembacaan memblokir dan thread utama berjalan, karena thread kalkulasi berjalan tepat sebelum thread pembacaan. Pergantian eksekusi antara utas utama dan kalkulasi ini berlanjut selama program berjalan dan bergantung pada utas dengan prioritas lebih tinggi yang sedang berjalan dan memblokir.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

Pembalikan prioritas dapat sangat menunda eksekusi utas dengan prioritas lebih tinggi. Misalnya, Anda memiliki tiga utas dengan prioritas 3, 4, dan 9. Utas prioritas 3 sedang berjalan dan utas lainnya diblokir. Asumsikan bahwa utas prioritas 3 mengambil kunci, dan utas prioritas 4 membuka blokir. Utas prioritas 4 menjadi utas yang sedang berjalan. Karena utas prioritas 9 memerlukan kunci, utas prioritas 3 terus menunggu hingga utas prioritas 3 melepaskan kunci. Namun, utas prioritas 3 tidak dapat membuka kunci hingga utas prioritas 4 terhalang atau dihentikan. Akibatnya, thread prioritas 9 menunda eksekusinya.