Pelaksana newCachedThreadPool () vs newFixedThreadPool ()

1. Ikhtisar

Dalam hal implementasi kumpulan utas, pustaka standar Java menyediakan banyak opsi untuk dipilih. Kumpulan thread tetap dan yang di-cache cukup ada di mana-mana di antara implementasi tersebut.

Dalam tutorial ini, kita akan melihat bagaimana kumpulan thread bekerja di bawah tenda dan kemudian membandingkan implementasi ini dan kasus penggunaannya.

2. Kumpulan Benang Tersimpan

Mari kita lihat bagaimana Java membuat kumpulan utas yang di-cache ketika kita memanggil Executors.newCachedThreadPool () :

public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

Kumpulan utas yang disimpan dalam cache menggunakan "handoff sinkron" untuk memasukkan tugas baru ke antrean. Ide dasar handoff sinkron sederhana namun kontra-intuitif: Seseorang dapat mengantrekan item jika dan hanya jika utas lain mengambil item itu pada waktu yang sama. Dengan kata lain, para SynchronousQueue tidak bisa menahan tugas apapun.

Misalkan ada tugas baru yang masuk. Jika ada thread diam menunggu di antrian, maka pembuat tugas menyerahkan tugas tersebut ke thread tersebut. Jika tidak, karena antrian selalu penuh, pelaksana membuat utas baru untuk menangani tugas itu .

Kumpulan yang di-cache dimulai dengan tanpa utas dan berpotensi dapat berkembang menjadi utas Integer.MAX_VALUE . Secara praktis, satu-satunya batasan untuk kumpulan utas yang di-cache adalah sumber daya sistem yang tersedia.

Untuk mengelola sumber daya sistem dengan lebih baik, kumpulan utas yang di-cache akan menghapus utas yang tetap menganggur selama satu menit.

2.1. Gunakan Kasus

Konfigurasi kumpulan utas yang di-cache menyimpan utas (karena itu namanya) untuk waktu yang singkat untuk digunakan kembali untuk tugas lain. Akibatnya, ini bekerja paling baik ketika kita berurusan dengan sejumlah tugas yang berumur pendek.

Kuncinya di sini adalah "masuk akal" dan "berumur pendek". Untuk memperjelas hal ini, mari kita evaluasi skenario di mana kumpulan yang di-cache tidak sesuai. Di sini kami akan mengirimkan satu juta tugas, masing-masing membutuhkan waktu 100 mikro-detik untuk menyelesaikannya:

Callable task = () -> { long oneHundredMicroSeconds = 100_000; long startedAt = System.nanoTime(); while (System.nanoTime() - startedAt  task).collect(toList()); var result = cachedPool.invokeAll(tasks);

Ini akan membuat banyak utas yang diterjemahkan ke penggunaan memori yang tidak wajar, dan lebih buruk lagi, banyak sakelar konteks CPU. Kedua anomali ini akan sangat mengganggu kinerja secara keseluruhan.

Oleh karena itu, kita harus menghindari kumpulan thread ini ketika waktu eksekusi tidak dapat diprediksi, seperti tugas yang terikat IO.

3. Kumpulan Benang Tetap

Mari kita lihat bagaimana kumpulan benang tetap bekerja di bawah kap:

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

Berbeda dengan kumpulan utas yang di-cache, yang ini menggunakan antrian tak terbatas dengan jumlah utas yang tidak pernah kedaluwarsa . Oleh karena itu, alih-alih jumlah utas yang terus meningkat, kumpulan utas tetap mencoba menjalankan tugas masuk dengan jumlah utas tetap . Ketika semua utas sibuk, maka pelaksana akan mengantri tugas baru. Dengan cara ini, kami memiliki kendali lebih besar atas konsumsi sumber daya program kami.

Hasilnya, kumpulan thread tetap lebih cocok untuk tugas dengan waktu eksekusi yang tidak dapat diprediksi.

4. Persamaan yang Tidak Diuntungkan

Sejauh ini, kami hanya menyebutkan perbedaan antara kumpulan thread yang di-cache dan tetap.

Terlepas dari semua perbedaan itu, mereka berdua menggunakan AbortPolicy sebagai kebijakan saturasi mereka . Oleh karena itu, kami mengharapkan pelaksana ini untuk membuat pengecualian ketika mereka tidak dapat menerima dan bahkan mengantrekan tugas lagi.

Mari kita lihat apa yang terjadi di dunia nyata.

Kumpulan utas yang di-cache akan terus membuat lebih banyak utas dalam keadaan ekstrim, jadi, secara praktis, mereka tidak akan pernah mencapai titik jenuh . Demikian pula, kumpulan utas tetap akan terus menambahkan lebih banyak tugas dalam antriannya. Oleh karena itu, kolam tetap juga tidak akan pernah mencapai titik jenuh .

Karena kedua kumpulan tidak akan jenuh, ketika bebannya sangat tinggi, mereka akan menghabiskan banyak memori untuk membuat utas atau tugas antrian. Menambahkan penghinaan ke cedera, kumpulan benang yang di-cache juga akan menimbulkan banyak sakelar konteks prosesor.

Bagaimanapun, untuk memiliki kontrol lebih atas konsumsi sumber daya, sangat disarankan untuk membuat ThreadPoolExecutor kustom :

var boundedQueue = new ArrayBlockingQueue(1000); new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy()); 

Di sini, kumpulan utas kami dapat memiliki hingga 20 utas dan hanya dapat mengantri hingga 1000 tugas. Juga, ketika tidak dapat menerima beban lagi, itu hanya akan membuat pengecualian.

5. Kesimpulan

Dalam tutorial ini, kami telah mengintip kode sumber JDK untuk melihat bagaimana Executor berbeda bekerja di balik terpal. Kemudian, kami membandingkan kumpulan utas tetap dan yang di-cache serta kasus penggunaannya.

Pada akhirnya, kami mencoba mengatasi konsumsi resource yang tidak terkontrol dari kumpulan tersebut dengan kumpulan thread kustom.