Sunday, March 25, 2018

11. Pemrograman Multithread Bagian Tiga



Untuk memperbaiki program tersebut, Anda perlu menserialkan akses terhadap panggil(). Jadi, Anda perlu membatasi aksesnya hanya satu thread pada satu waktu. Untuk melakukannya, Anda hanya perlu mengawali definisi panggil() dengan katakunci synchronized, seperti ditunjukkan di sini:

class PanggilAku {
   synchronized void panggil(String psn) {
   

Ini akan mencegah thread lain memasuki panggil() saat sebuah thread sedang menggunakannya. Setelah synchronized ditambahkan pada definisi panggil(), keluaran program menjadi seperti berikut:

[Halo]
[Sinkronisasi]
[Dunia]


Statemen Sinkronisasi
Meskipun menciptakan metode synchronized di dalam kelas yang Anda ciptakan merupakan cara mudah dalam melakukan sinkronisasi, hal itu tidak berlaku untuk semua kasus. Untuk memahaminya, perhatikan penjelasan berikut. Bayangkan bahwa Anda ingin mensinkronkan akses terhadap objek-objek dari sebuah kelas yang tidak dirancang untuk akses multithread. Kelas tersebut tidak menggunakan metode synchronized. Selain itu, kelas itu tidak diciptakan oleh Anda, tetapi oleh pihak ketiga, dan Anda tidak memiliki akses terhadap kode sumber.

Jadi, Anda tidak bisa menambahkan katakunci synchronized pada metode di dalam kelas tersebut. Bagaimana Anda bisa mengakses dan mensinkronkan sebuah objek dari kelas ini? Untunglah, solusi untuk permasalahan ini cukup mudah. Anda hanya perlu menempatkan pemanggilan terhadap metode-metode yang didefinisikan oleh kelas tersebut di dalam sebuah blok synchronized.

Ini merupakan bentuk umum dari statemen synchronized:

synchronized(refObjek) {
   // statemen-statemen yang disinkronkan
}

Di sini, refObjek adalah sebuah referensi ke objek yang sedang disinkronkan. Blok sinkronisasi memastikan bahwa pemanggilan terhadap sebuah metode tersinkron yang merupakan sebuah anggota dari kelas refObjek hanya terjadi setelah thread terkini berhasil memasuki monitor dari refObjek.

Berikut adalah versi alternative dari contoh sebelumnya, yang menggunakan sebuah blok sinkronisasi di dalam metode run():

// Program ini menggunakan sebuah blok sinkronisasi.
class PanggilAku {
   void panggil(String msg) {
      System.out.print("[" + msg);
      
      try {
         Thread.sleep(1000);
      } catch (InterruptedException e) {
         System.out.println("Diinterupsi");
      }
 
      System.out.println("]");
   }
} 

class Pemanggil implements Runnable {
   String psn;
   PanggilAku target;
   Thread t;

   public Pemanggil(PanggilAku targ, String s) {
      target = targ;
      psn = s;
      
      t = new Thread(this);
      t.start();
   }

   // mensinkronkan semua pemanggilan terhadap panggil()
   public void run() {
      synchronized(target) { // blok sinkronisasi
         target.panggil(psn);
      }
   }
}

public class Sinkron {
   public static void main(String args[]) {
      PanggilAku target = new PanggilAku();
  
      Pemanggil ob1 = new Pemanggil(target, "Halo");
      Pemanggil ob2 = new Pemanggil(target, "Dunia");
      Pemanggil ob3 = new Pemanggil(target, "Sinkronisasi");
  
      // Menunggu thread-thread berakhir
      try {
         ob1.t.join();
         ob2.t.join();
         ob3.t.join();
      } catch(InterruptedException e) {
         System.out.println("Diinterupsi");
      }
   }
}

Di sini, metode panggil() tidak dimodifikasi dengan synchronized. Tetapi, statemen synchronized dipakai di dalam metode run() dari kelas Pemanggil. Ini menyebabkan keluaran yang sama seperti pada contoh sebelumnya.


Komunikasi Antar Thread
Pada contoh-contoh terdahulu secara tak bersyarat memblok thread-thread lain dari akses asinkron terhadap metode-metode tertentu. Penggunaan monitor implisit dalam objek-objek Java ini sangat tangguh, tetapi Anda perlu memahami bagaimana komunikasi antar thread dilakukan.

Seperti yang telah dijelaskan, multithread mengganti pemrograman loop dengan membagi pekerjaan tersebut menjadi unit-unit logikal diskret. Thread juga menyediakan keuntungan sekunder: membuang mekanisme polling. Polling umumnya diimplementasikan oleh sebuah loop yang dipakai untuk memeriksa kondisi secara berulang. Ketika kondisi bernilai true, aksi tertentu akan dilakukan. Ini membuang waktu CPU. Sebagai contoh, perhatikan permasalahan pengantrian klasik, dimana satu thread menghasilkan data dan thread lain memproses data tersebut. Dimisalkan pula bahwa thread penghasil data harus menunggu sampai thread pemroses selesai dieksekusi sebelum thread penghasil data menghasilkan data selanjutnya. Pada sistem polling, thread pemroses data akan menghasilkan banyak siklus CPU ketika ia menunggu thread penghasil data menghasilkan data. Setelah thread penghasil data melakukan pekerjaannya, polling mulai berjalan, yang menghabiskan banyak siklus CPU untuk menunggu thread pemroses data selesai, dan seterusnya. Jelaslah, situasi ini sangat tidak diinginkan.

Untuk menghindari sistem polling, Java mencantumkan sebuah mekanisme komunikasi antar-proses melalui metode wait(), notify(), dan notifyAll(). Ketiga metode ini diimplementasikan sebagai metode final pada kelas Object, jadi semua kelas bisa memilikinya dan menggunakannya. Ketiga metode ini dapat dipanggil hanya dari dalam sebuah konteks synchronized. Berikut adalah aturan bagaimana menggunakan ketiga metode ini:

  • wait() memberitahu thread pemanggil untuk menyerahkan monitornya dan beristrahat sampai thread lain memasuki monitor yang sama dan memanggil notify() atau notifyAll().
  • notify() membangunkan sebuah thread yang memanggil wait() pada objek yang sama.
  • notifyAll() membangunkan semua thread yang memanggil wait() pada objek yang sama. Salah satu thread akan diberikan akses.

Ketiga metode tersebut dideklarasikan di dalam kelas Object, seperti ditunjukkan di sini:

final void wait() throws InterruptedException
final void notify()
final void notifyAll()

Sebelum mempelajari contoh yang mengimplementasikan komunikasi antar-thread, ada beberapa hal yang perlu diketahui: Meskipun wait() normalnya menunggu sampai notify() atau notifyAll() dipanggil, ada kemungkinan pada kasus yang sangat jarang bahwa thread pemanggil dapat dibangunkan karena pembangunan mendadak. Pada kasus ini, thread pemanggil memulai eksekusi tanpa pemanggilan notify() atau notifyAll(). Karena kemungkinan ini bisa saja terjadi, direkomendasikan bahwa pemanggilan terhadap wait() sebaiknya dilakukan di dalam sebuah loop yang memeriksa kondisi untuk menentukan kapan thread menunggu. Contoh berikut mengilustrasikan konsep ini.

Pelajari contoh berikut yang menggunakan wait() dan notify(). Untuk memulainya, perhatikan contoh berikut yang secara tidak tepat mengimplementasikan sebuah bentuk sederhana dari permasalahan penghasil/pemroses. Program ini memuat empat kelas: Q, antrian yang dicoba untuk disinkronkan; Penghasil, objek thread yang menghasilkan entri-entri antrian; Pemroses, objek thread yang menghasilkan memproses entri-entri antrian; PC, kelas kecil yang menghasilkan kelas Q, Penghasil, dan Pemroses.

// Sebuah implementasi yang tidak tepat
// dari permasalahan penghasil/pemroses.

class Q {
   int n;

   synchronized int ambil() {
      System.out.println("Ambil: " + n);
      return n;
   }

   synchronized void taruh(int n) {
      this.n = n;
      System.out.println("Taruh: " + n);
  }
}

class Penghasil implements Runnable {
   Q q;

   Penghasil(Q q) {
      this.q = q;
      new Thread(this, "Penghasil").start();
   }

   public void run() {
      int i = 0;

      while(true) {
         q.taruh(i++);
      }
   }
}

class Pemroses implements Runnable {
   Q q;
   
   Pemroses(Q q) {
      this.q = q;
      new Thread(this, "Pemroses").start();
   }

   public void run() {
      while(true) {
         q.ambil();
      }
   }
}

public class PC {
   public static void main(String args[]) {
      Q q = new Q();
      new Penghasil(q);
      new Pemroses(q);
  
      System.out.println("Tekan Control-C untuk berhenti.");
   }
}

Meskipun metode taruh() dan ambil() pada Q disinkronkan, tidak ada yang menghentikan pemroses yang menguasai penghasil atau sebaliknya. Jadi, berikut adalah salah satu contoh keluaran program tersebut:

Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624
Ambil: 42624

Cara terbaik dalam menuliskan program ini dalam Java adalah dengan menggunakan wait() dan notify(), seperti ditunjukkan berikut:

// Sebuah implementasi tepat untuk permasalahan penghasil/pemroses.

class Q {
   int n;
   boolean nilaiKondisi = false;

   synchronized int ambil() {
      while(!nilaiKondisi)
         try {
            wait();
         } catch(InterruptedException e) {
            System.out.println("Eksepsi InterruptedException ditangkap");
         }

      System.out.println("Ambil: " + n);
      nilaiKondisi = false;
      notify();
      return n;
   }

   synchronized void taruh(int n) {
      while(nilaiKondisi)
         try {
            wait();
         } catch(InterruptedException e) {
            System.out.println("Eksepsi InterruptedException ditangkap");
         }
  
      this.n = n;
      nilaiKondisi = true;

      System.out.println("Taruh: " + n);
      notify();
   }
}

class Penghasil implements Runnable {
   Q q;
   
   Penghasil(Q q) {
      this.q = q;
      new Thread(this, "Penghasil").start();
   }
 
   public void run() {
      int i = 0;
  
      while(true) {
         q.taruh(i++);
      }
   }
}
  
class Pemroses implements Runnable {
   Q q;
  
   Pemroses(Q q) {
      this.q = q;
      new Thread(this, "Pemroses").start();
   }
  
   public void run() {
      while(true) {
         q.ambil();
      }
   }
}

public class PCTepat {
   public static void main(String args[]) {
      Q q = new Q();
      new Penghasil(q);
      new Pemroses(q);
  
      System.out.println("Tekan Control-C untuk berhenti.");
   }
}

Program ini menghasilkan keluaran contoh sebagai berikut:

Ambil: 73897
Taruh: 73898
Ambil: 73898
Taruh: 73899
Ambil: 73899
Taruh: 73900
Ambil: 73900
Taruh: 73901
Ambil: 73901
Taruh: 73902
Ambil: 73902

Di dalam ambil(), wait() dipanggil. Ini menyebabkan eksekusinya tertunda sampai Penghasil memberitahu bahwa data telah siap. Ketika ini terjadi, eksekusi di dalam ambil() akan dimulai. Setelah data diperoleh, ambil() memanggil notify(). Ini memberitahu Penghasil bahwa sekarang saatnya untuk menempatkan data lain pada antrian. Di dalam taruh(), wait() menunda eksekusi sampai Pemroses menghapus item dari antrian. Ketika eksekusi dimulai kembali, item selanjutnya ditempatkan pada antrian dan notify() dipanggil. Ini memberitahu Pemroses bahwa sekarang seharusnya ia menghapusnya.


Deadlock
Jenis error yang perlu dihindari terkait dengan multitasking adalah deadlock, yang terjadi ketika dua thread memiliki ketergantungan sirkular pada sepasang objek sinkron. Sebagai contoh, dimisalkan bahwa satu thread memasuki monitor pada objek X dan thread lain memasuki monitor pada objek Y. Jika thread pada X mencoba untuk memanggil sembarang metode sinkron pada Y, hal itu akan diblok. Tetapi, jika thread pada Y, pada gilirannya, mencoba memanggil sembarang metode sinkron pada X, thread itu akan menunggu selamanya, karena untuk mengakses X, thread itu harus membebaskan pengunciannya pada Y sehingga thread pertama dapat menuntaskan eksekusinya. Deadlock merupakan error yang sulit untuk didebug karena dua alasan:

  • Umumnya, deadlock jarang terjadi, ketika dua thread beririsan satu sama lain.
  • Deadlock melibatkan lebih dari dua thread dan dua objek sinkron.

Untuk memahami deadlock, Anda akan melihat contoh berikut, yang menciptakan dua kelas, A dan B, dengan metode metodeA() dan metodeB(). Kedua metode ini ditunda sebelum mencoba melakukan pemanggilan terhadap sebuah metode pada kelas lain. Kelas utama, Deadlock, menciptakan sebuah objek A dan sebuah objek B, dan kemudian memulai sebuah thread kedua untuk menciptakan kondisi deadlock. Metode metodeA() dan metodeB() menggunakan sleep() sebagai cara untuk memaksa deadlock terjadi.

// Sebuah contoh deadlock.

class A {
   synchronized void metodeA(B b) {
      String nama = Thread.currentThread().getName();
      System.out.println(nama + " memasuki A.metodeA");

      try {
         Thread.sleep(1000);
      } catch(Exception e) {
         System.out.println("A diinterupsi");
      }

      System.out.println(nama + " mencoba memanggil B.akhir()");
         b.akhir();
   }

   synchronized void akhir() {
      System.out.println("Di dalam A.akhir");
   }
}

class B {
   synchronized void metodeB(A a) {
      String nama = Thread.currentThread().getName();
      System.out.println(nama + " memasuki B.metodeB");

      try {
         Thread.sleep(1000);
      } catch(Exception e) {
         System.out.println("B diinterupsi");
      }

      System.out.println(nama + " mencoba memanggil A.akhir()");
         a.akhir();
   }

   synchronized void akhir() {
      System.out.println("Di dalam A.akhir");
   }
}

public class Deadlock implements Runnable{
   A a = new A();
   B b = new B();
   
   Deadlock() {
      Thread.currentThread().setName("Thread Utama");
      Thread t = new Thread(this, "Thread Berkejaran");
 
      t.start();
      a.metodeA(b); // menciptakan deadlock pada thread ini.
 
      System.out.println("Kembali ke thread utama");
   }
 
   public void run() {
      b.metodeB(a); // menciptakan deadlock pada thread ini.
      System.out.println("Kembali di dalam thread lain");
   }
 
   public static void main(String args[]) {
      new Deadlock();
   }
}

Jika dijalankan, program ini menghasilkan keluaran berikut:
Thread Berkejaran memasuki B.metodeB
Thread Utama memasuki A.metodeA
Thread Utama mencoba memanggil B.akhir()
Thread Berkejaran mencoba memanggil A.akhir()





No comments: