Seberapa Jauh Bedanya Program Yang Multithreading?


Satu fitur dari bahasa Java yang sudah ada sejak awal adalah dukungan multi-threading atau concurrency.  Saya masih ingat saat pertama kali mengenal Java, saya sering diberitahu bahwa salah satu kelebihan Java dibanding C++ adalah dukungan concurrency-nya.  Salah satu buktinya adalah Java memiliki keyword synchronized yang dapat dipakai pada method atau blok kode program.  Selain itu, pada Java 5, terdapat Executor Framework untuk melakukan pooling thread yang berguna dalam meningkatkan kinerja concurrency.  Pada Java 7, Fork/Join Framework diperkenalkan untuk memaksimalkan kinerja concurrency pada prosesor multicore.

Concurrency menjadi semakin penting karena saat ini sudah semakin banyak komputer pengguna yang memiliki prosesor multicore.  Produsen prosesor sepertinya mengalami hambatan dalam meningkatkan clock rate CPU.  Sebagai gantinya, mereka menciptakan prosesor yang lebih ‘kencang’ dengan multicore.  Sebagai contoh, seri prosesor Intel Core i5 dan Intel Core i7 memiliki clock rate yang tidak jauh berbeda (maksimal 3.6 GHz), tetapi perbedaan signifikannya terletak pada jumlah core.   Sebagai contoh, Intel Core i5 2500K memiliki 4 core, sementara itu Intel Core i7 2600K yang dijual lebih mahal $101 juga memiliki 4 core tetapi disertai tambahan Intel Hyper-Threading Technology sehingga memiliki 8 virtual core.  Dari sisi clock rate sendiri, Intel Core i7 2600K  3.4 GHz hanya lebih cepat 100 MHz dibanding Intel Core i5 2500K 3.3 GHz.

Pertanyaannya adalah apakah memiliki 8 core selalu jauh lebih cepat dibanding 4 core?  Tidak juga.  Semua kembali lagi ke software yang dijalankan oleh CPU tersebut.  Bila program yang dijalankan hanya memakai 1 thread, maka tidak akan ada penambahan kinerja yang berarti.  Jadi, untuk mendukung prosesor multicore agar bisa bekerja semaksimal mungkin, developer aplikasi desktop harus membuat aplikasi multithreading.

Pada kesempatan ini saya akan mencoba membandingkan perbedaan kinerja antara kode program Java yang single thread dengan aplikasi yang multi thread. Kedua program akan melakukan hal yang sama, yaitu membaca isi dari seluruh tulisan dari blog ini dan menampilkan informasi jumlah kata yang ditemui.  Agar gampang, saya terlebih dahulu menyimpan isi blog ini dalam sebuah file (ukurannya 1.37 MB).

Ini adalah kode program SingleThread.java yang melakukan proses secara single thread:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class SingleThread {
    public void proses() throws IOException {
        List<Hasilpencarian> listHasil = new ArrayList<>();
        List<String> listBaris = Files.readAllLines(
            Paths.get("C:\\data.txt"),
            Charset.defaultCharset());

        for (String baris: listBaris) {
            for (String kata: baris.split(" ")) {
                if (kata.trim().length()==0) break;
                kata = kata.toLowerCase();
                HasilPencarian hasilPencarian = new HasilPencarian(kata, 1);
                int index = listHasil.indexOf(hasilPencarian);
                if (index >= 0) {
                    listHasil.get(index).tambahJumlah(hasilPencarian);
                } else {
                    listHasil.add(hasilPencarian);
                }
            }
        }

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {
            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o2.getJumlah() - o1.getJumlah();
            }
        });

        for (HasilPencarian hasilPencarian: listHasil) {
            System.out.println(hasilPencarian);
        }

    }

    public static void main(String[] args) {
        try {
            new SingleThread().proses();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

Kode program tersebut membutuhkan sebuah class HasilPencarian.java yang isinya adalah:

import java.util.Objects;

public class HasilPencarian {

    private static final long serialVersionUID = 1L;

    private String kata;
    private Integer jumlah;

    public HasilPencarian(String kata, Integer jumlah) {
        this.kata = kata;
        this.jumlah = jumlah;
    }

    public String getKata() {
        return kata;
    }

    public void setKata(String kata) {
        this.kata = kata;
    }

    public Integer getJumlah() {
        return jumlah;
    }

    public void setJumlah(Integer jumlah) {
        this.jumlah = jumlah;
    }

    public void tambahJumlah(Integer jumlah) {
       this.jumlah+=jumlah;
    }

    public void tambahJumlah(HasilPencarian hasilLain) {
        if (!hasilLain.getKata().equals(getKata())) {
            throw new RuntimeException("Tidak dapat menjumlahkan kata yang berbeda!");
        }
        tambahJumlah(hasilLain.getJumlah());
    }

    @Override
    public int hashCode() {
        int hash = 5;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final HasilPencarian other = (HasilPencarian) obj;
        if (!Objects.equals(this.kata, other.kata)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return String.format("%s: %d", getKata(), getJumlah());
    }

}

Kode program SingleThread.java cukup sederhana dan mudah dimengerti.  Ia akan membaca baris dari file, kemudian memecah setiap baris menjadi kata, lalu menghitung jumlah kata yang unik.  Setelah selesai, ia akan mengurutkan berdasarkan jumlah kata terbanyak dan menampilkannya.  Contoh hasil akan terlihat seperti:

yang: 3448
di: 2071
saya: 1783
akan: 1645
dengan: 1513
untuk: 1496
pada: 1357
dan: 1268
adalah: 1057
seperti: 986
...

Terlihat bahwa selama ini kata yang paling sering saya tulis adalah ‘yang’ :)  Ok, tapi bukan itu tujuan saya.  Saya ingin memperlihatkan seperti apa kinerja program ini.  Untuk itu, saya akan melakukan profiling dengan menggunakan NetBeans Profiler.  Untuk itu, saya men-klik icon Profile Project yang terilhat seperti pada gambar berikut ini:

Icon ProfileProject Di NetBeans

Icon ProfileProject Di NetBeans

Saya kemudian memilih CPU, Analyze Performance, dan men-klik tombol Run.  Setelah proses profiling selesai, saya dapat melihat hasil snapshot seperti pada gambar berikut ini:

CPU Sampling Pada Aplikasi Versi Single Thread

CPU Sampling Pada Aplikasi Versi Single Thread

Terlihat bahwa dibutuhkan sekitar 24,6 detik untuk mengerjakan program ini (ini adalah penantian yang cukup lama!).  Waktu paling lama habis dipakai untuk mengerjakan method indexOf() milik ArrayList (karena berada dalam looping).

Saya kembali men-klik icon Profile Project.  Kali ini saya memilih Monitor, memberi tanda centang pada Enable threads monitoring dan Sample threads states.  Setelah men-klik Run dan membiarkan proses profiling selesai, saya dapat melihat daftar threads seperti yang terlihat di gambar berikut ini:

Tampilan Thread Pada Aplikasi Single Thread

Tampilan Thread Pada Aplikasi Single Thread

Disini terlihat bahwa thread pada aplikasi saya hanya satu, yaitu main.  Sisanya adalah thread milik JVM yang selalu ada, misalnya untuk keperluan garbage collector.

Sekarang, saya akan membuat program versi multithread yang memakai Fork/Join Framework dari Java 7.  Kode program ini tidak akan bisa dijalankan pada Java versi sebelumnya.

Saya akan membuat class MultiThread.java yang isinya adalah:

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ForkJoinPool;

public class MultiThread {

    public void proses() throws IOException {

        List<String> listBaris = Files.readAllLines(
            Paths.get("C:\\data.txt"),
            Charset.defaultCharset());

        ForkJoinPool pool = new ForkJoinPool();

        TaskPemisah taskPemisah = new TaskPemisah(listBaris, 0, listBaris.size()-1);
        List<HasilPencarian> listHasil = pool.invoke(taskPemisah);

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {
            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o1.getKata().compareTo(o2.getKata());
            }
        });

        TaskPenghitung taskPenghitung = new TaskPenghitung(listHasil, 0, listHasil.size()-1);
        listHasil = pool.invoke(taskPenghitung);
        pool.shutdown();

        Collections.sort(listHasil, new Comparator<HasilPencarian>() {

            @Override
            public int compare(HasilPencarian o1, HasilPencarian o2) {
                return o2.getJumlah() - o1.getJumlah();
            }

        });

        for (HasilPencarian hasilPencarian: listHasil) {
            System.out.println(hasilPencarian);
        }

    }

    public static void main(String[] args) {
        try {
            new MultiThread().proses();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

}

Kode program MultiThread.java terlihat lebih rumit dibanding SingleThread.java.   Setelah setiap baris dibaca dari file, kode program tersebut akan memakai sebuah RecursiveTask untuk memisahkan setiap baris menjadi kata.  Setelah selesai, hasilnya akan diproses lagi oleh RecursiveTask lain yang menghitung jumlah kata. Karena prosesnya dilakukan secara concurrent, saya perlu mengurutkan kata berdasarkan abjad terlebih dahulu. Tujuannya adalah agar kata yang sama tidak diproses oleh thread yang berbeda (bila hal ini terjadi, hasil perhitungan akan salah!).

RecursiveTask pertama yang bertugas untuk memisahkan setiap baris menjadi kata akan saya beri nama sebagai TaskPemisah, isinya akan terlihat seperti berikut ini:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;

public class TaskPemisah extends RecursiveTask<List<HasilPencarian>> {

    private List<String> listBaris;
    private int indexMulai;
    private int indexSelesai;

    public TaskPemisah(List<String> listBaris, int indexMulai, int indexSelesai) {
        this.listBaris = listBaris;
        this.indexMulai = indexMulai;
        this.indexSelesai = indexSelesai;
    }

    @Override
    protected List<HasilPencarian> compute() {
        List<HasilPencarian> listHasil = new ArrayList<>();
        if ((indexSelesai - indexMulai) < 10) {
            for (int i=indexMulai; i<=indexSelesai; i++) {
                String baris = listBaris.get(i);
                for (String kata: baris.split(" ")) {
                    if (kata.trim().length()==0) break;
                    kata = kata.trim().toLowerCase();
                    HasilPencarian hasilPencarian = new HasilPencarian(kata, 1);
                    listHasil.add(hasilPencarian);
                }
            }
        } else {
            int indexTengah = (indexSelesai + indexMulai) / 2;
            TaskPemisah task1 = new TaskPemisah(listBaris, indexMulai, indexTengah);
            TaskPemisah task2 = new TaskPemisah(listBaris, indexTengah+1, indexSelesai);
            invokeAll(task1, task2);
            try {
                listHasil.addAll(task1.get());
                listHasil.addAll(task2.get());
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }
        return listHasil;
    }

}

RecursiveTask yang akan menghitung jumlah kata akan saya beri nama sebagai TaskPenghitung, isinya akan terlihat seperti berikut ini:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.RecursiveTask;

public class TaskPenghitung extends RecursiveTask<List<HasilPencarian>>{

    private List<HasilPencarian> listProses;
    private int indexMulai;
    private int indexSelesai;

    public TaskPenghitung(List<HasilPencarian> listProses, int indexMulai, int indexSelesai) {
        this.listProses = listProses;
        this.indexMulai = indexMulai;
        this.indexSelesai = indexSelesai;
    }

    private void proses(List<HasilPencarian> listHasil) {
        for (int i=indexMulai; i<=indexSelesai; i++) {
            HasilPencarian hasilPencarian = listProses.get(i);
            int index = listHasil.indexOf(hasilPencarian);
            if (index >= 0) {
                listHasil.get(index).tambahJumlah(hasilPencarian);
            } else {
                listHasil.add(hasilPencarian);
            }
        }
    }

    @Override
    protected List<HasilPencarian> compute() {
        List<HasilPencarian> listHasil = new ArrayList<>();
        if ((indexSelesai - indexMulai) < 1000) {
            proses(listHasil);
        } else {
            int tengah = (indexMulai + indexSelesai) / 2;
            while (true) {
                if (tengah==indexSelesai) {
                    proses(listHasil);
                    return listHasil;
                }
                String kataSekarang = listProses.get(tengah).getKata();
                String kataBerikut = listProses.get(tengah+1).getKata();
                if (!kataSekarang.equals(kataBerikut)) break;
                tengah++;
            }

            TaskPenghitung task1 = new TaskPenghitung(listProses, indexMulai, tengah);
            invokeAll(task1);

            TaskPenghitung task2 = null;
            if (tengah<indexSelesai) {
                task2 = new TaskPenghitung(listProses, tengah+1, indexSelesai);
                invokeAll(task2);
            }

            try {
                listHasil.addAll(task1.get());
                if (task2!=null) {
                    listHasil.addAll(task2.get());
                }
            } catch (InterruptedException | ExecutionException ex) {
                ex.printStackTrace();
            }
        }
        return listHasil;
    }

}

Bila dijalankan, hasilnya akan sama persis seperti pada versi SingleThread.java.  Yang jelas, kode program MultiThread.java lebih panjang, lebih rumit, dan jumlah class-nya tambah banyak.  Apakah kerumitan ini memiliki manfaat?

Saya akan mulai dengan melakukan profiling kinerja.  Hasil CPU sampling dapat dilihat pada gambar berikut ini:

CPU Sampling Pada Aplikasi Versi Multi Thread

CPU Sampling Pada Aplikasi Versi Multi Thread

Terlihat bahwa waktu yang dibutuhkan oleh MultiThread.java hanya 7,1 detik;  bandingkan dengan SingleThread.java yang membutuhkan 24,6 detik.   Peningkatan yang terjadi mencapai 71 %.  Ini adalah sebuah selisih yang cukup jauh.

Untuk membuktikan bahwa ini adalah aplikasi multithreading, saya akan melakukan profiling untuk me-monitor thread, dimana hasilnya akan terlihat seperti pada gambar berikut ini:

Tampilan Thread Pada Aplikasi Multi Thread

Tampilan Thread Pada Aplikasi Multi Thread

Pada gambar di atas, selain terdapat thread untuk main, juga ada thread ForkJoinPool-1-worker-1 dan ForkJoinPool-1-worker-1 (terlihat bahwa kode program belum maksimal karena masih sedikit yang ‘hijau’ secara bersamaan!).  Mereka diciptakan oleh class ForkJoinPool yang dipakai di MultiThread.java.   Secara default, jumlah thread yang diciptakan berdasarkan jumlah core CPU (prosesor) yang terdeteksi (pada prosesor yang memakai Hyper-Threading Technology, yang dipakai adalah jumlah virtual core).   Developer dapat menentukan nilai yang berbeda dengan melewatkan sebuah angka yang mewakili jumlah thread yang akan dibuat (nilai parallelism) di constructor ForkJoinPool.

Perihal Solid Snake
I'm nothing...

4 Responses to Seberapa Jauh Bedanya Program Yang Multithreading?

  1. leesunhyu mengatakan:

    ada contoh yg lebih simple nggak?
    kalau multithreading apa nggak pake extends thread atau implements runable gt ya?

  2. Ping-balik: Memakai Fork/Join Framework Di Groovy | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: