Kamis, 15 Desember 2011

Berbagi Sumber Daya (Java)

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 memanggilambilNilai(). 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();
    }
}

Pada metode 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 metodeberikut(). 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 mutexatau 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 */ }

Setiap objek memiliki kunci masing-masing yang otomatis dibuat ketka objek tersebut dibuat (kita tidak perlu membuat kode spesial). Ketika kita memanggil metode yang diberi tanda 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 metodeberikut() 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();
    }
}

Bagian Kritis

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
}
Bentuk umum di atas juga disebut blok tersinkron (synchronized block); sebelum blok ini bisa dieksekusi, kunci pada objekobjekKunci harus dicek terlebih dahulu. Jika thread lain telah mengunci ojek ini, maka bagian kritis tidak bisa dimasuki hingga thread lain selesai dan melepas kuncinya.

Tidak ada komentar:

Posting Komentar