Perbedaan Antara Thread dan Virtual Thread di Java

1. Perkenalan

Dalam tutorial ini, kami akan menunjukkan perbedaan antara utas tradisional di Java dan utas virtual yang diperkenalkan di Project Loom.

Selanjutnya, kami akan membagikan beberapa kasus penggunaan untuk utas virtual dan API yang telah diperkenalkan oleh proyek.

Sebelum kita mulai, perlu dicatat bahwa proyek ini sedang dalam pengembangan aktif. Kami akan menjalankan contoh kami pada VM alat tenun akses awal: openjdk-15-loom + 4-55_windows-x64_bin.

Versi build yang lebih baru bebas untuk mengubah dan merusak API saat ini. Meskipun demikian, sudah ada perubahan besar dalam API, karena kelas java.lang.Fiber yang sebelumnya digunakan telah dihapus dan diganti dengan kelas java.lang.VirtualThread yang baru .

2. Ringkasan Tingkat Tinggi Thread vs. Thread Virtual

Pada tingkat tinggi, utas dikelola dan dijadwalkan oleh sistem operasi, sedangkan utas virtual dikelola dan dijadwalkan oleh mesin virtual . Sekarang, untuk membuat utas kernel baru, kita harus melakukan panggilan sistem, dan itu operasi yang mahal .

Itulah mengapa kami menggunakan kumpulan utas alih-alih mengalokasikan ulang dan membatalkan alokasi utas sesuai kebutuhan. Selanjutnya, jika kita ingin menskalakan aplikasi kita dengan menambahkan lebih banyak utas, karena peralihan konteks dan jejak memorinya, biaya pemeliharaan utas tersebut mungkin signifikan dan memengaruhi waktu pemrosesan.

Kemudian, biasanya, kami tidak ingin memblokir utas tersebut, dan ini menghasilkan penggunaan API I / O non-pemblokiran dan API asinkron, yang mungkin mengacaukan kode kami.

Sebaliknya, utas virtual dikelola oleh JVM . Oleh karena itu, alokasi mereka tidak memerlukan panggilan sistem , dan mereka bebas dari sakelar konteks sistem operasi . Lebih lanjut, utas virtual berjalan pada utas pembawa, yang merupakan utas kernel sebenarnya yang digunakan di bawah tenda. Akibatnya, karena kami bebas dari sakelar konteks sistem, kami dapat menelurkan lebih banyak utas virtual serupa.

Selanjutnya, properti utama utas virtual adalah mereka tidak memblokir utas operator kami. Dengan itu, memblokir utas virtual menjadi operasi yang jauh lebih murah, karena JVM akan menjadwalkan utas virtual lain, membiarkan utas operator tidak diblokir.

Pada akhirnya, kami tidak perlu menjangkau NIO atau Async API. Ini akan menghasilkan kode yang lebih mudah dibaca yang lebih mudah dipahami dan di-debug. Namun demikian, kelanjutan berpotensi memblokir thread operator - khususnya, saat thread memanggil metode asli dan melakukan operasi pemblokiran dari sana.

3. API Pembuat Thread Baru

Di Loom, kami mendapatkan API pembangun baru di kelas Thread , bersama dengan beberapa metode pabrik. Mari kita lihat bagaimana kita dapat membuat pabrik standar dan virtual dan memanfaatkannya untuk eksekusi utas kita:

Runnable printThread = () -> System.out.println(Thread.currentThread()); ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory(); ThreadFactory kernelThreadFactory = Thread.builder().factory(); Thread virtualThread = virtualThreadFactory.newThread(printThread); Thread kernelThread = kernelThreadFactory.newThread(printThread); virtualThread.start(); kernelThread.start();

Inilah output dari proses di atas:

Thread[Thread-0,5,main] VirtualThread[,ForkJoinPool-1-worker-3,CarrierThreads]

Di sini, entri pertama adalah output toString standar dari utas kernel.

Sekarang, kita melihat dalam output bahwa utas virtual tidak memiliki nama, dan itu dijalankan pada utas pekerja dari kumpulan Fork-Join dari grup utas CarrierThreads .

Seperti yang dapat kita lihat, terlepas dari implementasi yang mendasarinya, API-nya sama, dan itu berarti kita dapat dengan mudah menjalankan kode yang ada pada utas virtual .

Selain itu, kami tidak perlu mempelajari API baru untuk memanfaatkannya.

4. Komposisi Benang Virtual

Ini adalah kelanjutan dan penjadwal yang, bersama-sama, membuat utas virtual. Sekarang, penjadwal mode-pengguna kami dapat berupa implementasi apa pun dari antarmuka Pelaksana . Contoh di atas telah menunjukkan kepada kita bahwa, secara default, kita berjalan di ForkJoinPool .

Sekarang, mirip dengan utas kernel - yang dapat dieksekusi pada CPU, kemudian diparkir, dijadwal ulang kembali, dan kemudian melanjutkan eksekusinya - kelanjutan adalah unit eksekusi yang dapat dimulai, kemudian diparkir (dihasilkan), dijadwal ulang kembali, dan dilanjutkan eksekusinya dengan cara yang sama dari saat ia berhenti dan masih dikelola oleh JVM alih-alih mengandalkan sistem operasi.

Perhatikan bahwa lanjutannya adalah API tingkat rendah, dan pemrogram harus menggunakan API tingkat yang lebih tinggi seperti API pembuat untuk menjalankan utas virtual.

Namun, untuk menunjukkan cara kerjanya, sekarang kami akan menjalankan percobaan lanjutan kami:

var scope = new ContinuationScope("C1"); var c = new Continuation(scope, () -> { System.out.println("Start C1"); Continuation.yield(scope); System.out.println("End C1"); }); while (!c.isDone()) { System.out.println("Start run()"); c.run(); System.out.println("End run()"); }

Inilah output dari proses di atas:

Start run() Start C1 End run() Start run() End C1 End run()

Dalam contoh ini, kami menjalankan kelanjutan kami dan, pada titik tertentu, memutuskan untuk menghentikan pemrosesan. Kemudian setelah kami menjalankannya kembali, kelanjutan kami berlanjut dari tempat yang ditinggalkannya. Melalui keluaran, kita melihat bahwa metode run () dipanggil dua kali, tetapi kelanjutan dimulai satu kali dan kemudian melanjutkan eksekusinya pada proses kedua dari tempat ia berhenti.

Ini adalah bagaimana operasi pemblokiran dimaksudkan untuk diproses oleh JVM. Setelah operasi pemblokiran terjadi, kelanjutan akan dihasilkan, membiarkan utas operator tidak diblokir.

Jadi, yang terjadi adalah utas utama kami membuat bingkai tumpukan baru pada tumpukan panggilannya untuk metode run () dan melanjutkan dengan eksekusi. Kemudian, setelah kelanjutan dihasilkan, JVM menyimpan status eksekusinya saat ini.

Selanjutnya, utas utama melanjutkan eksekusinya seolah-olah metode run () dikembalikan dan dilanjutkan dengan loop sementara . Setelah panggilan kedua ke metode run lanjutan , JVM memulihkan status utas utama ke titik di mana kelanjutan telah menghasilkan dan menyelesaikan eksekusi.

5. Kesimpulan

Pada artikel ini, kami membahas perbedaan antara utas kernel dan utas virtual. Selanjutnya, kami menunjukkan bagaimana kami dapat menggunakan API pembuat utas baru dari Project Loom untuk menjalankan utas virtual.

Akhirnya, kami menunjukkan apa itu kelanjutan dan bagaimana cara kerjanya di bawah tenda. Kita dapat mempelajari lebih lanjut status Project Loom dengan memeriksa VM akses awal. Alternatifnya, kita dapat menjelajahi lebih banyak API konkurensi Java yang sudah distandarisasi.