Lewati ke konten utama

Cari di CooLWJ

选择搜索范围,输入关键词后按回车。

Bahasa

Melihat Teknologi dari Perspektif Menyeluruh-Sejarah Evolusi Multithreading Java

Pengantar

Pada 22 bulan 09 tahun 2022,JDK19 dirilis,dengan sorotan utama berupa dukungan thread virtual,yang menambahkan anggota andalan baru ke keluarga thread ringan。Thread virtual membebaskan JVM dari ketergantungan pada penjadwalan thread oleh sistem operasi,sehingga JVM menjadwalkan thread sendiri。Sebenarnya,pada masa awal Sun pernah menerapkan penjadwalan thread oleh JVM dalam mesin virtual di sistem operasi Solaris,tetapi karena pertimbangan kompleksitas dan kemudahan pemeliharaan,akhirnya kembali ke model penjadwalan thread oleh sistem operasi。

“长安归来锦衣客,昨日城南起新宅。”(Pulang dari Chang’an, sang tamu berbusana indah;kemarin, kediaman baru berdiri di selatan kota。)Menengok kembali perjalanan ini, konsep-konsep multithreading begitu beragam hingga membuat mata berkunang-kunang, dan penjelasan terkait di internet pun tak terhitung banyaknya, tetapi saya selalu merasa ada sudut pandang menyeluruh yang belum tersedia. Karena itu, saya menyusun sejarah evolusi multithreading di Java secara sistematis, dengan harapan dapat membantu Anda menguasai pengetahuan multithreading。

Hal yang tidak dibahas dalam artikel ini:

1 Tidak membahas secara terperinci prinsip implementasi beberapa aspek teknis, tidak menguraikan kode sumber, dan tidak membuat diagram. Jika menemukan konsep atau teknologi yang menarik dalam artikel ini, Anda dapat mencarinya sendiri 2 Tidak membahas pustaka dan kerangka kerja yang mendukung konkurensi, seperti Quasar、Akka、Guava

Apa yang dibahas dalam artikel ini

1 Membahas sejarah evolusi multithreading JDK 2 Membahas fungsi, prinsip kerja, dan latar belakang beberapa aspek teknis dalam evolusi tersebut, serta masalah yang diatasinya 3 Membahas pandangan penulis tentang beberapa aspek teknis, dan mengundang siapa pun yang berpandangan berbeda untuk berdiskusi di kolom komentar

Tonggak pencapaian

Seperti biasa, kita mulai dengan tabel ringkasan. Tabel ini merangkum konsep inti terkait thread dalam berbagai generasi JDK. Di sini, dengan perumpamaan yang mungkin kurang tepat, evolusi multithreading dapat dipetakan ke mobil, yakni era transmisi manual (JDK1.4 dan sebelumnya), era transmisi otomatis(JDK5-JDK18), dan era mengemudi otonom (JDK19 dan sesudahnya). Perumpamaan ini hanya bertujuan memberi tahu pembaca bahwa sejak JDK5, multithreading dapat dikendalikan dengan cara yang lebih nyaman, sedangkan sejak JDK19, manfaatnya bahkan melampaui sekadar kenyamanan dan membawa lompatan kualitatif pada kinerja layanan yang intensif I/O.

ZamanVersiWaktu publikasiKonsep inti
Mode manualJDK1.01996-01-23Thread dan Runnable
Mode manualJDK1.21998-12-04ThreadLocal、Collections
Mode otomatisJDK1.5/5.02004-09-30Memperjelas model memori Java, memperkenalkan paket konkurensi
Mode otomatisJDK1.6/6.02006-12-11Optimisasi synchronized
Mode otomatisJDK1.7/7.02011-07-28Kerangka kerja Fork/Join
Mode otomatisJDK1.8/8.02014-03-18CompletableFuture、Stream
Mode otomatisJDK1.9/9.02014-09-08Memperbaiki mekanisme perebutan kunci
Mode otomatisJDK102018-03-21Pengelolaan lokal pada thread
Mode otomatisJDK152020-09-15Menonaktifkan dan mendeprekasi biased locking
Mengemudi otonomJDK192022-09-22Thread virtual

2f98c71c0436418d8d260d75cb921c81~tplv-73owjymdk6-jj-mark-v1_0_0_0_0_5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5Lqs5Lic5LqR5byA5Y-R6ICF_q75.webp

Era transmisi manual

Penulis menyebut masa JDK1.4 dan versi sebelumnya sebagai era multithreading “transmisi manual”, yang juga disebut era multithreading native. Pengoperasian thread masih relatif mendasar dan belum tersedia thread pool. Pengembang harus menulis sendiri utilitas untuk menghindari pemborosan sumber daya akibat pembuatan thread berulang kali, serta mengunci sumber daya bersama secara manual. Pada era inilah banyak kerangka kerja multithreading unggulan mulai berkembang, dan yang paling terkenal diadopsi oleh JDK5.0.

JDK 1.0 Thread dan Runnable

Versi JDK1.0 pada bulan 1 tahun 1996 sejak awal menetapkan model thread paling mendasar bagi Java. Dalam berbagai perbaikan berikutnya, model thread ini tidak mengalami perubahan substansial, sehingga dapat disebut sebagai rancangan yang baik dan terus diwariskan. Preemptive dan kooperatif merupakan dua metode umum penjadwalan proses/thread. Sistem operasi sangat cocok menggunakan metode preemptive untuk menjadwalkan prosesnya: sistem membagikan jatah waktu kepada berbagai proses, dan untuk proses yang lama tidak merespons, sistem mampu mencabut sumber dayanya, bahkan menghentikannya secara paksa. Metode kooperatif mengharuskan proses secara sadar dan aktif melepaskan sumber daya. Dalam metode penjadwalan ini, satu thread yang berjalan sangat lama mungkin membuat semua thread lain yang membutuhkan CPU mengalami ”kelaparan”. Mesin virtual Java hotspot menggunakan penjadwalan preemptive, sehingga bahasa Java sejak awal menerapkan penjadwalan thread preemptive. Dalam JDK 1.0, thread terutama dibuat dengan mewarisi kelas Thread atau mengimplementasikan antarmuka Runnable. Thread dijalankan melalui metode start pada instans objek, kode yang perlu diproses secara paralel ditempatkan dalam metode run, dan komunikasi untuk kerja sama antar-thread menggunakan metode yang sederhana dan kasar seperti stop/resume/suspend.

Diagram status thread Java dan perpindahan statusnya

Bagaimana menjelaskan konsep stop/resume/suspend?Artinya, thread utama dapat langsung memanggil metode penghentian, penangguhan, dan pelanjutan thread anak. Jika saat kecil kamu pernah memakai pemutar musik portabel, perangkat itu memiliki tiga tombol: berhenti, jeda, dan lanjutkan. Bayangkan kamu sedang mendengarkan 3 pemutar musik portabel sekaligus. Ketiga pemutar itu adalah tiga thread anak, sedangkan kamu adalah thread utama. Kamu bebas mengendalikan kapan ketiga perangkat itu berjalan atau berhenti.

Mekanisme ini memiliki masalah fatal,yaitu mudah mengalami deadlock,karena ketika thread A mengunci suatu sumber daya dan belum melepaskannya,thread utama menangguhkannya(metode suspend tidak melepaskan kunci),jika thread B ingin menguasai sumber daya tersebut saat itu,ia hanya bisa menunggu sampai thread A melanjutkan eksekusi(resume)lalu melepaskan sumber daya,jika tidak,ia tidak akan pernah mendapatkannya,sehingga terjadi deadlock。

JDK 1.2

Mekanisme stop/resume/suspend yang kasar dilarang digunakan dalam versi ini, lalu diganti dengan pendekatan koordinasi beberapa thread seperti wait/notify/sleep. Perlu disebutkan bahwa dalam versi ini, objek atomik AtomicityXXX sudah dirancang, terutama untuk mengatasi sifat i++ yang tidak atomik. Penambahan ThreadLocal dan Collections memperluas cara penggunaan multithreading. Karena kedua teknologi ini, penulis menyebutnya era turbo Java.

ThreadLocal

ThreadLocal merupakan solusi tanpa kunci yang memungkinkan beberapa thread menggunakan objek yang tidak aman untuk thread. Ia tidak dapat menyelesaikan masalah seperti “penambahan atau pengurangan saldo rekening bank maupun stok”, tetapi unggul dalam menjalankan metode kelas yang bersifat “alat bantu” secara aman melalui salinan masing-masing. Contoh khasnya adalah SimpleDateFormat, koneksi basis data, dan sebagainya. Desainnya patut disebut sangat cerdik. Bayangkan jika Anda diminta merancangnya, pendekatan sederhana yang umum adalah:memelihara Map global yang aman untuk thread di dalam ThreadLocal, dengan thread sebagai key dan objek bersama sebagai value. Desain ini memiliki kelemahan berupa kebocoran memori, karena Map tersebut akan terus membesar tanpa batas seiring semakin banyak thread bergabung. Untuk mengatasi kebocoran memori, Map harus dibersihkan ketika thread berakhir, yang berarti kemampuan GC harus ditingkatkan lagi. Jelas, perbandingan upaya dan hasilnya tidak sepadan. Karena itu, ThreadLocal dirancang agar Map tidak dimiliki oleh ThreadLocal, melainkan oleh Thread itu sendiri. Key-nya adalah variabel ThreadLocal, dan value-nya adalah nilainya. Setiap Thread menyimpan ThreadLoacl yang digunakannya di dalam Map tersebut(tentu saja desain ini juga menimbulkan masalah turunan lain yang tidak dibahas di sini;pembaca yang tertarik dapat mencarinya sendiri)。

Collections

Kelas utilitas Collections dirancang pada versi ini, menyediakan pembungkus koleksi yang aman untuk thread seperti SynchronizedList. Pada masa ketika koleksi yang aman untuk thread hanya mencakup Hashtable, Vector, Stack, dan sejenisnya, kemunculannya juga memiliki arti penting bagi zamannya. Gagasan dasar utilitas Collections adalah: saya membantu Anda membungkus koleksi yang tidak aman untuk thread menjadi koleksi yang aman untuk thread, sehingga pembaruan kode lama Anda tidak perlu memakan banyak waktu. Anda cukup menginisialisasi koleksi dengan metode yang saya sediakan saat membuatnya. Ini mirip teknologi turbo pada mobil, yang meningkatkan daya dan torsi mesin tanpa mengubah kapasitasnya. Era turbo Java telah tiba^\_^

Era transmisi otomatis

JDK 5.0

Mengimpor paket konkurensi

Doug Lea, yang nama Tionghoanya ditulis 道格·利, adalah seorang pengajar universitas di Amerika Serikat dan sosok pakar legendaris; J.U.C adalah hasil karyanya. Sebelum JDK1.5, kita hanya dapat menggunakan synchronized untuk mengendalikan akses konkuren ke kode yang disinkronkan. Saat itu, performa synchronized belum dioptimalkan dengan baik dan masih kurang bagus; untuk mengendalikan thread pun hanya tersedia metode wait dan notify milik Object. Pada masa itulah Doug Lea mengajukan proposal JSR-166 kepada JCP. Sebelum mengajukan JSR-166, Doug Lea sudah lebih dari tiga tahun menggunakan kode dengan fungsi serupa paket J.U.C; kode-kode itulah cikal bakal J.U.C.

J.U.C menyediakan beberapa kategori besar perangkat, seperti objek atomik, kunci dan kumpulan utilitas, kumpulan utas, serta kontainer yang aman untuk banyak utas. Pengembang dapat menggunakan kemampuan mana pun secara fleksibel untuk membangun produk mereka sendiri, baik menggunakan ReentrantLock untuk membangun kerangka kerja dasar maupun langsung menggunakan utilitas atau kontainer siap pakai untuk menulis kode bisnis. Dilihat dari sudut pandang sejarah, pada tahun 2004, J.U.C tanpa perdebatan layak disebut sebagai “produk teknologi mutakhir”. Kontribusinya terhadap penyebaran Java sangat besar. Era transmisi otomatis Java telah tiba;seperti mobil bertransmisi otomatis yang mempermudah orang mengemudi, J.U.C sangat mempermudah pemrogram menggunakan banyak utas. Ini adalah produk yang membuka sebuah era.

Tentu saja, J.U.C juga memiliki beberapa kekurangan:

Beban CPU besar:Jika CAS dalam putaran tunggu terus gagal dalam waktu lama, CPU akan menanggung beban yang sangat besar.

Solusi:Di beberapa bagian JUC, jumlah putaran CAS dibatasi, misalnya pada SynchronousQueue dalam BlockingQueue.

Masalah ABA: Jika suatu nilai awalnya A, berubah menjadi B, lalu kembali menjadi A, pemeriksaan CAS akan mendeteksi bahwa tidak ada perubahan, padahal nilai itu sebenarnya sudah berubah. Inilah masalah ABA. Dalam kebanyakan kasus, masalah ABA tidak memengaruhi ketepatan konkurensi program.

Solusi:tambahkan nomor versi pada setiap variabel dan naikkan sebesar 1 setiap kali terjadi perubahan, sehingga A —> B —> A menjadi 1A —> 2B —> 3A. Java menyediakan AtomicStampedReference untuk mengatasinya. AtomicStampedReference membungkus tuple [E,Integer] untuk memberi cap versi(stamp)pada objek, sehingga menghindari masalah ABA.

Hanya dapat menjamin operasi atomik pada satu variabel bersama:Mekanisme CAS hanya menjamin operasi atomik pada satu variabel, dan tidak dapat menjamin keatomikan seluruh blok kode.

Solusi:misalnya, jika perlu memastikan 3 variabel diperbarui bersama secara atomik, kita harus menggunakan Synchronized. Kita juga dapat mempertimbangkan AtomicReference untuk membungkus beberapa variabel, sehingga dapat menangani situasi dengan beberapa variabel bersama.

Memahami dengan jelas model memori Java

Versi JDK ini memperjelas kembali model memori Java. Sebelumnya, model memori yang umum mencakup model memori konsistensi sekuensial dan model happens-before. Dalam model konsistensi sekuensial, urutan eksekusi program sepenuhnya sama dengan urutan yang terlihat dalam kode. Hal ini sulit dijamin oleh CPU modern yang memiliki banyak inti dan melakukan optimasi eksekusi instruksi. Selain itu, jaminan konsistensi sekuensial sangat membatasi optimasi kode oleh JVM saat program berjalan.

Namun, hubungan terjadi-sebelum (Happens-before) yang ditetapkan oleh spesifikasi JSR 133 pada versi ini membuat urutan eksekusi instruksi menjadi fleksibel:

Dalam thread yang sama, sesuai urutan eksekusi kode(yaitu urutan semantik kode), operasi sebelumnya terjadi mendahului operasi berikutnya Operasi membuka kunci objek monitor terjadi mendahului operasi penguncian berikutnya pada objek monitor yang sama Operasi penulisan pada field volatile terjadi mendahului operasi pembacaan berikutnya pada field tersebut Operasi start pada thread(pemanggilan metode start() objek thread)terjadi mendahului semua operasi lain pada thread itu Semua operasi dalam suatu thread terjadi mendahului pemanggilan metode join() pada thread tersebut oleh thread lain mana pun Jika operasi A terjadi mendahului B, dan operasi B terjadi mendahului C, maka operasi A terjadi mendahului C

Dalam alokasi memori, pemisahan memori kerja masing-masing thread dari memori utama juga memberi JVM keleluasaan besar untuk mengoptimalkan eksekusi instruksi di dalam thread. Variabel dalam memori utama dapat disalin ke memori kerja thread untuk diproses secara terpisah; setelah eksekusi selesai, hasilnya dapat ditulis kembali ke memori utama pada suatu waktu: Namun, bagaimana memastikan konsistensi data antar-thread? Solusi yang diberikan JLS(Java Language Specification)adalah bahwa secara bawaan, konsistensi data pada sembarang waktu tidak dapat dijamin, tetapi konsistensi data dapat dicapai dengan menggunakan kata kunci synchronized, volatile, dan final yang semantiknya telah diperkuat.

Optimasi synchronized pada JDK 6.0

Kata kunci synchronized, sebagai “putra sulung republik”, kalah pamor dari ReentrantLock pada versi 5.0. Versi ini harus membalikkan keadaan, sehingga JDK 6.0 melakukan sejumlah optimasi pada penguncian, seperti pemutaran kunci, eliminasi kunci, penggabungan kunci, kunci ringan, dan kunci berbias. Optimasi kali ini merupakan perwujudan gagasan “pengelolaan terperinci”. Sebelum dioptimalkan, objek yang dikunci dengan synchronized hanya memiliki dua keadaan: tanpa kunci, terkunci(kunci berat). Setelah optimasi, terdapat total 4 keadaan kunci, dengan tingkatan dari rendah ke tinggi sebagai berikut: tanpa kunci, kunci berbias, kunci ringan, kunci berat. Keadaan-keadaan ini meningkat secara bertahap sesuai kondisi persaingan, tetapi tidak dapat diturunkan, dengan tujuan meningkatkan efisiensi perolehan dan pelepasan kunci(menurut penulis, sebenarnya karena terlalu rumit sehingga para pengembang JVM mundur).

Optimalisasi kali ini membuat synchronized akhirnya dapat menegakkan kepala, dan sejak saat itu tak lagi membiarkan orang mengatakan bahwa kinerjanya lebih buruk daripada ReentrantLock. Namun, bagian menariknya masih berlanjut, penguncian bias ditinggalkan pada JDK 15(─.─||). Menurut penulis, synchronized dirugikan karena ia hanyalah sebuah kata kunci, dan JVM bertanggung jawab atas tindakan tingkat bawahnya. JVM harus “menebak” cara penguncian yang paling cocok ketika aplikasi mengunci. ReentrantLock berbeda, ia menyerahkan urusan ini langsung kepada pemrogram. Jika Anda menginginkan keadilan, gunakan kunci adil; jika Anda menginginkan ketidakadilan, gunakan kunci tidak adil. Dari sisi desain, ini bisa dianggap sebagai bentuk kemalasan, tetapi sekaligus memberikan fleksibilitas.

Kerangka kerja Fork/Join JDK 7.0

Lahirnya Fork/Join juga menghadirkan produk yang cukup maju. Keunggulan utamanya adalah mendukung pemecahan tugas secara rekursif sekaligus menggabungkan hasil setiap tugas. Namun, teknologi ini terasa akrab sekaligus asing. Akrab karena diterapkan di berbagai tempat, misalnya CompletableFuture dan Stream yang akan dibahas pada JDK8.0 berikutnya;asing karena kita tampaknya jarang menggunakannya dalam pengembangan aplikasi bisnis。

Bahkan ada yang menganggapnya kurang berguna. Menurut penulis, jika Anda mengembangkan perangkat lunak terkait kebutuhan bisnis, teknologi ini memang kurang berguna karena hampir tidak pernah diperlukan. Untuk skenario dengan data dalam jumlah besar, sudah tersedia perangkat pergudangan data, sedangkan skenario lainnya dapat menggunakan kumpulan thread sebagai pengganti;jika Anda mengembangkan kerangka kerja middleware, teknologi ini bukan sesuatu yang kurang berguna dan mungkin akan digunakan. Di internet berbahasa Mandarin, jarang ada yang mempertanyakan teknologi ini, tetapi di luar negeri sudah ada yang membahasnya. Jika tertarik, Anda dapat langsung membuka tautan untuk membacanya Apakah kerangka kerja Fork-Join di Java bermasalah?

JDK 8.0

Peluncuran versi ini merupakan tonggak bersejarah bagi Java, sampai-sampai versi ini kini digunakan oleh lebih dari separuh program Java yang berjalan di seluruh dunia. Namun, pembaruan terkait multithreading tidak seradikal JDK5.0. Selain menambahkan beberapa objek atomik, dua pembaruan berikut merupakan yang paling menonjol dalam versi ini.

CompletableFuture

Ada banyak pengantar tentang CompletableFuture di internet, yang sebagian besar membahas prinsip kerja dan cara penggunaannya. Namun, ada satu hal yang selalu tidak dipahami penulis: mengapa CompletableFuture muncul ketika sudah ada begitu banyak alat kumpulan utas, dan masalah apa yang diselesaikannya? Apa sebenarnya keunggulan utamanya? Saya yakin jika kamu pernah memikirkannya, kamu juga akan mengajukan pertanyaan ini. Tenang, penulis sudah menemukan jawabannya untukmu.

Kesimpulan:keunggulan utama CompletableFuture adalah Orkestrasi tugas。CompletableFuture mewarisi karakteristik antarmuka Future dan memiliki kemampuan seperti menjalankan tugas secara konkuren; semua kemampuan ini memiliki alternatif. Namun, kemampuan orkestrasi tugasnya tidak tergantikan. API intinya mencakup pembuatan rantai tugas, penggabungan hasil tugas, dan lainnya, yang semuanya dirancang untuk orkestrasi tugas. Karena itu, alasan utama JDK memperkenalkan kerangka kerja ini pada versi tersebut adalah untuk memenuhi kebutuhan orkestrasi tugas yang semakin menyulitkan dalam pengembangan aplikasi bisnis.

Terakhir, satu hal lagi, implementasi tingkat bawah CompletableFuture menggunakan kerangka kerja Fork/Join.

Stream

Buku 《架构整洁之道》 (Clean Architecture) menyebutkan tiga paradigma pemrograman: pemrograman terstruktur (pemrograman prosedural), pemrograman berorientasi objek, dan pemrograman fungsional. Stream merupakan salah satu perwujudan pemrograman fungsional dalam bahasa Java. Menurut penulis, menguasai Stream adalah langkah yang harus dilalui programmer pemula untuk naik ke tingkat menengah. Saat pertama kali mengenal Stream, Anda pasti akan merasa sangat asing, tetapi setelah terbiasa, Anda akan menemukan cara pandang baru dalam pemrograman. Para pengembang sering mencampuradukkan tiga konsep, yaitu pemrograman fungsional, Stream, dan ekspresi Lambda, karena menganggap ketiganya merujuk pada hal yang sama. Berikut pemahaman penulis:

•Pemrograman fungsional adalah suatu paradigma pemrograman yang penerapannya dapat ditemukan dalam berbagai bahasa pemrograman

•Stream adalah fitur baru dalam JDK8.0. Fitur ini juga dapat dipahami sebagai konsep baru yang diperkenalkan untuk mengakomodasi gagasan pemrograman fungsional. Melalui Stream, pemrograman fungsional dapat diterapkan pada kelas koleksi

•Ekspresi Lambda(lambda expression)adalah fungsi anonim yang memungkinkan pemrograman fungsional diungkapkan dengan lebih ringkas dan efisien

Setelah penjelasan panjang ini, apa hubungan Stream dengan multithreading? Metode paralel terkait dalam Stream diimplementasikan menggunakan kerangka kerja Fork/Join pada lapisan dasarnya. Dalam 《Effective Java》 terdapat anjuran terkait, “Gunakan paralelisme Stream dengan hati-hati”, karena seluruh pemrosesan paralel berjalan dalam satu pool Fork/Join bersama. Jika satu pipeline berjalan tidak normal, kinerja bagian lain yang tidak berkaitan dapat terganggu.

JDK 9.0

Memperbaiki mekanisme perebutan kunci

Perebutan kunci membatasi kinerja banyak aplikasi Java multithread. Mekanisme perebutan kunci yang baru meningkatkan kinerja monitor objek Java dan telah divalidasi melalui berbagai pengujian tolok ukur(seperti Volano), yang dapat memperkirakan batas throughput JVM. Dalam praktiknya, mekanisme perebutan kunci yang baru memperoleh hasil luar biasa dalam 22 pengujian tolok ukur yang berbeda. Jika mekanisme baru ini dapat diterapkan dalam Java 9, kinerja aplikasi akan meningkat pesat. Penjelasan sederhananya adalah ketika beberapa thread berebut kunci, sebelum optimasi:thread yang datang belakangan semuanya mengikuti prosedur standar yang sama untuk menunggu kunci. Setelah optimasi:ketika JVM mengenali situasi tertentu yang dapat dioptimalkan, thread yang datang belakangan langsung diizinkan merebut kunci melalui semacam “jalur VIP”.

Untuk penjelasan terperinci, silakan merujuk ke: Penjelasan perebutan kunci – pendekatan kinerja

Aliran reaktif

Aliran reaktif (Reactive Streams) adalah standar untuk menangani aliran data asinkron dengan tekanan balik nonblokir, yang menyediakan sekumpulan antarmuka, metode, dan protokol minimal untuk mendeskripsikan operasi serta entitas yang diperlukan.

Apa yang dimaksud dengan tekanan balik nonblokir?Tekanan balik merupakan singkatan dari back pressure. Sederhananya, produsen mengirimkan data kepada konsumen. Ketika konsumen tidak sanggup memprosesnya lagi, konsumen memberi tahu produsen, lalu produsen menurunkan laju produksinya. Mekanisme ini paling mudah diterapkan dengan cara memblokir, yakni langsung mengembalikan data tekanan saat pengiriman. Penerapan nonblokir menambah kerumitan desain sekaligus meningkatkan kinerja. PS:Saya merasa terjemahan “背压” kurang tepat, maknanya tidak bisa ditebak dari aksaranya. Apakah “反压”(tekanan balik)lebih baik^\_^

Untuk mengatasi kemungkinan konsumen mengalami kegagalan akibat tekanan sumber daya (pressure) yang sangat besar, laju aliran data perlu dikendalikan, yaitu melalui kontrol aliran (flow control), agar aliran data yang cepat tidak membebani target hingga kewalahan. Karena itu diperlukan tekanan balik atau back pressure, dan produsen serta konsumen perlu saling beroperasi melalui penerapan mekanisme tekanan balik. Mekanisme ini harus diterapkan secara asinkron dan nonblokir; jika sinkron dan memblokir, produsen harus menunggu saat konsumen memproses data, yang akan menimbulkan masalah kinerja.

Aliran reaktif(Reactive Streams)menetapkan standar untuk menerapkan tekanan balik nonblokir dengan mendefinisikan sekumpulan entitas, antarmuka, dan metode interoperabilitas. Pihak ketiga mengikuti standar ini untuk menerapkan solusi konkret, yang umum di antaranya adalah Reactor, RxJava, Akka Streams, Ratpack, dan sebagainya.

JDK 10 Thread-Pengendalian Lokal

Safepoint dan kekurangannya:

Safepoint adalah mekanisme dalam Hotspot JVM yang membuat seluruh aplikasi berhenti. Untuk melakukan beberapa pekerjaan tingkat rendah, JVM harus menjalankan Stop The World agar semua thread aplikasi berhenti. Namun, thread tidak boleh dihentikan secara paksa begitu saja;JVM mengirim sinyal instruksi kepada thread aplikasi untuk memberi tahu bahwa sudah waktunya berhenti. Ketika thread aplikasi mencapai sebuah Safepoint, thread akan mengikuti instruksi dan merespons. Itulah alasan namanya Safepoint. Kata safe ditambahkan untuk menekankan bahwa JVM akan melakukan beberapa pekerjaan global secara aman, sehingga titik ini diberi sebutan safe.

Operasi yang memerlukan kondisi aman secara global meliputi: 1、jeda pembersihan sampah 2、deoptimisasi kode(Code deoptimization)。 3、pengosongan cache kode。 4、pendefinisian ulang berkas kelas(Class redefinition, misalnya pembaruan langsung atau instrumentasi)。 5、pencabutan biased lock(Biased lock revocation)。 6、berbagai operasi debug(misalnya: pemeriksaan deadlock atau dump stacktrace)。

Namun, membuat semua thread berhenti di safepoint terdekat saja sudah memerlukan waktu yang cukup lama. Lagi pula, bukankah menghentikan semua thread terkesan terlalu gegabah dan sepihak? Untuk itu, Java10 memperkenalkan cara yang tidak mengharuskan penghentian semua thread, yaitu pengendalian lokal per thread(Thread Local Handshake).

Misalnya, berikut adalah skenario yang dapat ditangani tanpa perlu menghentikan semua thread: 1、Pencabutan biased locking. Untuk mencabut biased locking, cukup menghentikan satu thread, tanpa perlu menghentikan semua thread. 2、Mengurangi dampak latensi VM secara keseluruhan dari berbagai jenis kueri diagnostik, misalnya mengambil jejak tumpukan semua thread pada VM dengan banyak thread Java bisa menjadi operasi yang lambat. 3、Melakukan pengambilan sampel jejak tumpukan yang lebih aman dengan mengurangi ketergantungan pada sinyal(signals). 4、Menggunakan teknik sinkronisasi Dekker asimetris untuk menghilangkan beberapa penghalang memori melalui jabat tangan dengan thread Java. Misalnya, “kode penandaan kartu bersyarat”(conditional card mark code)yang digunakan dalam G1 dan CMS tidak lagi membutuhkan yang disebut “penghalang memori” itu. Dengan demikian, “penghalang penulisan(write barrier)” yang dikeluarkan G1 dapat dioptimalkan, dan cabang-cabang yang mencoba menghindari “penghalang memori” juga dapat dihapus.

JDK 15 menonaktifkan dan menandai penguncian bias sebagai usang

Mengapa biased locking ditinggalkan? Peningkatan kinerja yang dahulu diberikan biased locking kini tidak lagi begitu kentara. Aplikasi yang memperoleh manfaat darinya biasanya adalah program yang menggunakan API koleksi Java awal (JDK 1.1), yang melakukan sinkronisasi pada setiap akses (Hashtable dan Vector). JDK 1.2 memperkenalkan koleksi tanpa sinkronisasi untuk penggunaan satu thread (HashMap dan ArrayList), sementara JDK 1.5 menghadirkan struktur data konkuren dengan kinerja lebih tinggi untuk penggunaan banyak thread. Artinya, jika kode diperbarui agar menggunakan kelas yang lebih baru, aplikasi yang sebelumnya memperoleh manfaat dari biased locking akibat sinkronisasi yang tidak diperlukan mungkin akan mengalami peningkatan kinerja yang besar. Selain itu, aplikasi yang dibangun dengan antrean kumpulan thread dan thread pekerja biasanya memiliki kinerja lebih baik ketika biased locking dinonaktifkan.

Berikut implementasi menggunakan API Hashtable dan Vector: java.lang.Classloader menggunakan Vector java.util.Properties extends Hashtable java.security.Provider extends Properties java.net.URL menggunakan Hashtable java.net.URConnection menggunakan Hashtable java.util.ZipOutputStream menggunakan Vector javax.management.timer.TimerMBean memiliki Vector pada antarmukanya

Era kendaraan otonom

Thread virtual membawa Java memasuki era kemudi otomatis. Banyak bahasa memiliki teknologi yang serupa dengan “thread virtual”, seperti Go, C#, Erlang, Lua, dan sebagainya, yang menyebutnya “coroutine”. Kali ini Java tidak menambahkan kata kunci apa pun, bahkan tidak memperkenalkan konsep baru. Dibandingkan dengan goroutine dan coroutine, thread virtual jauh lebih mudah dipahami;dari namanya saja kita sudah dapat memperkirakan fungsinya.

Thread virtual JDK 19

Model thread dalam Java tradisional memiliki hubungan 1:1 dengan sistem operasi,pembuatan dan pergantian thread memerlukan biaya besar,dan karena dibatasi sistem operasi,jumlah yang dapat dibuat terbatas。Ketika tingkat konkurensi sangat tinggi,tidak mungkin membuat satu thread untuk setiap permintaan。Penggunaan kumpulan thread dapat meringankan masalah,kumpulan thread mengurangi biaya pembuatan thread,tetapi juga tidak dapat meningkatkan jumlah thread。Misalnya tingkat konkurensinya 2000,sedangkan kumpulan thread hanya memiliki 1000 thread,maka pada saat yang sama hanya 1000 permintaan yang dapat diproses,dan masih ada 1000 permintaan yang tidak dapat diproses,permintaan itu dapat ditolak,atau dibiarkan menunggu,hingga ada thread yang tersedia。

Sebelum thread virtual, solusi yang digunakan adalah gaya asinkron. Banyak framework telah menerapkan pemrograman konkurensi bergaya asinkron (seperti Reactor pada Spring5), yang mencapai ketersediaan lebih tinggi melalui pemakaian thread bersama. Prinsipnya adalah mengurangi perpindahan thread melalui pemakaian bersama, sehingga menekan beban sekaligus menghindari pemblokiran. Thread hanya digunakan saat program dieksekusi; ketika program perlu menunggu, thread tidak ditempati. Gaya asinkron memang memberikan cukup banyak peningkatan, tetapi juga memiliki kekurangan. Sebagian besar framework asinkron menggunakan penulisan berantai, yang membagi program menjadi banyak langkah, dan setiap langkah mungkin dijalankan pada thread yang berbeda. Anda tidak lagi dapat menggunakan API pemrograman konkurensi yang sudah familier seperti ThreadLocal, karena hal itu dapat menimbulkan kesalahan. Gaya pemrogramannya juga berubah drastis dan jauh lebih rumit daripada gaya tradisional, sehingga membutuhkan upaya belajar yang besar. Banyak modul yang sudah ada dalam proyek mungkin juga harus diubah agar sesuai dengan pola asinkron.

Prinsip implementasi thread virtual mirip dengan beberapa kerangka asinkron, yakni berbagi thread, sehingga tentu tidak memerlukan pooling. Saat menggunakannya, Anda dapat menganggap thread virtual tersedia dalam jumlah tak terbatas;buatlah sebanyak yang Anda inginkan tanpa perlu khawatir akan masalah. Selain itu, thread virtual mendukung debug dan dapat didukung oleh alat pemantauan Java, hal yang sangat penting. Thread virtual akan sangat mengurangi penggunaan memori program. Semua aplikasi yang intensif I/O, seperti server Web, dapat meningkatkan throughput I/O secara signifikan dengan perangkat keras yang sama. Sebelumnya, memori 1G dapat melayani 1000 akses sekaligus;setelah menggunakan thread virtual, menurut pernyataan resmi, sistem dapat dengan mudah menangani 100 puluh ribu akses serentak. Apakah hal itu dapat didukung dalam skenario bisnis tertentu masih harus dilihat melalui uji beban, tetapi jika kita menurunkan perkiraannya, 10 puluh ribu seharusnya mudah dicapai, tanpa Anda harus mengorbankan apa pun, bahkan mungkin tanpa mengubah kode. Sebab, thread virtual memungkinkan Anda mempertahankan gaya pemrograman tradisional, yakni satu thread untuk satu permintaan. Gunakan thread virtual seperti menggunakan thread biasa;program hanya membutuhkan sedikit perubahan. Thread virtual juga tidak memperkenalkan sintaks baru, sehingga biaya pembelajaran dan migrasinya dapat dikatakan sangat rendah。

Perlu disebutkan bahwa implementasi dasar utas virtual tetap menggunakan kerangka kerja Fork/Join.


Kolom komentar

Mengirim

Pilih tema favorit

Tema pilihan berlaku di seluruh situs.