Senin, 03 Maret 2014

Dasar-Dasar Thread

Cara termudah untuk membuat thread adalah membuat kelas turunan dari java.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();
    }
}
Pada contoh program di atas, objek thread diberi nama melalui argumen pada konstruktornya. Nama ini dipanggil ketika metode 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() dengan
    public void run() {
      while (true) {
        System.out.println( getName() + " : " + hitungMundur );
        if (--hitungMundur == 0)
            return;
        yield();
      }
    }
Secara umum, yield mungkin berguna untuk situasi yang agak langka, dan kita tidak bisa menggunakannya secara serius untuk memperbaiki kinerja aplikasi kita.
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);
        }            
      }
    }
Ketika kita memanggil 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);
    }
}
Pada contoh di atas, kita ubah konstruktornya untuk mengeset prioritas kemudian menjalankan thread. Pada metode 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();
    }
 
}
Perintah 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();
    }
 
}
Hasil keluarannya adalah seperti pada gambar berikut.

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();
        }
    }
}
Satu-satunya yang dibutuhkan oleh kelas 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();
    }
}
Jika kita menggunakan 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.

Tidak ada komentar:

Posting Komentar