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.Sumber : http://java.lyracc.com/belajar/java-untuk-pemula/berbagi-sumber-daya
Tidak ada komentar:
Posting Komentar