makalah java
TRANSCRIPT
MAKALAH
PEMROGRAMAN JAVA THREADPEMROGRAMAN JAVA THREAD
Oleh:
Reni fatimah
1001081025
TK 2 a
POLITEKNIK NEGERI PADANG
2012
PEMROGRAMAN JAVA THREADPEMROGRAMAN JAVA THREAD
1.1. PengertianPengertian
Thread merupakan kemampuan yang disediakan oleh Java untuk membuat aplikasiThread merupakan kemampuan yang disediakan oleh Java untuk membuat aplikasi
yang tangguh, karena thread dalam program memiliki fungsi dan tugas tersendiri.yang tangguh, karena thread dalam program memiliki fungsi dan tugas tersendiri.
Dengan adanya thread, dapat membuat program yang lebih efisien dalam halDengan adanya thread, dapat membuat program yang lebih efisien dalam hal
kecepatan maupun penggunaan sumber daya, karena kita dapat membagi proseskecepatan maupun penggunaan sumber daya, karena kita dapat membagi proses
dalam aplikasi kita pada waktu yang sama. Thread umumnya digunakan untukdalam aplikasi kita pada waktu yang sama. Thread umumnya digunakan untuk
pemrograman multitasking, networking, yang melibatkan pengaksesan ke sumberpemrograman multitasking, networking, yang melibatkan pengaksesan ke sumber
daya secara konkuren. Ada dua cara yang bisa digunakan dalam membuat sebuahdaya secara konkuren. Ada dua cara yang bisa digunakan dalam membuat sebuah
thread, yaitu :thread, yaitu :
•• Membuat subclass dari threadMembuat subclass dari thread
Untuk menjalankan thread, dapat dilakukan dengan memanggil method start().Untuk menjalankan thread, dapat dilakukan dengan memanggil method start().
Saat start() dijalankan, maka sebenarnya method run() dari class akanSaat start() dijalankan, maka sebenarnya method run() dari class akan
dijalankan. Jadi untuk membuat thread, harus mendefinisikan method run()dijalankan. Jadi untuk membuat thread, harus mendefinisikan method run()
pada definisi class. Konstruktor dari cara ini adalah :pada definisi class. Konstruktor dari cara ini adalah :
ClassThread namavar = new ClassThread(); Namavar.start();ClassThread namavar = new ClassThread(); Namavar.start();
Atau dapat juga langsung dengan cara:Atau dapat juga langsung dengan cara:
New ClassThread().start();New ClassThread().start();
•• Mengimplementasikan interface RunnableMengimplementasikan interface Runnable
Cara ini merupakan cara yang paling sederhana dalam membuat thread.Cara ini merupakan cara yang paling sederhana dalam membuat thread.
Runnable merupakan unit abstrak, yaitu kelas yang mengimplementasikanRunnable merupakan unit abstrak, yaitu kelas yang mengimplementasikan
interface ini hanya cukup mengimplementasikan fungsi run(). Dalaminterface ini hanya cukup mengimplementasikan fungsi run(). Dalam
mengimplementasi fungsi run(), kita akan mendefinisikan instruksi yangmengimplementasi fungsi run(), kita akan mendefinisikan instruksi yang
membangun sebuah thread. Konstruktor dari cara ini adalah : membangun sebuah thread. Konstruktor dari cara ini adalah :
ObjekRunnable objek = new ObjekRunnable(); ObjekRunnable objek = new ObjekRunnable();
Thread namavar = new Thread(Objek Runnable); Thread namavar = new Thread(Objek Runnable);
Atau dengan cara singkat seperti :Atau dengan cara singkat seperti :
New Thread(new ObjekRunnable());New Thread(new ObjekRunnable());
Daemon Dan User Thread,Daemon Dan User Thread, Ada dua Macam thread dalam Java, yaitu Ada dua Macam thread dalam Java, yaitu
daemon dan user thread. Daemon thread merupakan thread yang siklus hidupnyadaemon dan user thread. Daemon thread merupakan thread yang siklus hidupnya
tergantung pada thread utama atau induk, sehingga apabila thread induk berakhir,tergantung pada thread utama atau induk, sehingga apabila thread induk berakhir,
maka otomatis thread-thread daemon juga ikut berakhir. Sedangkan user threadmaka otomatis thread-thread daemon juga ikut berakhir. Sedangkan user thread
memiliki sifat berbeda, dimana apabila thread utama sudah selesai, maka user threadmemiliki sifat berbeda, dimana apabila thread utama sudah selesai, maka user thread
akan terus dijalankan.akan terus dijalankan.
Sleep,Sleep, Mengatur thread untuk menghentikan prosesnya sejenak dan memberi Mengatur thread untuk menghentikan prosesnya sejenak dan memberi
kesempatan pada thread atau proses lain. Sleep dilakukan dengan cara memanggilkesempatan pada thread atau proses lain. Sleep dilakukan dengan cara memanggil
method : method :
Sleep(long waktu); Waktu untuk method ini merupakan tipe long dalam milisekon. Sleep(long waktu); Waktu untuk method ini merupakan tipe long dalam milisekon.
Interrupt,Interrupt, Apabila menginginkan suatu thread untuk menghentikan proses, Apabila menginginkan suatu thread untuk menghentikan proses,
maka perlu memanggil method interrupt. Interrupt digunakan untuk memberi signalmaka perlu memanggil method interrupt. Interrupt digunakan untuk memberi signal
pada thread untuk menghentikan prosesnya.pada thread untuk menghentikan prosesnya.
Latihan Thread.java Latihan Thread.java
class ThreadBaru extends Thread { class ThreadBaru extends Thread {
public ThreadBaru(String id) {public ThreadBaru(String id) {
super(id); super(id);
start(); //Mulai eksekusi thread barustart(); //Mulai eksekusi thread baru
}}
public void run() { public void run() {
for(int i=0;i<5;i++){ for(int i=0;i<5;i++){
try{try{
Thread.sleep(100);Thread.sleep(100);
}catch(InterruptedException e) {}}catch(InterruptedException e) {}
} }
} }
}}
class DemoThread {class DemoThread {
public static void main(String[] args) {public static void main(String[] args) {
ThreadBaru thread1 = new ThreadBaru("Thread1"); ThreadBaru thread2 = newThreadBaru thread1 = new ThreadBaru("Thread1"); ThreadBaru thread2 = new
ThreadBaru("Thread2"); ThreadBaru thread3 = new ThreadBaru("Thread3");ThreadBaru("Thread2"); ThreadBaru thread3 = new ThreadBaru("Thread3");
System.out.println("Thread1 masih dieksekusi : " + thread1.isAlive());System.out.println("Thread1 masih dieksekusi : " + thread1.isAlive());
System.out.println("Thread2 masih dieksekusi : " + thread2.isAlive());System.out.println("Thread2 masih dieksekusi : " + thread2.isAlive());
System.out.println("Thread3 masih dieksekusi : " + thread3.isAlive()); System.out.println("Thread3 masih dieksekusi : " + thread3.isAlive()); //tunggu//tunggu
hingga semua child thread selesai dieksekusi hingga semua child thread selesai dieksekusi
try{try{
thread1.join(); thread1.join();
System.out.println("Thread1 selesai dieksekusi"); System.out.println("Thread1 selesai dieksekusi");
thread2.join();thread2.join();
System.out.println("Thread2 selesai dieksekusi"); System.out.println("Thread2 selesai dieksekusi");
thread3.join();thread3.join();
System.out.println("Thread3 selesai dieksekusi"); System.out.println("Thread3 selesai dieksekusi");
}catch(InterruptedException e) {}catch(InterruptedException e) {
System.out.println("Thread utama diinterupsi " + e);System.out.println("Thread utama diinterupsi " + e);
}}
System.out.println("Thread utama selesai dieksekusi"); System.out.println("Thread utama selesai dieksekusi");
} }
}}
SynchronizedSynchronized
Sinkronisasi adalah method atau blok yang memiliki tambahan keywordSinkronisasi adalah method atau blok yang memiliki tambahan keyword
synchronized, sehingga apabila dijalankan maka hanya satu thread pada suatu waktusynchronized, sehingga apabila dijalankan maka hanya satu thread pada suatu waktu
yang dapat menjalankan method atau blok program. Thread lain akan menungguyang dapat menjalankan method atau blok program. Thread lain akan menunggu
thread yang sedang mengeksekusi method ini hingga selesai. Mekanisme sinkronisasithread yang sedang mengeksekusi method ini hingga selesai. Mekanisme sinkronisasi
penting apabila terjadi pembagian sumber daya maupun data di antara thread-thread.penting apabila terjadi pembagian sumber daya maupun data di antara thread-thread.
Sinkronisasi juga melakukan penguncian pada sumber daya atau data yang sedangSinkronisasi juga melakukan penguncian pada sumber daya atau data yang sedang
diproses.diproses.
Latihan ThreadSinkronisasi.java Latihan ThreadSinkronisasi.java
class TestSinkronisasi {class TestSinkronisasi {
private java.util.Random random = new java.util.Random(); private java.util.Random random = new java.util.Random();
public void callMe(String data) { public void callMe(String data) {
System.out.print("[");System.out.print("[");
try{ try{
Thread.sleep(random.nextInt(200)); Thread.sleep(random.nextInt(200));
}catch(InterruptedException e) {}catch(InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
}}
System.out.print(data);System.out.print(data);
try{try{
Thread.sleep(random.nextInt(200));Thread.sleep(random.nextInt(200));
}catch(InterruptedException e) {}catch(InterruptedException e) {
e.printStackTrace(); e.printStackTrace();
} System.out.println("]");} System.out.println("]");
}}
}}
class ThreadBaru extends Thread {class ThreadBaru extends Thread {
private String data;private String data;
private TestSinkronisasi obj;private TestSinkronisasi obj;
public ThreadBaru(TestSinkronisasi obj,String data) {public ThreadBaru(TestSinkronisasi obj,String data) {
this.obj = obj; this.obj = obj;
this.data = data;this.data = data;
start(); start();
} public void run() { } public void run() {
obj.callMe(data); obj.callMe(data);
}}
}}
class DemoThread {class DemoThread {
public static void main(String[] args) {public static void main(String[] args) {
TestSinkronisasi obj = new TestSinkronisasi(); TestSinkronisasi obj = new TestSinkronisasi();
ThreadBaru thread1 = new ThreadBaru(obj,"Superman");ThreadBaru thread1 = new ThreadBaru(obj,"Superman");
ThreadBaru thread2 = new ThreadBaru(obj,"Batman"); ThreadBaru thread2 = new ThreadBaru(obj,"Batman");
ThreadBaru thread3 = new ThreadBaru(obj,"Spiderman");ThreadBaru thread3 = new ThreadBaru(obj,"Spiderman");
//tunggu hingga semua child thread selesai dieksekusi //tunggu hingga semua child thread selesai dieksekusi
try{ thread1.join();try{ thread1.join();
thread2.join();thread2.join();
thread3.join();thread3.join();
}catch(InterruptedException e) {}catch(InterruptedException e) {
System.out.println("Thread utama diinterupsi " + e);System.out.println("Thread utama diinterupsi " + e);
}}
}}
}}
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.
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.
CONTOH LAIN PROGRAM JAVA THREAD
Program Countdown Menggunakan Thread Pada Java
public class CountDown implements Runnable{private static int startCount = 11;private int countDown = --startCount;private static int threadCount = 0;private int threadNumber = ++threadCount;public void run() { System.out.println("Waktu tinggal "+countDown+" detik ( Thread nomer : "+threadNumber+" )");if (countDown == 1) {
System.out.println("\nWaktu Habis ~ !!"); } }
private static void doThreadCountdown() throws java.lang.InterruptedException{for (int i = 0; i < 10; i++){Thread.sleep(1000);Runnable ot = new CountDown();Thread th = new Thread(ot);th.start(); }}
public static void main(String[] args) throws java.lang.InterruptedException{ System.out.println("\nMenghitung mundur dalam 10 detik ...\n"); doThreadCountdown(); } }
Penjelasan : Pada program ini menghasilkan 10 buah thread dengan menggunakan
looping sebanyak 10 kali dan setiap thread yang tercipta dimanfaatkan untuk melakukan
hitung mundur/countdown.
REFERENSI
Ady Wicaksono, Dasar – Dasar Pemrograman Java 2, Penerbit PT Elex Media
Komputindo, Jakarta, 2002.
Benny Hermawan, Menguasai JAVA 2 Object Oriented Programming, Penerbit ANDI
Yogyakarta, Yogyakarta, 2004.
Ginanjar Utama, Berfikir Objek:Cara Efektif Menguasai Java, 2003,
http://ilmukomputer.com/berseri/ginanjar-java/index.php (26 Desember 2004).
Indrajani dan Martin, Pemrograman Berorientasi Objek dengan Java, Penerbit PT
Elex Media Komputindo, Jakarta, 2004.
Isak Rickyanto, Dasar Pemrograman Berorientasi Objek dengan Java 2 (JDK1.4),
Penerbit ANDI Yogyakarta, Yogyakarta, 2003.
http://nolimitz.web.id/2010/03/program-countdown-menggunakan-thread-pada-java/
http://java.lyracc.com/belajar/java-untuk-pemula/dasar-dasar-thread