Pemrograman Serentak (Concurrency)
Java adalah bahasa pemrograman banyak thread, yang artinya beberapa hal bisa dilakukan bersama-sama. Thread adalah unit terkecil dari eksekusi suatu program. Thread mengeksekusi rangkaian instruksi satu demi satu. Ketika sistem menjalankan program, komputer akan membuat thread baru. (Thread dalam konteks ini disebut proses, akan tetapi perbedaanya tidank penting di sini). Instruksi-instruksi dalam program akan dieksekusi oleh thread ini secara berantai, satu demi satu dari awal hingga akhir. Thread disebut "mati" jika program selesai dieksekusi.Dalam sistem komputer modern, beberapa thread bisa tercipta dalam satu waktu. Pada satu saat tertentu, hanya ada satu thread yang bisa dijalankan, karena CPI hanya bisa melakukan satu hal dalam satu waktu. (Pada komputer dengan multiprosesor, multicore, dan hyper-threading, masing-masing prosesor atau core melakukan thread yang berbeda-beda). Akan tetapi sebenarnya komputer membagi waktu menjadi bagian-bagian kecil sehingga seolah-olah seluruh thread dijalankan secara bersama-sama. Pembagian waktu berarti CPU mengeksekusi suatu thread dalam kurun waktu tertentu, setelah itu beralih mengeksekusi thread yang lain, kemudian thread lain, dan seterusnya dan kemudian kembali ke thread pertama -- kira-kira 100 kali per detik. Di mata user, semua thread berjalan pada saat yang sama.
Java adalah bahasa pemrograman banyak thread. Artinya Java bisa membuat satu atau lebih thread yang bisa dijalankan secara paralel. Hal ini adalah bagian mendasar, yang dibuat di dalam core bahasa, bukan merupakan tambahan (add-on) seperti bahasa pemrograman lain. Tetap saja pemrogaman dengan banyak thread adalah sesuatu yang tidak mudah.
Penggunaan thread yang banyak digunakan adalah untuk membuat GUI (graphical user interface) yang responsif. Pada dasarnya suatu program harus dapat terus bejalan dan pada saat yang sama tetap bisa menerima input dari user, menanggapi klik mouse, dan sebagainya.
Thread juga digunakan untuk mempercepat suatu proses, misalnya kita ingin membuat program yang menunggu suatu input I/O dari network, dan pada saat yang sama mengolahnya sehingga proses pengolahan berjalan serentak. Jika program harus menunggu seluruh input datang baru kemudian melakukan pengolahan, tentunya akan memakan waktu yang lebih lama, terutama apabila aliran network lambat atau pengolahannya memakan waktu lama.
Jika kita memiliki CPU multiprocessor atau multicore, maka menggunakan banyak thread akan mempercepat eksekusi program, karena masing-masing thread dijalankan secara terpisah. Misalnya untuk melakukan video encoding dengan jumlah data besar, jika kita menggunakan seluruh core yang tersedia maka prosesnya akan dapat diselesaikan dengan cepat.
Dasar-dasar Thread
Cara termudah untuk membuat thread adalah membuat kelas turunan darijava.lang.Thread
, yang memiliki semua metode untuk membuat dan menjalankan thread. Metode paling penting adalah run()
, yang bisa kita beban-lebihkan untuk melakukan tugas yang kita butuhkan. Atau dengan kata lain run()
adalah metode yang akan dijalankan bersamaan dengan thread lain.Contoh berikut membuat 5 thread, masing-masing memiliki nomor identifikasi unik yang dibuat dengan menggunakan variabel statik. Metode
run()
dibebanlebihkan untuk menghitung mundur hingga hitungMundur
bernilai nol. Setelah metode run()
selesai dijalankan, thread akan mati secara otomatis.(Contoh-contoh pada bagian ini bisa diunduh untuk diimport ke dalam Eclipse. Lihat akhir halaman ini untuk tautannya)
package com.lyracc.threaddasar1; public class ThreadDasar extends Thread { private int hitungMundur = 5; private static int jumlahThread = 0; public ThreadDasar() { super("Thread ke-" + ++jumlahThread); start(); } public void run() { while (true) { System.out.println( getName() + " : " + hitungMundur ); if (--hitungMundur == 0) return; } } /** * @param args */ public static void main(String[] args) { for(int i = 0; i < 5; i++) new ThreadDasar(); } }
run()
melakukan penghitungan mundur, yaitu dengan menggunakan metode getName()
.Metode
run()
pada thread biasanya memiliki perulangan internal yang akan terus menerus dipanggil hingga tidak lagi digunakan. Kita harus membuat suatu kondisi sehingga bisa keluar dari perulangan tersebut (misalnya pada contoh di atas, perulangan akan selesai jika hitungMundur
bernilai 0). Seringkali, run()
dijalankan di dalam perulangan yang tak pernah berhenti (kita akan lihat nanti bagaimana menghentikan suatu thread dengan aman).Pada metode
main()
, thread dibuat beberapa kali kemudian dijalankan. Metode start()
pada kelas Thread
digunakan untuk melakukan tugas tertentu sebelum metode run()
dijalankan. Jadi, langkah-langkahnya adalah : konstruktor dipanggil untuk membuat objek, kemudian memanggil start()
untuk melakukan konfigurasi thread, dan kemudian metode run()
dijalankan. Jika kita tidak memanggil start()
maka metode run()
tidak akan pernah dijalankan.Keluaran dari program ini akan berbeda setiap kali dijalankan, karena penjadwalan thread tidak dapat ditentukan dengan pasti (non-deterministik). Bahkan, kita bisa melihat perbedaan yang sangat jelas ketika kita menggunakan versi JDK yang berbeda. Misalnya, JDK lama tidak melakukan pembagian waktu lebih cepat, artinya, 1 thread mungkin bisa melakukan tugasnya dengan cepat hingga selesai sebelum thread lain dijalankan. Pada JDK lain kita akan melihat program akan mencetak 5 untuk seluruh thread hingga 1 untuk seluruh thread. Artinya pembagian waktunya lebih baik, karena setiap thread memiliki kesempatan yang sama untuk menjalankan program. Karenanya, untuk membuat suatu program multi-threading, kita tidak boleh terpaku pada keluaran suatu kompiler. Program kita harus dibuat seaman mungkin.
Ketika objek
Thread
dibuat pada metode main()
, kita lihat bahwa kita tidak menyimpan referensi ke objek tersebut. Pada objek biasa, tentunya objek ini akan langsung ditangkap oleh pemulung memori karena objek ini tidak direferensikan di manapun. Akan tetapi pada thread, objek hanya bisa diambil oleh pemulung memori jika metode run()
selesai dijalankan. Pada contoh di atas, program masih bisa berjalan seperti biasa, dan objek Thread
akan diberikan kepada pemulung memori setelah mencetak angka 1.Yielding (menghasilkan)
Jika kita tahu bahwa kita telah mendapatkan hasil yang kita inginkan pada metode
run()
, kita bisa memberi tahu penjadwal thread bahwa kita telah selesai dan memberi jalan kepada thread lain untuk mendapatkan kesempatan pada CPU. Akan tetapi ini hanya sebagai petunjuk, yang artinya belum tentu dijalankan oleh penjadwal thread.Misalnya pada contoh di atas, kita bisa mengganti isi metode
run()
denganpublic void run() { while (true) { System.out.println( getName() + " : " + hitungMundur ); if (--hitungMundur == 0) return; yield(); } }
Tidur (sleeping)
Cara lain untuk mengatur perilaku thread kita adalah dengan memanggil
sleep
untuk menunda eksekusi thread selama waktu tertentu (dalam mili detik). Misalnya pada kode berikut, kita ubah metode run()
menjadi seperti :public void run() { while (true) { System.out.println( getName() + " : " + hitungMundur ); if (--hitungMundur == 0) return; try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } }
sleep()
, metode ini harus diletakkan di dalam blok try
karena sleep()
bisa melemparkan pengecualian, yaitu jika tidurnya diganggu sebelum waktunya selesai. Hal ini terhadi misalnya apabila thread lain yang memiliki referensi ke thread ini memanggil interrupt()
pada thread ini. Pada contoh di atas, kita lemparkan lagi pengecualian yang terjadi dengan pengecualian lain bertipe RuntimeException
, karena kita tidak tahu bagaimana pengecualian ini harus ditangani, dan membiarkan metode yang memanggilnya menangkap pengecualian baru ini.Metode
sleep()
tidak digunakan untuk mengatur bagaimana thread akan berjalan menurut urutan tertentu. Metode ini hanya menghentikan eksekusi suatu thread sementara. Yang dijamin adalah bahwa thread akan tidur selama paling sedikit 100 mili detik (atau mungkin sedikit lebih lama hingga thread jalan kembali). Urutan thread diatur oleh penjadwal thread yang memiliki mekanisme sendiri tergantung dari keadaan thread lain atau bahkan aplikasi lain di luar Java, oleh karena itu sifatnya disebut non-deterministik.Jika kita harus mengatur thread mana dahulu yang harus dijalankan, cara terbaik mungkin tidak menggunakan thread sama sekali, atau mendesain agar suatu thread memanggil thread lain dengan suatu urutan tertentu. Tentunya cara terakhir lebih rumit dari yang dibayangkan.
Prioritas
Prioritas suatu thread digunakan untuk memberi tahu penjadwal thread tentang prioritas thread tersebut. Tetap saja urutannya tidak bisa ditentukan karena sifatnya yang non-deterministik. Jika ada beberapa thread yang sedang diblok dan menunggu giliran untuk dijalankan, penjadwal thread akan cenderung menjalankan thread dengan prioritas tertinggi terlebih dahulu. Akan tetapi, tidak berarti thread dengan prioritas rendah tidak akan pernah dijalankan, hanya lebih jarang dijalankan ketimbang thread dengan prioritas tinggi.
Perhatikan contoh berikut :
package com.lyracc.prioritasthread; public class PrioritasThread extends Thread { private int hitungMundur = 5; private volatile double d = 0; // No optimization public PrioritasThread(int prioritas) { setPriority(prioritas); start(); } public void run() { while (true) { for(int i = 1; i < 100000; i++) d = d + (Math.PI + Math.E) / (double)i; System.out.println(this.toString() + " : " + hitungMundur); if (--hitungMundur == 0) return; } } /** * @param args */ public static void main(String[] args) { new PrioritasThread(Thread.MAX_PRIORITY); for(int i = 0; i < 5; i++) new PrioritasThread(Thread.MIN_PRIORITY); } }
main()
kita buat 6 thread, yang pertama dengan prioritas maximum, dan yang lain dengan prioritas minimum. Perhatikan keluarannya, bagaimana thread pertama dijalankan lebih dulu sedangkan thread-thread lain berjalan seperti biasa dalam kondisi acak karena memiliki prioritas yang sama.Di dalam metode
run()
kita lakukan perhitungan matematika selama 100.000 kali. Tentunya ini perhitungan yang memakan waktu sehingga setiap thread harus menunggu giliran di saat thread lain sedang dijalankan. Tanpa perhitungan ini, thread akan dilaksanakan sangat cepat dan kita tidak bisa melihat efek dari prioritas thread.Prioritas suatu thread bisa kita set kapan saja (tidak harus pada konstruktor) dengan metode
setPriority(int prioritas)
dan kita bisa membaca prioritas suatu thread dengan menggunakan metode getPriority()
.Meskipun JDK memiliki 10 tingkat prioritas, akan tetapi sistem operasi memiliki tingkat prioritas yang berbeda-beda. Windows misalnya memiliki 7 tingkat dan Solaris memiliki 231 tingkat prioritas. Yang lebih pasti adalah menggunakan konstanta
MAX_PRIORITY
, NORM_PRIORITY
, dan MIN_PRIORITY
pada kelas thread.Thread Daemon
Thread daemon adalah thread yang bekerja di belakang layar yang memberikan layanan umum kepada thread-thread lain selama program berjalan, akan tetapi thread ini bukan bagian penting dari suatu program. Artinya ketika semua thread yang bukan daemon selesai dijalankan, program akan berhenti, dan jika masih ada thread non-daemon yang masih dieksekusi, program tidak akan berhenti.
Perhatikan contoh program berikut ini.
package com.lyracc.threaddaemon; public class ThreadDaemon extends Thread { public ThreadDaemon() { setDaemon(true); // Harus dipanggil sebelum start start(); } public void run() { while (true) { try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(this); } } /** * @param args */ public static void main(String[] args) { for (int i = 0; i < 5; i++) new ThreadDaemon(); } }
setDaemon()
sebelum metode start()
dipanggil. Pada metode run()
, thread diperintahkan untuk tidur selama 100 mili detik. Ketika semua thread dimulai, program langsung berhenti sebelum thread bisa mencetak dirinya. Ini karena semua thread kecuali main()
adalah thread daemon. Hanya thread non-daemon saja yang bisa mencegah program untuk terus berjalan.Untuk mengetahui suatu thread adalah thread daemon atau bukan, kita bisa menggunakan perintah
isDaemon()
. Suatu thread daemon akan membuat thread yang juga merupakan thread daemon. Menggabungkan thread
Perintah
join()
bisa digunakan pada thread lain untuk menunda eksekusi hingga thread lain tersebut selesai dijalankan. Misalnya, jika thread a
memanggil t.join()
pada thread t
, maka eksekusi thread a akan terhenti sementara hingga thread t selesai dijalankan (atau ketika t.isAlive()
bernilai false
).Kita bisa juga memanggil
join()
dengan argumen waktu (baik dalam mili detik, ataupun milidetik dan nanodetik), yaitu jika thread target tidak selesai dalam kurun waktu tersebut, eksekusi pada thread induk akan kembali dilakukan.Panggilan
join()
bisa dibatalkan dengan memanggil interrupt()
pada thread induk, sehingga klausa try ... catch
diperlukan pada metode join()
.Mari kita lihat contoh berikut ini.
package com.lyracc.joindemo; class ThreadPemalas extends Thread { private int waktu; public ThreadPemalas(String namaThread, int waktuTidur) { super(namaThread); waktu = waktuTidur; start(); } public void run() { try { sleep(waktu); } catch (InterruptedException e) { System.out.println(getName() + " dibangunkan. " + "isInterrupted(): " + isInterrupted()); return; } System.out.println(getName() + " sudah bangun."); } } class ThreadPenggabung extends Thread { private ThreadPemalas sleeper; public ThreadPenggabung(String namaThread, ThreadPemalas pemalas) { super(namaThread); this.sleeper = pemalas; start(); } public void run() { try { sleeper.join(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(getName() + " selesai setelah " + sleeper.getName()); } } public class JoinDemo { /** * @param args */ public static void main(String[] args) { ThreadPemalas brr = new ThreadPemalas("brr", 2000); ThreadPemalas grr = new ThreadPemalas("grr", 2000); ThreadPenggabung saya = new ThreadPenggabung("saya",brr); ThreadPenggabung anda = new ThreadPenggabung("anda",grr); grr.interrupt(); } }
ThreadPemalas
adalah thread yang akan ditidurkan sepanjang waktu yang diberikan pada konstruktornya. Metode run()
bisa berhenti jika waktu tidur sudah habis atau ada interupsi yang terjadi. Di dalam klausa catch
, interupsi akan dilaporkan. Fungsi isInterrupted()
melaporkan apakah thread ini diinterupsi atau tidak. Akan tetapi ketika thread ini diinterupsi, kemudian pengecualiannya ditangkap oleh klausa catch
, misalnya, maka tanda interupsi akan segera dihapus. Oleh karenanya isInterrupted()
akan selalu bernilai false
pada program di atas. Tanda interupsi akan digunakan pada situasi lain yang mungkin berada di luar pengecualian.ThreadPenggabung
adalah thread yang menunggu hingga ThreadPemalas
selesai dengan tugasnya, yaitu dengan memanggil join()
ke objek ThreadPemalas
pada metode run()
-nya.Pada metode utama
main()
, setiap ThreadPemalas
tersambung pada ThreadPenggabung
. Dan kita lihat pada keluarannya, jika ThreadPemalas
selesai bekerja, baik karena dibangunkan melalui interupsi atau karena waktu sudah selesai, ThreadPenggabung
yang tersambung juga akan menyelesaikan tugasnya.Variasi Kode
Pada contoh-contoh di atas, semua objek thread yang kita buat diturunkan dari kelas
Thread
. Kita hanya membuat objek yang berfungsi sebagai thread dan tidak memiliki tugas dan fungsi lain. Akan tetapi, kelas kita mungkin saja merupakan kelas turunan dari kelas lain. Karena Java tidak mendukung pewarisan berganda, kita tidak bisa menurunkan kelas tersebut bersamaan dengan kelas Thread
.Dalam hal ini, kita bisa menggunakan cara alternatif yaitu dengan mengimplementasi interface
Runnable
. Runnable
hanya memiliki satu metode untuk diimplementasi, yaitu metode run()
.Contoh berikut mendemonstrasikan contoh penggunaannya :
package com.lyracc.runnablesederhana; public class RunnableSederhana implements Runnable { private int hitungMundur = 5; public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; } } public static void main(String[] args) { for (int i = 1; i <= 5; i++) { // Buat thread baru dan jalankan Thread a = new Thread(new RunnableSederhana(), "Thread ke-" + i); a.start(); } } }
RunnableSederhana
adalah metode run()
, akan tetapi jika kita ingin melakukan hal lainnya, seperti getName()
, sleep()
, dan lainnya, kita harus secara eksplisit memberikan referensi dengan menggunakan Thread.currentThread()
.Ketika suatu kelas mengimplementasikan interface
Runnable
, artinya kelas ini memiliki metode bernama run()
, akan tetapi tidak berarti bahwa kelas ini bisa melakukan sesuatu seperti kelas Thread
atau kelas-kelas turunan yang kita buat dari kelas ini. Kita harus membuat objek Thread
sendiri seperti ditunjukkan dalam metode main()
di atas, kemudian menjalankan start()
sendiri.Kemudahan yang ditawarkan oleh interface
Runnable
adalah kemungkinan untuk menggabungkannya dengan kelas dan interface lain. Misalnya kita ingin membuat kelas baru yang merupakan kelas turunan dari suatu kelas lain. Kita cukup menambahkan impement Runnable
pada definisi kelasnya untuk membuat kelas yang bisa kita jadikan thread. Dengan cara ini, kita masih bisa mengakses anggota kelas induk secara langsung, tanpa melalui objek lain. Akan tetapi, kelas dalam (inner class) juga bisa mengakses anggota kelas luar (outer class). Kadang-kadang kita ingin juga membuat kelas dalam yang merupakan turunan dari kelas Thread
.Perhatikan beberapa variasi untuk mendeklarasikan dan menggunakan thread pada contoh berikut ini.
package com.lyracc.variasithread; // Kelas dalam bernama class KelasDalamBernama { private int hitungMundur = 5; private Dalam dalam; // Kelas Dalam adalah kelas dalam (inner class) yang // merupakan kelas turunan kelas Thread private class Dalam extends Thread { Dalam(String nama) { super(nama); start(); } public void run() { while (true) { System.out.println(getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; try { sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } // akhir Dalam // Konstruktor KelasDalamBernama // Membuat objek baru yang merupakan instansi kelas Dalam public KelasDalamBernama(String nama) { dalam = new Dalam(nama); } } // akhir KelasDalamBernama // Kelas dalam anonim class KelasDalamAnonim { private int hitungMundur = 5; private Thread t; // Konstruktor KelasDalamAnonim public KelasDalamAnonim(String nama) { // Kelas anonim turunan Thread t = new Thread(nama) { public void run() { while (true) { System.out.println(getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; try { sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; // akhir kelas anonim t.start(); } } // akhir KelasDalamAnonim // Kelas dalam implementasi runnable bernama class KelasRunnableBernama { private int hitungMundur = 5; private Dalam dalam; // Kelas Dalam adalah kelas dalam (inner class) yang // merupakan kelas yang mengimplementasi Runnable private class Dalam implements Runnable { Thread t; Dalam(String nama) { t = new Thread(this, nama); t.start(); } public void run() { while (true) { System.out.println(t.getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } } // akhir kelas Dalam // Konstruktor KelasRunnableBernama // Membuat objek baru yang merupakan instansi kelas Dalam public KelasRunnableBernama(String nama) { dalam = new Dalam(nama); } } // akhir KelasRunnableBernama // Kelas dalam implementasi runnable anonim class KelasRunnableAnonim { private int hitungMundur = 5; private Thread t; public KelasRunnableAnonim(String nama) { t = new Thread(new Runnable() { public void run() { while (true) { System.out.println(Thread.currentThread().getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; try { Thread.sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }, nama); // akhir kelas dalam anonim t.start(); } } // akhir KelasRunnableAnonim // Menjalankan thread dari dalam metode dan kelas anonim class ThreadViaMetode { private int hitungMundur = 5; private Thread t; private String nama; public ThreadViaMetode(String nama) { this.nama = nama; } public void runThread() { if (t == null) { // Definisi kelas anonim dari dalam metode t = new Thread(nama) { public void run() { while (true) { System.out.println(getName() + " : " + hitungMundur); if (--hitungMundur == 0) return; try { sleep(10); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }; // akhir kelas dalam anonim t.start(); } } } // akhir ThreadViaMetode public class VariasiThread { public static void main(String[] args) { new KelasDalamBernama("KelasDalamBernama"); new KelasDalamAnonim("KelasDalamAnonim"); new KelasRunnableBernama("KelasRunnableBernama"); new KelasRunnableAnonim("KelasRunnableAnonim"); new ThreadViaMetode("ThreadViaMetode").runThread(); } }
Runnable
, pada dasarnya kita menyatakan bahwa kita ingin membuat suatu proses -- yang implementasinya berada di dalam metode run()
-- bukan suatu objek yang melakukan proses tertentu. Tentunya hal ini tergantung dari cara pandang kita, apakah kita ingin menganggap suatu thread sebagai objek atau sesuatu yang sama sekali berbeda, yaitu proses.Jika kita menganggap suatu thread sebagai proses, tetntunya kita akan terbebas dari cara pandang berorientasi objek yaitu "semuanya adalah objek". Artinya juga, kita tidak perlu membuat seluruh kelas menjadi
Runnable
jika hanya kita ingin memulai proses di bagian tertentu program kita. Karenanya, mungkin lebih masuk akal untuk menyembunyikan thread di dalam kelas kita menggunakan kelas dalam.KelasDalamBernama[.code] membuat kelas dalam yang merupakan kelas turunan dari kelas Thread, dan membuat instansi kelas ini di dalam konstruktornya. Cara ini baik jika kita ingin kelas dalam tersebut memiliki suatu kemampuan tertentu (metode lain) yang ingin kita gunakan. Akan tetapi, seringkali kita membuat thread hanya untuk memanfaatkan [code]Thread
saja, artinya kita mungkin tidak perlu membuat kelas yang memiliki nama.KelasDalamAnonim
adalah alternatif dari KelasDalamBernama
di mana kelas dalamnya merupakan kelas anonim yang merupakan kelas turunan dari kelas Thread
. Kelas anonim ini dibuat di dalam konstruktor dan disimpan dalam bentuk referensi t
bertipe Thread
. Jika metode kelas lain membutuhkan akses ke t
, maka kita bisa menggunakannya seperti Thread
biasa tanpa perlu mengetahui tipe objek t
sesungguhnya.Kelas ketiga dan keempat pada contoh di atas mirip dengan contoh pertama dan kedua, akan tetapi menggunakan interface
Runnable
. Contoh ini hanya ingin menunjukkan bahwa menggunakan Runnable
tidak menambah nilai apa-apa, kecuali membuat kodenya lebih sulit dibaca.Kelas
ThreadViaMetode
menunjukkan bagaimana membuat thread dari dalam metode. Kita bisa memanggil metode tersebut jika kita siap untuk menjalankan thread ini. Metode ini akan selesai setelah thread berjalan. Jika thread hanya melakukan tugas sampingan, mungkin cara ini lebih cocok daripada mengimplementasikan kelas khusus untuk melakukan fungsi-fungsi thread.Sisipan | Ukuran | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
threaddasar1.zip | 2.02 KB | ||||||||||||||||||||||||||||||
prioritasthread.zip | 1.21 KB | ||||||||||||||||||||||||||||||
threaddaemon.zip | 1.91 KB | ||||||||||||||||||||||||||||||
joindemo.zip | 3.69 KB | ||||||||||||||||||||||||||||||
runnablesederhana.zip | 2.13 KB | ||||||||||||||||||||||||||||||
variasithread.zip | 1.72 KB |
Berbagi Sumber Daya
Kita bisa bayangkan sebuah program dengan thread tunggal hanya memiliki satu hal yang berpindah dari satu bagian ke bagian lain secara harmonis, karena perpindahan data dari satu tempat ke tempat lain diatur hanya oleh satu alur. Jika ada dua atau lebih thread yang menggunakan, membaca, menulis, menghapus data yang sama, tentunya hal ini menjadi lebih rumit. Kita harus bisa memahami bagaimana thread-thread bekerja sama dalam berbagi sumber daya pada komputer, termasuk memori, hard disk, tampilan, input/output dan lain-lain, sehingga program yang kita buat menjadi lebih baik.Ini bukan hal yang mudah, terutama karena suatu thread bersifat non-deterministik. Kita tidak bisa menentukan atau memprediksi kapan suatu thread akan dijalankan oleh penjadwal. Bisa saja pada saat yang bersamaan dua thread mencoba untuk mengakses data yang sama, menghapus data yang sama, melakukan debit di rekening yang sama, mencetak di printer yang sama, menampilkan gambar di layar yang sama. Tabrakan sumber daya harus bisa dicegah sedini mungkin.
Cara Buruk Mengakses Sumber Daya
Kita ambil contoh berikut, di mana suatu kelas "menjamin" bahwa ia akan memberikan angka genap setiap kali kita memanggil
ambilNilai()
. Akan tetapi, ada thread kedua yang dinamakan Hansip
yang selalu memanggil ambilNilai()
untuk mengecek apakah nilainya selalu genap. Sepertinya ini cara yang tidak perlu, karena setelah kita melihat kode berikut, hasilnya pasti selalu genap. Akan tetapi, kita akan melihat ada beberapa kejutan yang terjadi.Berikut ini adalah program versi pertama.
package com.lyracc.selalugenap; public class SelaluGenap { private int i; public void berikut() { i++; i++; } public int ambilNilai() { return i; } public static void main(String[] args) { final SelaluGenap genap = new SelaluGenap(); new Thread("Hansip") { public void run() { while (true) { int nilai = genap.ambilNilai(); // Jika ganjil, keluar dan cetak nilainya if (nilai % 2 != 0) { System.out.println(nilai); System.exit(0); } } } }.start(); while (true) genap.berikut(); } }
main()
, objek SelaluGenap
akan dibuat -- sifatnya harus final
karena objek ini harus bisa diakses oleh kelas anonim yang berupa Thread
. Jika nilai yang dibaca oleh thread berupa bilangan ganjil, maka bilangan tersebut akan dicetak di layar kemudian keluar dari program.Apa yang terjadi adalah program pasti akan keluar dengan mencetak nilai ganjil. Ini berarti ada ketidakstabilan dalam program tersebut. Ini adalah contoh masalah mendasar dengan pemrograman banyak thread. Kita tidak pernah tahu kapan suatu thread akan jalan. Thread kedua bisa jalan ketika thread pertama baru selesai menjalankan
i++;
yang pertama di dalam metode berikut()
. Di sini thread kedua menganggap ada kesalahan perhitungan, padahal proses belum selesai.Kadang-kadang kita memang tidak peduli ketika suatu sumber daya (dalam contoh di atas, variabel i) sedang diakses apakah sedang digunakan atau tidak. Akan tetapi supaya program banyak thread bisa bekerja dengan baik, kita harus mencegah supaya dua thread tidak mengakses sumber daya yang sama, terutama di saat-saat kritis.
Mencegah tabrakan seperti ini bisa dicegah dengan meletakkan kunci pada sumber daya ketika sedang digunakan. Thread pertama yang sedang mengubah variabel
i
seharusnya mengunci variabel ini sehingga thread kedua yang ingin mengambil nilainya harus menunggu hingga proses penambahan selesai.Pemecahan Masalah Tabrakan Sumber Daya Bersama
Untuk memecahkan masalah tabrakan pada thread, hampir semua metode serentak melakukan akses serial ke suatu sumber daya yang digunakan bersama. Artinya hanya satu thread yang bisa mengakses suatu sumber daya pada suatu waktu. Biasanya hal ini dilakukan dengan membuat kunci sehingga satu thread saja yang bisa mengakses kunci tersebut. Kunci ini sering disebut mutex atau mutual exclusion.
Mari kita ambil contoh di rumah kita hanya ada satu kamar mandi. Beberapa orang (thread) ingin masuk ke kamar mandi (sumber daya bersama), dan mereka ingin masuk sendirian. Untuk masuk ke dalam kamar mandi, seseorang harus mengetok pintu untuk mengetahui apakah ada orang di dalamnya. Jika tidak ada, maka mereka bisa masuk dan mengunci pintunya. Thread lain yang mau menggunakan kamar mandi "diblok" sehingga tidak bisa masuk, sehingga thread harus menunggu hingga seseorang keluar dari kamar mandi.
Analogi di atas sedikit berbeda jika ketika seseorang keluar dari kamar mandi dan ada beberapa orang yang ingin mengakses kamar mandi secara bersamaan. Karena tidak ada "antrian" maka kita tidak tahu siapa yang harus masuk berikutnya, artinya penjadwal thread bersifat non-deterministik. Yang terjadi adalah, jika banyak orang menunggu di depan kamar mandi, maka siapa yang paling dekat dengan kamar mandi akan masuk terlebih dahulu. Seperti telah diulas sebelumnya, kita bisa memberi tahu penjadwal thread dengan perintah
yield
dan setPriority()
akan tetapi tetap saja masih sangat bergantung kepada JVM dan implementasi pada suatu platform dan tidak bisa ditentukan dengan pasti siapa yang berhak masuk terlebih dahulu.Java memiliki fitur untuk mencegah terjadinya tabrakan sumber daya, yaitu dengan menggunakan kata kunci
synchronized
. Ketika suatu thread berusaha untuk mengeksekusi suatu perintah yang diberi kata kunci synchronized
, Java akan mengecek apakah sumber daya tersebut tersedia. Jika ya, maka kunci ke sumber daya tersebut akan diambil, kemudian perintah dijalankan, dan setelah selesai melepaskannya kembali. Akan tetapi synchronized
tidak selalu berhasil.Sumber daya bersama bisa berbentuk lokasi memori (dalam bentuk objek), atau bisa juga berupa file, I/O atau bahkan printer. Untuk mengontrol akses ke sumber daya bersama, kita biasanya membungkusnya dalam bentuk objek. Metode lain yang mencoba untuk mengakses sumber daya tersebut bisa diberi kata kunci
synchronized
. Artinya jika thread sedang mengeksekusi salah satu metode synchronized
, thread lain diblok untuk mengeksekusi metode synchronized
lain dalam kelas itu hingga thread pertama selesai.Karena biasanya data dari suatu kelas kita buat
private
dan akses ke memori hanya bisa dilakukan dengan menggunakan metode, maka kita bisa mencegah tabrakan dengan membuat metode menjadi synchronized
. Berikut ini adalah contoh pendeklarasian synchronized
.synchronized void a() { /* perintah Anda di sini */ } synchronized void b() { /* perintah Anda di sini */ }
synchronized
, objek tersebut dikunci dan tidak boleh ada lagi metode synchronized
yang bisa dieksekusi hingga metode sebelumnya selesai dijalankan dan kunci dilepas. Karena hanya ada satu kunci untuk setiap objek, maka kita tidak mungkin menyimpan 2 data pada satu tempat pada saat yang bersamaan.Satu thread bisa mengunci objek beberapa kali. Ini terjadi jika satu metode memanggil metode lain di kelas yang sama, kemudian metode tersebut memanggil metode lain lagi di kelas yang sama dan seterusnya. JVM akan melacak berapa kali objek tersebut terkunci. Setiap kali suatu metode selesai, kunci akan dilepas. Ketika objek tidak terkunci lagi, maka kuncinya bernilai 0, yang artinya thread lain bisa mulai menggunakan metode pada objek ini.
Ada juga kunci per kelas, yang artinya kunci ini berlaku untuk suatu kelas. Otomatis semua objek yang diciptakan dari kelas yang sama memiliki kunci bersama. Caranya yaitu dengan menggunakan
synchronized static
metode sehingga suatu objek bisa juga mengunci kelas sehingga objek lain yang menggunakan metode ini tidak bisa jalan apabila sedang digunakan oleh objek lain.Memperbaiki SelaluGenap
Kita akan ubah sedikit program
SelaluGenap
di awal bagian ini untuk memberikan kata kunci synchronized
pada metode berikut()
dan ambilNilai()
. Jika kita hanya meletakkan kunci pada salah satu metode, maka metode yang tidak diberi kunci akan tetap bebas untuk dieksekusi mengabaikan ada atau tidaknya kunci. Di sini lah kunci pemrograman serentak, di mana kita harus memberi kunci di setiap akses ke sumber daya bersama.Metode ini akan berjalan terus menerus, oleh karena itu kita akan gunakan
waktuMulai
untuk menyimpan waktu ketika thread mulai berjalan, kemudian secara periodik mengecek waktu saat ini. Jika proses sudah berjalan lebih dari 4 detik, kita hentikan proses kemudian mencetak hasilnya.package com.lyracc.selalugenapsynchronized; public class SelaluGenapSynchronized { private int i; synchronized public void berikut() { i++; i++; } synchronized public int ambilNilai() { return i; } public static void main(String[] args) { final SelaluGenapSynchronized genap = new SelaluGenapSynchronized(); new Thread("Hansip") { // mencatat waktu ketika thread dimulai private long waktuMulai = System.currentTimeMillis(); public void run() { while (true) { int nilai = genap.ambilNilai(); // Jika ganjil, keluar dan cetak nilainya if (nilai % 2 != 0) { System.out.println(nilai); System.exit(0); } // Selesaikan program jika sudah melewati 4 detik if (System.currentTimeMillis() - waktuMulai > 4000) { System.out.println(nilai); System.exit(0); } } } }.start(); while (true) genap.berikut(); } }
Kadang-kadang kita hanya ingin mencegah beberapa thread untuk mengakses sebagian kode saja di dalam suatu metode, bukan keseluruhan metode. Bagian kode yang kita ingin lindungi ini disebut bagian kritis (critical section) dan juga bisa dibuat dengan kata kunci
synchronized
. Akan tetapi, kata kunci ini digunakan dengan menyatakan objek mana yang memiliki kunci yang harus dicek sebelum bagian ini dijalankan.Berikut ini adalah bentuk umum dari pernyataan
synchronized
untuk melindung bagian kritis :synchronized(objekKunci) { // Kode di bagian ini hanya bisa diakses // Jika objekKunci sedang tidak diakses oleh thread lain }
objekKunci
harus dicek terlebih dahulu. Jika thread lain telah mengunci ojek ini, maka bagian kritis tidak bisa dimasuki hingga thread lain selesai dan melepas kuncinya.Sisipan | Ukuran |
---|---|
selalugenap.zip | 1.11 KB |
selalugenapsynchronized.zip | 1.36 KB |
Siklus Hidup Thread
Suatu thread bisa berada dalam salah satu kondisi berikut :1. Baru : Objek thread baru saja dibuat, akan tetapi belum mulai dijalankan, sehingga belum bisa berbuat apa-apa.
2. Bisa-jalan : Artinya objek ini sudah dimulai dan sudah bisa dijalankan oleh mekanisme pembagian waktu oleh CPU. Sehingga thread ini bisa jalan kapan saja, selama diperintahkan oleh penjadwal thread.
3. Mati : suatu thread biasanya mati ketika selesai menjalankan metode
run()
. Sebelumnya, kita bisa memanggi metode stop()
, akan tetapi program bisa berada dalam kondisi tidak stabil jika metode ini dipanggil. Kita akan lihat beberapa metode lain untuk menghentikan thread di bagian berikutnya.4. Diblok : Thread seharusnya bisa berjalan, akan tetapi ada yang menghalanginya. Salah satunya adalah jika thread menunggu di bagian kritis sementara ada thread lain yang sedang menjalankan bagian kritis tersebut. Ketika suatu thread berada dalam kondisi diblok, penjadwal thread akan mengabaikannya dan tidak memberikan waktu CPU.
Bagaimana Suatu Thread Berada dalam Kondisi Diblok
Ketika suatu thread diblok, ada suatu alasan kenapa thread tersebut tidak bisa terus berjalan. Suatu thread dapat diblok karena beberapa alasan sebagai berikut :
- Kita memberi perintah thread untuk tidur dengan
sleep(milidetik)
sehingga thread tidak akan jalan dalam waktu yang sudah disebutkan - Kita memerintahkan thread untuk menunggu dengan perintah
wait()
. Thread tidak akan dijalankan kembali hingga diberikan pesannotify()
ataunotifyAll()
. - Thread sedang menunggu selesainya operasi I/O
- Thread mencoba memanggil metode dengan kata kunci
synchronized
, akan tetapi thread lain sedang memegang kuncinya.
Kerjasama Antar Thread
Setelah kita mengerti bagaimana thread bisa bertabrakan satu sama lain, dan bagaimana caranya mencegah tabrakan antar thread, langkah berikutnya adalah belajar bagaimana membuat thread dapat bekerja sama satu sama lain. Kuncinya adalah komunikias antar thread yang diimplementasi dengan aman dalam metode-metode pada kelasObject
, yaitu wait()
dan notify()
.wait() dan notify()
Pertama-tama penting untuk mengerti bahwa
sleep()
tidak melepas kunci thread ketika dipanggil. Artinya jika sleep()
dipanggil dari dalam bagian kritis, maka thread lain tidak bisa masuk hingga thread yang memanggil sleep()
bangun, meneruskan eksekusi, hingga keluar dari bagian kritis. Sedangkan wait()
melepas kunci ketika dipanggil, sehingga thread lain bisa masuk ke dalam bagian kritis.Ada dua bentuk
wait()
. Yang pertama memiliki argumen waktu dalam bentuk mili detik (mirip dengan sleep()
. Perbedaannya dengan sleep()
adalah :wait()
melepaskan kunci- Kita bisa membatalkan
wait()
dengan menggunakannotify()
ataunotifyAll()
, atau hingga waktu tunggu berlalu.
wait()
adalah wait()
yang tidak memiliki argumen. Jenis wait()
ini akan terus berlangsung hingga dibatalkan dengan notify
atau notifyAll()
.Aspek penting dari
wait()
, notify()
dan notifyAll()
adalah metode ini merupakan bagian dari kelas dasar Obejct
dan bukan bagian dari kelas Thread
seperti sleep()
. Meskipun kelihatan janggal, hal ini sangat penting karena semua objek memiliki kunci. Artinya kita bisa memanggil wait()
dari dalam metode synchronized
, tidak peduli apakah kelas tersebut merupakan kelas turunan dari Thread
atau bukan.Sebetulnya satu-satunya tempat kita bisa memanggil
wait()
, notify()
dan notifyAll()
adalah dari dalam blok atau metode synchronized
. (sleep()
bisa dipanggil dari manapun karena ia tidak berhubungan dengan kunci suatu objek). Jika kita memanggil wait()
, notify()
atau notifyAll()
dari luar metode atau blok synchronized
, compiler tidak akan memperingatkan Anda, akan tetapi ketika program dijalankan, kita akan mendapatkan pengecualian IllegalMonitorStateException
dengan pesan kesalahan yang tidak dimengerti, seprti "thread ini bukan pemiliknya". Pesan ini berarti bahwa thread yang memanggil wait()
, notify()
atau notifyAll()
harus memiliki kunci objek sebelum bisa memanggil salah satu metode ini.Kita juga bisa meminta suatu objek untuk memanipulasi kuncinya sendiri. Caranya, pertama-tama kita harus mengambil kuncinya. Misalnya, jika kita ingin memanggil
notify()
ke suatu objek x
, kita harus melakukannya di dalam blok synchronized
untuk mengambil kunci x
, seperti :synchronized(x) { x.notify(); }
wait()
digunakan jika kita menunggu sesuatu yang dikontrol oleh sesuatu di luar kontrol metode kita (di mana sesuatu ini hanya bisa diubah oleh thread lain). Kita tidak ingin menunggu dan berulang-ulang menguji apakah sesuatu itu sudah tersedia, karena cara ini akan memboroskan penggunaan CPU. Kita bisa menggunakan wait()
untuk memerintahkan suatu thread untuk menunggu hingga sesuatu tersebut berubah, dan hanya ketika notify()
dipanggil, maka thread tersebut akan bangun dan mengeceknya. Dengan kata lain wait()
digunakan melakukan aktifitas tak-sinkron antara beberapa thread.Sebagai contoh, anggap suatu restoran memiliki satu orang koki dan satu orang pelayan. Pelayan harus menunggu hingga si koki selesai memasak makanan. Ketika koki selesai, ia akan memberi tahu pelayan, kemudian membawa makanan ini ke customer, kemudian menunggu kembali. Koki di sini kita sebut sebagai produsen, dan pelayan disebut sebagai konsumen.
package com.lyracc.rumahmakan; class Pesanan { private int i = 0; public Pesanan(int i) { this.i = i; } public String toString() { return "pesanan " + i; } } // akhir kelas Pesanan class Pelayan extends Thread { private RumahMakan rumahMakan; public Pelayan(RumahMakan r) { rumahMakan = r; start(); } public void run() { while (true) { while (rumahMakan.pesanan == null) // tunggu hingga dipanggil dengan notify oleh Koki synchronized (this) { try { wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("Pelayan mengantarkan " + rumahMakan.pesanan); // pesanan sudah diantar, pesanan sekarang kosong rumahMakan.pesanan = null; } } } // akhir kelas Pelayan class Koki extends Thread { private RumahMakan rumahMakan; private Pelayan pelayan; public Koki(RumahMakan r, Pelayan p) { rumahMakan = r; pelayan = p; start(); } public void run() { // masak 10 makanan for (int i = 0; i < 10; i++) { if (rumahMakan.pesanan == null) { rumahMakan.pesanan = new Pesanan(i); System.out.print("Pesanan selesai! "); // coba panggil pelayan jika tidak sibuk synchronized (pelayan) { pelayan.notify(); } } try { sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println("Makanan habis.."); System.exit(0); } } // akhir kelas Koki public class RumahMakan { Pesanan pesanan; public static void main(String[] args) { RumahMakan rumahMakan = new RumahMakan(); Pelayan pelayan = new Pelayan(rumahMakan); new Koki(rumahMakan, pelayan); } }
<img src="/sites/java.lyracc.com/files/kerjasamathread_gbr1.png" alt="" />
Pesanan
adalah kelas sederhana yang berisi pesanan. Konstruktor menerima angka yang diibaratkan seperti pesanan, kemudian membebanlebihkan metode toString()
untuk mencetak objek ini langsung dengan System.out.println()
.Seorang
Pelayan
harus tahu RumahMakan
tempat ia bekerja, karena ia harus ke sana untuk mengantarkan pesanan dari "jendela pemesanan", yaitu rumahMakan.pesanan
. Pada metode run()
, Pelayan
masuk dalam mode menunggu. Kuncinya dimiliki oleh pelayan ini sendiri. Kunci ini yang akan digunakan oleh Koki
untuk membangunkan Pelayan
jika makanan sudah siap dengan metode notify()
.Pada aplikasi yang lebih kompleks, misalnya jika pelayannya banyak, kita bisa memanggil
notifyAll()
untuk membangunkan semua pelayan. Setiap pelayan nanti akan menguji apakah panggilan itu untuknya atau tidak.Perhatikan bahwa
wait()
ditulis di dalam pernyataan while
untuk menguji apakah pesanan sudah datang. Mungkin ini agak terasa ganjil karena ketika thread ini dibangunkan ketika menunggu pesanan, seharusnya pesanannya sudah tersedia khan? Masalahnya jika aplikasinya terdiri dari banyak pelayan, thread lain mungkin sudah keburu mengantarkan pesanannya ketika thread ini sedang bangun. Untuk itu, lebih aman apabila kita menggunakan bentuk berikut untuk semua aplikasi yang menggunakan wait()
:while (sesuatuYangDitunggu) wait();
Objek
Koki
harus tahu di rumah makan mana ia bekerja. Pesanan yang dia masak akan dia letakkan pada jendela pesanan (dalam hal ini rumahMakan.pesanan
) dan dia juga harus tahu siapa Pelayan
yang akan mengantarkan pesanan yang sudah selesai dimasak.Pada contoh sederhana di atas,
Koki
membuat objek Pesanan
, kemudian setelah selesai akan memanggil Pelayan
dengan notify()
. Karena panggilan notify()
dilakukan di dalam klausa synchronized
, maka sudah bisa dipastikan Koki
memanggil pelayan jika pelayan tersebut sedang tidak digunakan oleh thread lain.Sisipan | Ukuran |
---|---|
rumahmakan.zip | 1.44 KB |
Kunci Mati (Deadlock)
Thread bisa diblok dan objek bisa memanggil metodesynchronized
ke suatu objek sehingga objek lain tidak bisa mengakses objek tersebut hingga kuncinya dilepas. Karenanya mungkin saja satu thread tersangkut menunggu suatu thread, akan tetapi thread yang ditunggu ini juga sedang menunggu thread lain, dan seterusnya. Jika rangkaian kunci kembali ke thread pertama, maka semua thread akan diam menunggu satu sama lain dan tidak akan pernah jalan. Kasus ini dinamakan kunci mati (deadlock).Jika program yang kita buat tiba-tiba mengalamai deadlock, kita akan segera tahu dan memperbaikinya. Akan tetapi permasalahan utamanya adalah deadlock sulit untuk dideteksi. Sering kali program yang kita buat tampak baik-baik saja, akan tetapi mungkin menyimpan bahaya laten deadlock, yang suatu saat nanti terjadi ketika program sudah dirilis (bahkan sering kali deadlock ini juga tidak bisa direproduksi sehingga menyulitkan debugging). Mencegah deadlock dengan membuat desain program yang lebih hati-hati sangat penting ketika kita membuat program banyak thread.
Mari kita lihat contoh klasik dari deadlock yang ditemukan oleh Dijkstra, yaitu "ilmuwan yang sedang makan". Misalnya ada 5 orang ilmuwan (kita bisa mengganti berapa saja). Ilmuwan-ilmuwan ini menghabiskan sebagian waktu untuk berfikir dan sebagian lagi untuk makan. Ketika mereka berfikir, mereka tidak membutuhkan apa-apa, akan tetapi ketika mereka makan, mereka duduk di meja dengan jumlah alat makan yang terbatas. Mereka membutuhkan dua garpu untuk mengambil spaghetti dari mangkok di tengah meja.
Kesulitannya adalah karena ilmuwan tidak punya uang, mereka tidak mampu untuk membeli banyak garpu. Hanya ada 5 garpu yang tersedia. Garpu-garpu ini diletakkan di meja tersebar di dekat masing-masing ilmuwan ini. Ketika ilmuwan ingin makan, dia harus mengambil garpu di sebelah kiri dan kanannya. Jika ilmuwan di sebelahnya sedang menggunakan garpu tersebut, maka ia harus menunggu hingga garpunya selesai digunakan.
Persoalan ini menjadi menarik karena menjelaskan bahwa program yang sepertinya berjalan dengan benar akan tetapi mudah terkena deadlock. Kita bisa mengganti beberapa konstanta sehingga deadlock bisa lebih cepat terjadi, atau bisa dicegah sama sekali. Parameter-parameter yang bisa diganti adalah konstanta bertipe
final static int
di awal deklarasi kelas IlmuwanMakan
. Jika kita menggunakan banyak ilmuwan dan waktu berfikir yang lama, deadlock akan lebih jarang terjadi.package com.lyracc.ilmuwanmakan; import java.util.*; class Garpu { private static int hitung = 0; private int nomor = hitung++; public String toString() { return "garpu " + nomor; } } // akhir kelas Garpu class Ilmuwan extends Thread { private static Random acak = new Random(); private static int hitung = 0; private int nomor = hitung++; private Garpu garpuKiri; private Garpu garpuKanan; static int waktuFikirMaks = IlmuwanMakan.WAKTU_FIKIR_MAKS; public Ilmuwan(Garpu kiri, Garpu kanan) { garpuKiri = kiri; garpuKanan = kanan; start(); } // Ilmuwan berfikir, gunakan sleep untuk mensimulasi public void berfikir() { System.out.println(this + " berfikir"); try { sleep(acak.nextInt(waktuFikirMaks)); } catch (InterruptedException e) { throw new RuntimeException(e); } } // Ilmuwan makan public void makan() { // cek apakah garpu kirinya tersedia synchronized (garpuKiri) { System.out.println(this + " punya " + this.garpuKiri + ". Menunggu " + this.garpuKanan); // kemudian cek apakah garpu kanannya tersedia synchronized (garpuKanan) { System.out.println(this + " makan"); } } } public String toString() { return "Ilmuwan " + nomor; } // Metode ketika thread dijalankan // masing-masing ilmuwan akan berfikir kemudian makan // begitu seterusnya public void run() { while (true) { berfikir(); makan(); } } } // akhir kelas ilmuwan // Kelas timeout untuk menghentikan proses setelah // waktu yang ditentukan class Timeout extends Timer { public Timeout(int jeda, final String pesan) { super(true); // Daemon thread schedule(new TimerTask() { public void run() { System.out.println(pesan); System.exit(0); } }, jeda); } } // akhir kelas Timeout // Kelas utama public class IlmuwanMakan { final static int JUMLAH_ILMUWAN = 3; // bisa diganti final static int WAKTU_FIKIR_MAKS = 10; // mili detik, bisa diganti final static boolean DEADLOCK = true; // ubah ini menjadi false untuk mencegah deadlock final static int WAKTU_TIMEOUT = 10000; // mili detik atau buat 0 jika tidak ingin timeout public static void main(String[] args) { // Buat array ilmuwan sejumlah JUMLAH_ILMUWAN Ilmuwan[] ilmuwan = new Ilmuwan[JUMLAH_ILMUWAN]; // Mula-mula buat 2 garpu Garpu kiri = new Garpu(); Garpu kanan = new Garpu(); // Garpu pertama hanya sebagai penanda // yaitu garpu di kiri ilmuwan pertama Garpu pertama = kiri; int i = 0; // buat masing-masing ilmuwan // yang pertama memiliki garpu kiri dan kanan // ilmuwan kedua duduk di sebelah kanan ilmuwan pertama // sehingga garpu kirinya adalah garpu kanan ilmuwan pertama // buat garpu baru untuk garpu kanannya // demikian seterusnya hingga JUMLAH_ILMUWAN minus 1 while (i < ilmuwan.length - 1) { ilmuwan[i++] = new Ilmuwan(kiri, kanan); kiri = kanan; kanan = new Garpu(); } // Sekarang buat ilmuwan terakhir // Jika kita ingin membuat deadlock (makan menghadap meja) : // - garpu kirinya adalah garpu kanan ilmuwan sebelumnya // - garpu kanannya adalah garpu kiri ilmuwan pertama // // Jika tidak (makan berbalik arah) // - garpu kirinya adalah garpu kiri ilmuwan pertama // - garpu kanannya adalah garpu kanan ilmuwan sebelumnya if (DEADLOCK) ilmuwan[i] = new Ilmuwan(kiri, pertama); else ilmuwan[i] = new Ilmuwan(pertama, kiri); // Keluar dari program setelah jeda waktu selesai if (WAKTU_TIMEOUT > 0) new Timeout(WAKTU_TIMEOUT, "Waktu habis.."); } }
Garpu
dan Ilmuwan
menggunakan penghitung otomatis untuk memberi nomor identifikasi tersendiri untuk setiap objek Garpu
dan Ilmuwan
yang diciptakan. Setiap Ilmuwan
diberi referensi ke garpu kiri dan garpu kanan. Garpu-garpu ini akan diambil oleh ilmuwan ketika hendak makan.Variabel statik
waktuFikirMaks
adalah waktu maksimum yang digunakan oleh ilmuwan untuk berfikir. Jika nilainya tidak nol, maka nilai variabel ini akan digunakan sebagai argumen perintah sleep()
dalam kelas Ilmuwan
. Mungkin kita beranggapan dengan mengubah waktu berfikir setiap ilmuwan, mereka tidak akan makan secara bersamaan sehingga kemungkinan terjadinya deadlock menjadi lebih kecil. Padahal sebenarnya tidak demikian.Di dalam metode
makan()
, seorang ilmuwan akan mengambil garpu dengan melakukan sinkronisasi pada garpu tersebut. Jika garpu sedang digunakan oleh ilmuwan lain, maka ilmuwan tersebut akan menunggu hingga garpu selesai digunakan. Mula-mula garpu kiri dahulu yang dicoba untuk diambil, baru kemudian garpu kanan. Setelah digunakan, garpu kanan akan dilepas terlebih dahulu baru kemudian garpu kiri.Dalam metode
run()
serorang ilmuwan makan dan berfikir terus menerus.Ada empat konstanta yang bisa kita ubah-ubah di dalam kelas
IlmuwanMakan
. JUMLAH_ILMUWAN
dan WAKTU_FIKIR_MAKS
adalah banyaknya ilmuwan yang ada dan waktu fikir ilmuwan seperti dijelaskan sebelumnya. Variabel ketiga DEADLOCK
berupa boolean yang bisa berisi true
atau false
. Jika bernilai true
maka program cepat atua lambat pasti akan mengalami deadlock. Untuk menghindari deadlock, kita bisa menggantinya dengan false
. Variabel keempat, yaitu WAKTU_TIMEOUT
digunakan untuk menghentikan semua proses pada waktu tertentu. Pada proses yang sulit atau tidak mungkin deadlock (jika variabel DEADLOCK
false, atau jumlah ilmuwan banyak, atau waktu fikir ilmuwan sangat panjang), maka proses akan berhenti pada waktu time out, seringkali sebelum deadlock terjadi.Setelah array objek
Ilmuwan
dibuat, dua objek Garpu
akan dibuat. Objek pertama, juga disimpan dalam variabel pertama
akan digunakan kemudian. Setiap objek ilmuwan akan diberi garpu kiri dan kanannya, kecuali objek ilmuwan terakhir. Setiap kali, garpu kiri dipindah ke garpu kanan. Bayangkan meja ilmuwan dibuat dalam urutan melingkar berlawanan arah jarum jam. Garpu kiri ilmuwan baru adalah garpu kanan ilmuwan sebelumnya. Sedangkan garpu kanan ilmuwan baru adalah objek garpu baru.Pada versi di mana
DEADLOCK
bernilai true
, garpu kiri ilmuwan terakhir adalah garpu kanan ilmuwan sebelumnya, akan tetapi garpu kanannya adalah garpu pertama, karena semua ilmuwan duduk pada posisi melingkar. Dengan pengaturan seperti ini, mungkin saja pada suatu waktu semua ilmuwan akan makan dan saling menunggu garpu di sebelahnya, dan ilmuwan sebelahnya menunggu garpu sebelahnya lagi. Dan karena posisi duduknya melingkar, semua saling menunggu satu sama lain.Coba ganti variabelnya dengan beberapa nilai dan amati seberapa cepat deadlock terjadi. Deadlock ditandai dengan semua ilmuwan saling menunggu satu sama lain hingga waktu time out berakhir. (Seperti pada gambar berikut).
Untuk memecahkan masalah ini, kita harus mengerti bahwa deadlock bisa terjadi jika keempat kondisi berikut ini terjadi pada saat yang sama :
- Saling melarang (mutual exclusion): Paling sedikit salah satu sumber daya yang digunakan objek tidak boleh digunakan bersama. Dalam hal ini, satu garpu bisa digunakan oleh dua orang ilmuwan
- Paling sedikit salah satu proses sedang memegang suatu sumber daya, dan di saat yang sama menunggu sumber daya lain yang dipegang oleh proses lain. Dalam hal ini, agar deadlock terjadi, seorang ilmuwan pasti sedang memegang satu garpu dan menunggu garpu lain yang dipegang oleh ilmuwan lain.
- Suatu sumber daya tidak bisa diambil secara paksa. Proses hanya bisa melepas sumber daya dalam kondisi normal. Ilmuwan-ilmuwan kita adalah orang yang beradab, sehingga tidak bisa merebut garpu yang sedang dipegang oleh ilmuwan lain.
- Lingkaran menunggu sedang terjadi, di mana proses pertama sedang menunggu satu sumber daya yang dipegang oleh proses kedua, yang juga sedang menunggu sumber daya yang dipegang oleh proses ketiga, dan seterusnya hingga proses terakhir menunggu sumber daya yang dipegang oleh proses pertama, sehingga semua proses saling menunggu satu sama lain. Pada contoh ini, lingkaran menunggu terjadi karena semua ilmuwan mengambil garpu kiri terlebih dahulu baru kemudian garpu kanan. Kita bisa memecahkan deadlock dengan membalik garpu kiri dan garpu kanan pada ilmuwan terakhir, sehingga ilmuwan terakhir akan mengambil garpu kanan terlebih dahulu, baru kemudian garpu kiri.
Kesimpulannya, Java tidak menyediakan bantuan secara alami untuk mencegah deadlock: Anda harus menghindarinya sendiri dengan membuat program multi threading dengan lebih hati-hati.
Sisipan | Ukuran |
---|---|
ilmuwanmakan.zip | 2.14 KB |
Menghentikan Thread
Salah satu perubahan pada Java 2 untuk mengurangi kemungkinan terjadinya deadlock adalah dengan dideprekasi (artinya pengembangannya dihentikan, dan user disarankan untuk menghindari penggunaannya) metodestop()
, suspend()
, dan resume()
pada kelas Thread
.Alasan mengapa metode
stop()
dideprekasi adalah karena metode ini tidak melepas kunci yang sudah dimilikinya, dan jika objek tersebut berada dalam kondisi "cacat" seperti ini, thread lain bisa melihat dan mengubah objek cacat ini. Hasilnya akan muncul masalah yang tersembunyi yang akan sangat sulit dideteksi.Java menyediakan cara lain untuk menghentikan thread, yaitu dengan mengeset suatu variabel untuk memberi tahu thread tersebut agar menghentikan dirinya sendiri yaitu dengan keluar dari metode
run()
-nya. Variabel ini akan dicek pada metode run()
yang jika bernilai true, maka metode run()
akan berhenti. Berikut ini adalah contohnya :package com.lyracc.hentikanthread; import java.util.*; class Berhenti extends Thread { // Harus bertipe volatile: private volatile boolean stop = false; private int hitung = 0; public void run() { // Jika stop masih bernilai false teruskan cetak angka // Jika stop bernilai true, blok ini tidak lagi dijalankan while (!stop && hitung < 10000) { System.out.println(hitung++); } // Jika stop berubah menjadi true if (stop) System.out.println("Permintaan stop dideteksi"); } public void requestStop() { stop = true; } } public class HentikanThread { /** * @param args */ public static void main(String[] args) { final Berhenti threadBaru = new Berhenti(); threadBaru.start(); new Timer(true).schedule(new TimerTask() { public void run() { System.out.println("Permintaan berhenti"); threadBaru.requestStop(); } }, 500); // run() setelah 500 mili detik } }
stop
harus bertipe volatile
sehingga metode run()
pasti bisa melihat variabel ini (jika tidak, maka nilainya bisa saja di-cache). Tugas thread ini adalah mencetak 10000 angka, yang akan berhenti ketika hitung >= 10000
atau objek lain meminta berhenti dengan memanggil requestStop()
. Perhatikan bahwa requestStop()
tidak synchronized
karena stop
bertipe boolean
dan volatile
(mengubah boolean menjadi [code]true
adalah operasi atomis yang tidak bisa dihentikan di tengah jalan, karena dilakukan dalam 1 clock).Pada
main()
, objek Berhenti
dimulai. Pada saat yang sama, Timer
dimulai untuk memanggil requestStop()
setelah setengah detik (500 mili detik). Konstruktor Timer
diisi true
untuk memastikan bahwa program berhenti saat itu juga.Menginterupsi Thread yang Diblok
Kadang-kadang, ketika thread dalam keadaan diblok (misalnya ketika sedang menunggu input), thread tersebut tidak bisa membaca variabel seperti kita lakukan di atas. Di sini, kita bisa menggunakan metode
interrupt()
pada kelas Thread
untuk mengeluarkannya dari kondisi diblok. Misalnya,package com.lyracc.interupsi; import java.util.*; class ThreadDiblok extends Thread { public ThreadDiblok() { System.out.println("Memulai blokade"); start(); } public void run() { try { synchronized (this) { wait(); // Memblok selamanya } } catch (InterruptedException e) { System.out.println("Diinterupsi"); } System.out.println("Keluar dari run()"); } } public class Interupsi { static ThreadDiblok threadDiBlok = new ThreadDiblok(); /** * @param args */ public static void main(String[] args) { new Timer(true).schedule(new TimerTask() { public void run() { System.out.println("Bersiap-siap untuk interupsi"); threadDiBlok.interrupt(); threadDiBlok = null; // buat null untuk diambil oleh pemulung memori } }, 2000); // run() setelah 2 detik } }
wait()
di dalam ThreadDiBlok.run()
akan memblok thread selamanya. Ketika Timer
selesai, objek akan melakukan interupsi dengan memanggil interrupt()
. Kemudian objek threadDiBlok
diset ke null
sehingga bisa diambil oleh pemulung memori untuk dibersihkan.Sisipan | Ukuran |
---|---|
hentikanthread.zip | 1.3 KB |
interupsi.zip | 1.19 KB |
0 komentar:
Posting Komentar