Multithreading Di Android

Pada program yang saya buat di artikel Belajar Memakai Implicit Intent Di Android, sistem operasi Android akan terlihat seperti hang bila saya membaca file dengan ukuran besar. Tombol yang ada di perangkat keras juga tidak bisa dipakai, misalnya saya tidak membatalkan proses dan tidak bisa kembali ke menu utama. Mengapa demikian? Hal ini berkaitan dengan threading (concurrency). Setiap aplikasi Android dimulai dengan sebuah thread tunggal yang diberi nama ‘main’. Semua operasi yang berkaitan dengan UI seperti event ditangani oleh thread ini. Pada Swing, thread ini memiliki fungsi yang sama seperti event dispatch thread (EDT). Bedanya, di Swing, aplikasi tidak langsung mulai dari EDT melainkan thread non-UI (pengguna memakai SwingUtilities.invokeLater() atau SwingUtilities.invokeAndWait() untuk mengerjakan kode program di EDT). Bila saya mengerjakan sebuah proses yang sangat lama di thread UI, maka ia tidak bisa bekerja menangani event UI. Ia akan menunggu hingga proses lama ini selesai baru bisa lanjut bekerja. Ini yang membuat aplikasi menjadi tidak responsif.

Bila pada aplikasi Java, saya menggunakan JVisualVM (bawaan JDK) untuk memantau thread yang ada di sebuah aplikasi, maka untuk Android, saya dapat menggunakan Android Device Monitor (bawaan Android SDK). Sebagai contoh, bila saya memilih aplikasi dan men-klik icon Update Threads, saya dapat melihat tampilan seperti pada gambar berikut ini di tab Threads:

Melihat thread di aplikasi Android

Melihat thread di aplikasi Android

Walaupun ada banyak thread yang dibuat, satu-satunya thread yang menjalankan kode program aplikasi saya saat ini adalah thread 1 dengan nama ‘main’. Thread lainnya adalah bawaan Android, misalnya thread ‘GC’ untuk membebaskan memori yang tidak dipakai lagi, thread ‘JDWP’ untuk keperluan debugging, dan sebagainya.

Sekarang, saya akan melakukan perubahan pada kode program di method tampilkan() agar ia mengerjakan tugasnya di thread terpisah:

private void tampilkan(Intent data) {
    Thread.start('ProsesFile') {
        TextView output = (TextView) findViewById(OUTPUT_VIEW_ID)
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i
        for (i = 0; i < bytes.length; i++) {
            hexdump.append(String.format("%02x", bytes[i]))
            hexdump.append(' ')
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }

            output.post({
                output.setText("Memproses byte $i dari ${bytes.length}")
            } as Runnable)

            Thread.sleep(1)
        }
        if (ascii) {
            (1..(8 - ((i - 1) % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            output.setText(hexdump.toString())
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hexdump.toString())
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)

    }
}   

Pada kode program di atas, saya memakai Thread.start() dari Groovy untuk membuat sebuah thread baru dengan nama ‘ProsesFile’. Selama berada di dalam thread baru ini, saya tidak boleh mengerjakan method yang berhubungan dengan UI secara langsung. Hal ini karena method pada UI tidak thread-safe. Oleh sebab itu, untuk menjadwalkan kode program agar dikerjakan oleh thread ‘Main’ (yang menangani UI), saya menggunakan post().

Bila saya menjalankan aplikasi, ia akan lebih responsif. Saya juga akan menemukan sebuah thread baru dengan nama ‘ProsesFile’ bila memantau aplikasi melalui Android Device Monitor, seperti yang terlihat pada gambar berikut ini:

Membuat thread baru

Membuat thread baru

Masalah lain yang saya hadapi adalah penggunaan memori yang besar karena memproses isi file sekaligus. Oleh sebab itu, saya akan menggunakan seek bar sehingga saya bisa menampilkan isi file per kilobyte sesuai dengan posisi seek bar. Agar lebih mudah dalam merancang UI, saya akan menggunakan designer bawaan Android Studio. Untuk itu, saya membuat folder baru bernama layout di lokasi src/main/res. Setelah itu, saya men-klik kanan pada folder layout dan memilih New, Layout Reource Folder. Saya mengisi dialog yang muncul dengan mainscreen pada File name dan RelativeLayout pada Root element. Saya kemudian merancang UI sehingga terlihat seperti berikut ini:

Merancang UI secara visual

Merancang UI secara visual

Saya perlu mengubah method onCreate() untuk membaca layout dari XML sehingga isinya menjadi seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.mainscreen)
    ((SeekBar) findViewById(R.id.posisi)).onSeekBarChangeListener = this
    if (intent.data) {
        tampilkan(intent)
    }
  }

  ...

  @Override
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @Override
  void onStartTrackingTouch(SeekBar seekBar) {}

  @Override
  void onStopTrackingTouch(SeekBar seekBar) {}

}

Kali ini saya perlu men-share variabel bytes yang berisi data lengkap yang sudah dibaca agar dapat diakses dari thread berbeda. Karena ia bytes hanya ditulis sekali, setelah itu dibaca oleh beberapa thread berbeda, saya boleh saja men-share variabel ini dengan cara yang tidak thread-safe. Akan tetapi, agar lebih aman, saya akan menggunakan ReadWriteLock sehingga pada saat variabel ini ditulis oleh sebuah thread, maka thread lain yang ingin membaca harus menunggu sampai penulisan selesai. Groovy menyediakan AST transformation @WithWriteLock dan @WithReadLock untuk keperluan tersebut. Sebagai contoh, saya bisa menambahkannya seperti:

public class MainActivity extends Activity implements SeekBar.OnSeekBarChangeListener {

  private byte[] bytes

  ...

  @Override @WithReadLock
  void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}

  @WithWriteLock
  private void tampilkan(Intent data) { ... }

}

Sekarang, fungsi dari kode program tampilkan() hanya untuk membaca isi file saja:

@WithWriteLock
private void tampilkan(Intent data) {
    Thread.start('BacaFile') {
        Uri uri = data.getData()
        byte[] bytes = getContentResolver().openInputStream(uri).bytes
        SeekBar seekBar = (SeekBar) findViewById(R.id.posisi)
        seekBar.post({
            seekBar.max = bytes.length / 1024
        })
    }
}

Kode program di onProgressChanged akan dikerjakan setiap kali pengguna menggeser seek bar. Karena proses untuk menghasilkan hexdump cukup lama, maka saya akan mengerjakannya pada sebuah thread terpisah, misalnya:

@Override @WithReadLock
void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    Thread.start('ProsesHexdump') {
        TextView output = (TextView) findViewById(R.id.output)
        StringBuilder hexdump = new StringBuilder()
        StringBuilder ascii = new StringBuilder()
        int i = progress * 1024
        int max = Math.min(i + 1024, bytes.length)
        for (; i < max; i++) {
            hexdump.append(String.format("%02x ", bytes[i]))
            ascii.append(Character.isLetterOrDigit(bytes[i]) ? (char) bytes[i] : '.')
            if ((i > 0) && (((i + 1) % 8) == 0)) {
                hexdump.append(ascii.toString())
                hexdump.append('n')
                ascii = new StringBuilder()
            }
        }
        if (ascii) {
            (1..(8 - (i % 8))).each { hexdump.append('   ') }
            hexdump.append(ascii.toString())
        }

        output.post({
            String hasil = hexdump.toString()
            output.setText(hasil)
            Intent sendIntent = new Intent(Intent.ACTION_SEND)
            sendIntent.putExtra(Intent.EXTRA_TEXT, hasil)
            sendIntent.setType('text/plain')
            shareActionProvider.setShareIntent(sendIntent)
        } as Runnable)
    }
}

Sekarang, setiap kali membaca file, thread ‘BacaFile’ akan dibuat. Setiap kali seek bar digeser, thread ‘ProsesHexDump’ akan tercipta. Thread baru ini juga akan menunggu hingga ‘BacaFile’ selesai dikerjakan bila thread tersebut masih bekerja.

Iklan

Memakai Fork/Join Framework Di Groovy

Salah satu masalah yang timbul pada percobaan saya di artikel sebelumnya (Multithreading Di Groovy) adalah walaupun saya membuat beberapa thread berbeda, thread yang sudah menyelesaikan tugasnya terlebih dahulu akan nganggur (berada dalam kondisi wait). Untuk mengatasi permasalahan tersebut, Java 7 telah dilengkapi dengan framework Fork/Join dengan kemampuan work-stealing. Groovy kemudian menyediakan cara mudah memakai Fork/Join Framework melalui GPars.

Sebagai latihan, saya akan mencoba membuat kode program sebelumnya yang memakai framework Executor menjadi menggunakan framework Fork/Join seperti berikut ini:

import groovy.transform.CompileStatic
import javax.swing.JOptionPane
import static groovyx.gpars.GParsPool.runForkJoin
import static groovyx.gpars.GParsPool.withPool

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

@CompileStatic
boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

List hasil

withPool(4) {
    hasil = runForkJoin(2, 100000) { angka1, angka2 ->
        List hasilSementara = []
        if ((angka2 - angka1) > 500) {
            def tengah = (int)((angka2-angka1)/2)
            forkOffChild(angka1, angka1+tengah-1)
            forkOffChild(angka1+tengah, angka2)
            childrenResults.each { hasilSementara.addAll(it) }
        } else {
            for (long angka=angka1; angka<=angka2; angka++) {
                if (isPrimary(angka)) {
                    hasilSementara << angka
                }
            }
        }
        hasilSementara
    }
}

hasil.sort()
long waktuSelesai = System.nanoTime()
println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Pada kode program di atas, saya perlu meletakkan kode program dalam withPool() yang akan memakai framework Fork/Join. Saya juga dapat menentukan jumlah thread yang akan dipakai dengan memberikan parameter angka seperti withPool(4) { ... }. Setelah itu, saya meletakkan kode program yang memakai algoritma divide and conquer pada closure di runForkJoin(). Di dalam closure untuk runForkJoin(), saya dapat memanggil method forkOffChild() dan getChildrenResults(). Method forkOffChild() akan menjadwalkan pemanggilan dan langsung lanjut ke perintah berikutnya (setara dengan fork). Method getChildrenResults() akan menunggu hingga seluruh child selesai di-eksekusi (setara dengan join) dan mengembalikan hasilnya dalam bentuk List. Tanpa @CompileStatic di method isPrimary(), kode program akan berjalan sangat lambat, bahkan lebih lambat dari versi yang hanya memakai thread tunggal.

Bila saya menjalankan kode program di atas dan melihat daftar thread melalui JVisualVM, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan thread yang sibuk semua

Tampilan thread yang sibuk semua

Kali ini terlihat bahwa thread yang ada semuanya sibuk dan tidak ada lagi yang ‘santai’. Walaupun demikian, secara garis besar, kinerjanya tidak lebih baik daripada versi sebelumnya yang memakai framework Executor. Hal ini karena kode program harus melakukan hal baru yang sebelumnya tidak ada seperti menjadwalkan tugas (dengan forkOffChild()) dan mengisi/menggabungkan List.

Setidaknya bila dibandingkan dengan kode program versi Java di artikel Seberapa Jauh Bedanya Program Yang Multithreading?, versi Groovy-nya terasa lebih sederhana dan lebih mudah dipahami. Bukan hanya itu, kode program di atas sebenarnya masih dapat disederhanakan lagi menjadi seperti:

import groovy.transform.CompileStatic
import javax.swing.JOptionPane
import static groovyx.gpars.GParsPool.withPool

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

@CompileStatic
boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

List hasil

withPool(4) {
    hasil = (2..100000).findAllParallel { isPrimary(it) }
    hasil.sort()
}

long waktuSelesai = System.nanoTime()
println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program di atas memakai findAllParallel() yang akan menggunakan framework Fork/Join untuk mengerjakan operasi pada List secara paralel. Pada Groovy, range seperti (2..100000) adalah sebuah List (Range diturunkan dari List).

Bila saya ingin mencetak setiap bilangan prima ke layar, saya dapat menggunakan kode program seperti:

withPool {
    (2..100000).eachParallel {
        if (isPrimary(it)) println it
    }
}

Multithreading Di Groovy

Karena Groovy dapat mengakses class Java, maka secara tidak langsung, Groovy juga mendukung multithreading. Java 8 memiliki banyak fitur tambahan yang berkaitan dengan multithreading (concurrency), misalnya tambahan method forEach(), search(), reduce(), Array.parallelSort(), dan sebagainya. Groovy, tanpa Java 8, juga memiliki fasilitas yang serupa melalui GPars. Sebelumnya GPars adalah proyek terpisah tapi kini sudah menjadi bagian dari Groovy sehingga tidak perlu di-download secara terpisah lagi.

Sebagai latihan, saya akan membuat sebuah program yang mencari bilangan prima. Program ini akan memakai algoritma yang paling buruk dan paling lambat, tapi setidaknya sangat mudah dimengerti. Ini adalah versi kode program yang hanya berjalan pada thread tunggal:

List hasil = []

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

(2..100000).each { angka ->
    if (isPrimary(angka)) hasil << angka
}

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program tersebut membutuhkan waktu sekitar 9.356.485.040 ns. Kode program tersebut hanya bekerja pada satu thread yaitu thread main yang terlihat seperti pada gambar hasil di JVisualVM berikut ini:

Kode program yang hanya dijalankan di thread main

Kode program yang hanya dijalankan di thread main

Thread dengan nama main adalah thread yang diciptakan secara otomatis pada saat kode program Java dikerjakan. Karena kode program di atas tidak membuat thread baru, maka hingga selesai dikerjakan, program yang saya buat di atas akan berjalan di thread main.

Pada Groovy, saya tetap dapat menggunakan instance dari Thread untuk mewakili sebuah thread baru. Akan tetapi, saya akan memakai cara yang lebih mudah dengan menggunakan task dari GPars. Sebagai contoh, saya akan mengubah kode program di atas agar membagi proses looping menjadi 2 bagian yang paralel, seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (50001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Kode program di atas membutuhkan waktu sekitar 7.585.801.575 ns atau lebih cepat 19% dibanding versi thread tunggal sebelumnya. Pada saat program ini dijalankan, t1 dan t2 masing-masing akan dikerjakan pada thread terpisah dengan nama Actor Thread 1 dan Actor Thread 2 seperti yang terlihat pada gambar hasil JVisualVM berikut ini:

Kode program yang dijalankan pada 2 thread

Kode program yang dijalankan pada 2 thread

Terlihat bahwa thread main kini menghabiskan lebih banyak waktunya untuk menunggu. Actor Thread 2 bekerja lebih keras sementara Actor Thread 1 lebih duluan selesai. Walaupun sudah selesai, Actor Thread 1 harus menunggu hingga thread lain selesai, karena saya memanggil join() dengan [t1,t2]*.join(). Thread yang nganggur adalah pemborosan, oleh sebab itu Java 7 dilengkapi dengan fork/join framework yang memiliki algoritma work-stealing untuk mengurangi thread yang nganggur. Saya tidak akan memakainya disini.

Saya akan mencoba meningkatkan jumlah task menjadi lebih banyak dengan mengubah kode program menjadi seperti berikut ini:

import javax.swing.JOptionPane
import java.util.concurrent.ConcurrentSkipListSet
import static groovyx.gpars.dataflow.Dataflow.task

ConcurrentSkipListSet hasil = new ConcurrentSkipListSet()

JOptionPane.showMessageDialog(null, 'Attach Terlebih Dahulu Profiler Bila Perlu...')

long waktuMulai = System.nanoTime()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (1..25000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) hasil << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) hasil << it
    }
}

[t1,t2,t3,t4]*.join()

long waktuSelesai = System.nanoTime()

println "Waktu Eksekusi: ${waktuSelesai - waktuMulai}"

Waktu eksekusi kini menjadi 5.899.754.797 atau lebih cepat 37% dibanding versi thread tunggal. Gambar hasil JVisualVM menunjukkan bahwa kini ada 4 thread yang bekerja secara paralel:

Kode program yang dijalankan pada 4 thread

Kode program yang dijalankan pada 4 thread

Terlihat pada thread yang memproses bilangan yang kecil akan lebih cepat selesai dan lebih banyak menunggu (warna kuning menunjukkan thread sedang nganggur). Hal ini karena bilangan kecil membutuhkan looping yang lebih singkat, sementara bilang besar membutuhkan lebih banyak looping.

Bila saya hanya ingin menampilkan bilangan prima yang ditemukan langsung pada layar, saya dapat membuat sebuah task baru seperti berikut ini:

def outputTask = task {
    while (true) {
        println hasil.last()
    }
}

Task di atas akan menampilkan hasil perhitungan terakhir. Tapi karena ia dijalankan tidak tersinkronisasi dengan task lainnya, maka sering kali nilai yang sama ditampilkan berulang kali. Groovy mengatasi hal ini dengan cara yang sangat sederhana yang disebut dataflow. Dengan dataflow, sebuah task menulis nilai, dan task lain akan mengerjakan sesuatu hanya jika nilai tersebut sudah terisi. Sebagai contoh, saya dapat menggunakan DataflowQueues seperti pada kode program berikut ini:

import groovyx.gpars.dataflow.DataflowQueue
import static groovyx.gpars.dataflow.Dataflow.task

DataflowQueue buffer = new DataflowQueue()

boolean isPrimary(long angka) {
    for (int i=2; i<angka-1; i++) {
        if ((angka % i) == 0) return false
    }
    true
}

def t1 = task {
    (2..25000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t2 = task {
    (25001..50000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t3 = task {
    (50001..75000).each {
        if (isPrimary(it)) buffer << it
    }
}

def t4 = task {
    (75001..100000).each {
        if (isPrimary(it)) buffer << it
    }
}

def output = task {
    while (true) {
        println buffer.val
    }
}

[t1,t2,t3,t4]*.join()

Untuk mengisi nilai pada dataflow, saya menggunakan kode program seperti buffer << it. Untuk mengakses nilainya, saya menggunakan kode program seperti buffer.val yang akan menunggu hingga ada nilai yang dapat dibaca.

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.

Multithreading Dengan Mudah Di Griffon

Hampir semua jenis GUI memakai sebuah thread tunggal untuk menangani seluruh operasi yang berkaitan dengan GUI tersebut. Ingin buktinya? Drag sebuah button ke form, lalu buat sebuah kode program yang memakan waktu lama (mungkin 3 menit!).  Jalankan program dan klik button tersebut!  Hasilnya?  Program akan terlihat seolah-olah hang dan tidak dapat merespon.   Mungkin itu sebabnya saya pernah membaca cerita developer berpengalaman yang protes melihat tutorial memberi aksi pada button yang sesederhana ini tanpa menyebutkan soal thread sama sekali.

Btw, masalah ini bukan hanya terjadi pada Swing milik Java, tapi juga Windows Presentation Foundation milik .NET, GTK, SWT, dan banyak lagi.  Karena yang menangani tampilan hanya sebuah thread, maka bila ingin men-update tampilan, harus berada di thread tersebut.   Secara teori, melakukan modifikasi tampilan tanpa melalui thread yang mengelola tampilan, adalah sebuah kesalahan!

Thread yang mengelola tampilan di Swing disebut sebagai Event Dispatching Thread (EDT).  Semua kode program yang saya buat untuk memodifikasi user interface (misalnya, mengubah isi label, mengubah warna, dsb) harus berjalan pada EDT.   Selain itu, semua kode program yang membutuhkan waktu eksekusi yang lama harus berjalan pada sebuah thread lain.   Jika dipaksakan berjalan pada EDT, maka selama EDT sibuk mengerjakan proses tersebut, tampilan tidak akan memberi respon pada pengguna dan terlihat seperti nge-hang.

Beruntungnya, Griffon sebagai framework MVC untuk aplikasi desktop, dilengkapi dengan berbagai kemudahan dalam pengelolaan threading.   Salah satunya adalah kode program untuk view secara otomatis akan dijalankan di EDT, sementara itu kode program untuk controller secara otomatis akan dijalankan pada thread terpisah.   Developer tentu saja masih punya kewenangan untuk mengubah perilaku ini.

Sebagai contoh, saya ingin men-simulasi-kan cara yang SALAH, yaitu menjalankan kode program controller yang memakan waktu lama di EDT. Saya mulai dengan membuat view sederhana seperti berikut ini:

package latihan

application(title: 'latihan',
  preferredSize: [520, 240],
  pack: true,
  locationByPlatform:true,
  iconImage: imageIcon('/griffon-icon-48x48.png').image,
  iconImages: [imageIcon('/griffon-icon-48x48.png').image,
               imageIcon('/griffon-icon-32x32.png').image,
               imageIcon('/griffon-icon-16x16.png').image]) {

	flowLayout()
	button("Simulasi Proses Lama", 
		actionPerformed: controller.simulasiProsesLama)
	textField(id: "txtOutput", columns: 20)
	button("Klik Saya Saat Simulasi Proses Lama Berlangsung",
		actionPerformed: controller.klikSelamaSimulasi)
}

Lalu pada controller, saya membuat kode program seperti berikut ini:

package latihan

import griffon.transform.Threading

class LatihanController {

	def view

	@Threading(Threading.Policy.INSIDE_UITHREAD_SYNC)
	def simulasiProsesLama = {
		// Looping tak terhingga
		while (true)
			print "Looping & Looping terus..."		
	}

	def klikSelamaSimulasi = {
		view.txtOutput.text = "Nilai dikirim ke text field!"
	}
}

Perhatikan bahwa saya membuat looping tak terhingga di closure simulasiProsesLama.   Anggap saja ini mewakili sebuah proses yang memakan waktu lama, misalnya melakukan query yang sangat berat atau mencari file di harddisk.   Kemudian, saya memaksakan agar method ini dikerjakan di EDT dengan memberikan annotation @Threading.    Bila saya men-klik tombol “Simulasi Proses Lama“, maka hasilnya akan terlihat seperti pada gambar berikut ini:

Tampilan UI yang Hang

Tampilan UI yang Hang

Begitu saya men-klik tombol tersebut, maka aplikasi menjadi hang!   Yup!   Inilah yang terjadi bila saya ‘sembarangan‘ meletakkan kode program.

Griffon sebenarnya sudah secara otomatis menjalankan kode program controller di thread terpisah.   Saya hanya perlu membuang annotation @Threading di closure simulasiProsesLama untuk membuat aplikasi berjalan sesuai dengan yang diharapkan.

Bagaimana bila saya ingin men-update tampilan GUI di controller? Bukankah untuk melakukan perubahan terhadap GUI harus selalui di EDT, sementara kode program controller berjalan di thread terpisah? Saya bisa meletakkan kode program yang melakukan modifikasi GUI di sebuah closure yang dilewatkan sebagai argumen untuk method edt().

Sebagai contoh, saya mengubah kode program pada controller untuk mengubah nilai JTextField selama berada dalam looping tak terhingga, yang terlihat seperti berikut ini:

package latihan

import java.awt.Color

class LatihanController {

	def view

	def rand = new Random()

	def simulasiProsesLama = {
		def i = 0;
		// Looping tak terhingga
		while (true) {
			edt {
				view.txtOutput.text = i++
			}
		}

	}

	def klikSelamaSimulasi = {
                edt {
		  view.txtOutput.setBackground(new Color(
			rand.nextInt(255), rand.nextInt(255), rand.nextInt(255)))
                }
	}
}

Hasilnya bila dijalankan akan terlihat seperti pada animasi GIF berikut ini:

Tetap Responsif Saat Mengupdate UI

Tetap Responsif Saat Mengupdate UI

Apa yang terjadi bila saya lupa memanggil method edt()? Tampilan angka tidak akan terlihat bertambah satu per satu secara mulus, tetapi kadang loncat cukup jauh. Ini bukan masalah logic di program karena saya sudah benar memakai i++, hanya EDT yang ‘tidak pasti‘ memperbaharui tampilan.   Yup!   Salah satu bug yang paling menakutkan adalah bug yang berhubungan multithreading.   Bug jenis ini sangat susah ditebak dan sulit direka ulang.