Terlepas dari apakah Anda mengikuti atau tidak, aplikasi web Java menggunakan kumpulan utas untuk menangani permintaan ke tingkat yang lebih besar atau lebih kecil. Rincian implementasi kumpulan utas dapat diabaikan, tetapi perlu untuk memahami cepat atau lambat penggunaan dan penyetelan kumpulan benang. Artikel ini terutama memperkenalkan penggunaan kumpulan utas java dan cara mengkonfigurasi kumpulan utas dengan benar.
Tunggal berulir
Mari kita mulai dengan dasar -dasarnya. Tidak peduli server atau kerangka kerja aplikasi mana (seperti Tomcat, Jetty, dll.) Digunakan, mereka memiliki implementasi dasar yang serupa. Dasar layanan web adalah soket, yang bertanggung jawab untuk mendengarkan port, menunggu koneksi TCP, dan menerima koneksi TCP. Setelah koneksi TCP diterima, data dapat dibaca dan dikirim dari koneksi TCP yang baru dibuat.
Untuk memahami proses di atas, kami tidak menggunakan server aplikasi apa pun secara langsung, tetapi membangun layanan web sederhana dari awal. Layanan ini adalah mikrokosmos dari sebagian besar server aplikasi. Layanan web tunggal yang sederhana terlihat seperti ini:
Serversocket listener = New ServerSocket (8080); coba {while (true) {socket socket = listener.accept (); coba {handleRequest (soket); } catch (ioException e) {e.printstacktrace (); }}} akhirnya {listener.close ();}Kode di atas membuat soket server (ServerSocket) , mendengarkan port 8080, dan kemudian loop untuk memeriksa soket untuk melihat apakah ada koneksi baru. Setelah koneksi baru diterima, soket akan diteruskan ke metode handlerequest. Metode ini mem -parsing aliran data ke dalam permintaan HTTP, merespons, dan menulis data respons. Dalam contoh sederhana ini, metode HandLeRequest hanya mengimplementasikan bacaan dalam aliran data dan mengembalikan data respons sederhana. Secara umum implementasi, metode ini akan jauh lebih kompleks, seperti membaca data dari database, dll.
Respons string statis akhir = "http/1.0 200 ok/r/n" + "tipe konten: teks/polos/r/n" + "/r/n" + "halo dunia/r/n"; public static void handleRequest (soket soket) melempar ioException {// Baca aliran input, dan return "200 OK" coba {bufferedReader di = BufferedReader baru (inputStreamReader baru (socket.getInputStream ()))); log.info (in.readline ()); OutputStream out = socket.getoutputStream (); out.write (response.getbytes (standardcharsets.utf_8)); } akhirnya {socket.close (); }}Karena hanya ada satu utas untuk memproses permintaan, setiap permintaan harus menunggu permintaan sebelumnya diproses sebelum dapat ditanggapi. Dengan asumsi waktu respons permintaan adalah 100 milidetik, jumlah respons per detik (TPS) server ini hanya 10.
Multi-threaded
Meskipun metode HandLeRequest dapat memblokir pada IO, CPU masih dapat menangani lebih banyak permintaan. Tetapi dalam satu kasus berulir, ini tidak dapat dilakukan. Oleh karena itu, kemampuan pemrosesan paralel server dapat ditingkatkan dengan membuat metode multi-threading.
kelas public static handleRequestrunnable mengimplementasikan runnable {final socket socket; public handleRequestrunnable (soket soket) {this.socket = socket; } public void run () {coba {handleRequest (soket); } catch (ioException e) {e.printstacktrace (); }}} Serversocket listener = new ServerSocket (8080); coba {while (true) {socket socket = listener.accept (); utas baru (handleRequestrunnable baru (soket)). start (); }} akhirnya {listener.close ();}Di sini, metode ACCECT () masih dipanggil di utas utama, tetapi begitu koneksi TCP dibuat, utas baru akan dibuat untuk menangani permintaan baru, yaitu untuk menjalankan metode handlerequest dalam teks sebelumnya di utas baru.
Dengan membuat utas baru, utas utama dapat terus menerima koneksi TCP baru, dan permintaan ini dapat diproses secara paralel. Metode ini disebut "satu utas per permintaan". Tentu saja, ada cara lain untuk meningkatkan kinerja pemrosesan, seperti model yang digerakkan oleh peristiwa asinkron yang digunakan oleh nginx dan node.js, tetapi mereka tidak menggunakan kumpulan benang dan karenanya tidak dicakup oleh artikel ini.
Dalam setiap permintaan satu implementasi utas, membuat overhead utas (dan penghancuran berikutnya) sangat mahal karena JVM dan sistem operasi perlu mengalokasikan sumber daya. Selain itu, implementasi di atas juga memiliki masalah, yaitu, jumlah utas yang dibuat tidak dapat dikendalikan, yang dapat menyebabkan sumber daya sistem dengan cepat habis.
Sumber daya yang kelelahan
Setiap utas membutuhkan sejumlah ruang memori tumpukan. Dalam JVM 64-bit terbaru, ukuran tumpukan default adalah 1024kb. Jika server menerima sejumlah besar permintaan, atau metode HandLeRequest dijalankan secara perlahan, server mungkin macet karena membuat sejumlah besar utas. Misalnya, ada 1000 permintaan paralel, dan 1000 utas yang dibuat perlu menggunakan 1GB memori JVM sebagai ruang tumpukan utas. Selain itu, objek yang dibuat selama pelaksanaan kode masing -masing utas juga dapat dibuat di heap. Jika situasi ini memburuk, itu akan melebihi memori tumpukan JVM dan menghasilkan sejumlah besar operasi pengumpulan sampah, yang pada akhirnya akan menyebabkan memori overflow (OutofmemoryErrors).
Utas ini tidak hanya mengkonsumsi memori, mereka juga menggunakan sumber daya terbatas lainnya, seperti pegangan file, koneksi basis data, dll. Utas pembuatan yang tidak terkendali juga dapat menyebabkan jenis kesalahan dan kerusakan lainnya. Oleh karena itu, cara penting untuk menghindari kelelahan sumber daya adalah dengan menghindari struktur data yang tidak terkendali.
Ngomong -ngomong, karena masalah memori yang disebabkan oleh ukuran tumpukan benang, ukuran tumpukan dapat disesuaikan melalui sakelar -XSS. Setelah mengurangi ukuran tumpukan utas, overhead per benang dapat dikurangi, tetapi stack overflow (Stackoverflowerrors) dapat dinaikkan. Untuk aplikasi umum, 1024kB default terlalu kaya, dan mungkin lebih tepat untuk menguranginya menjadi 256kb atau 512kB. Nilai minimum yang diizinkan di Java adalah 160kb.
Kumpulan benang
Untuk menghindari pembuatan utas baru yang berkelanjutan, Anda dapat membatasi batas atas kumpulan utas dengan menggunakan kumpulan utas sederhana. Thread Pool mengelola semua utas. Jika jumlah utas belum mencapai batas atas, kumpulan utas membuat utas ke batas atas dan menggunakan kembali utas bebas sebanyak mungkin.
ServerSocket listener = New ServerSocket (8080); ExecutorService Executor = executors.newfixedThreadPool (4); coba {while (true) {socket socket = listener.accept (); executor.submit (handleRequestrunnable baru (soket)); }} akhirnya {listener.close ();}Dalam contoh ini, alih -alih membuat utas secara langsung, ExecutorService digunakan. Ini mengirimkan tugas yang perlu dieksekusi (perlu mengimplementasikan antarmuka RunNables) ke kumpulan utas dan menjalankan kode menggunakan utas di kumpulan utas. Dalam contoh, kumpulan utas ukuran tetap dengan sejumlah utas 4 digunakan untuk memproses semua permintaan. Ini membatasi jumlah utas yang menangani permintaan dan juga membatasi penggunaan sumber daya.
Selain membuat kumpulan utas ukuran tetap melalui metode NewFixEdThreadPool, kelas Executors juga menyediakan metode NewCachedThreadPool. Menggunakan kembali kumpulan utas masih dapat menyebabkan jumlah utas yang tidak terkendali, tetapi akan menggunakan benang idle yang telah dibuat sebelumnya sebanyak mungkin. Biasanya jenis kumpulan benang ini cocok untuk tugas pendek yang tidak diblokir oleh sumber daya eksternal.
Antrian kerja
Setelah menggunakan kumpulan utas ukuran tetap, jika semua utas sibuk, apa yang akan terjadi jika permintaan lain datang? ThreadPoolExecutor menggunakan antrian untuk memegang permintaan yang tertunda, dan kumpulan utas ukuran tetap menggunakan daftar tertaut tanpa batas secara default. Perhatikan bahwa ini pada gilirannya dapat menyebabkan masalah kelelahan sumber daya, tetapi itu tidak akan terjadi selama kecepatan pemrosesan utas lebih besar dari tingkat pertumbuhan antrian. Kemudian pada contoh sebelumnya, setiap permintaan antrian akan menampung soket, yang dalam beberapa sistem operasi akan mengkonsumsi pegangan file. Karena sistem operasi membatasi jumlah pegangan file yang dibuka oleh proses, yang terbaik adalah membatasi ukuran antrian kerja.
public static ExecutorService newBoundedFixedThreadPool(int nThreads, int capacity) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(capacity), new ThreadPoolExecutor.DiscardPolicy());}public static void boundedThreadPoolServerSocket() melempar ioException {serversocket listener = New ServerSocket (8080); ExecutorService Executor = newboundedFixedThreadPool (4, 16); coba {while (true) {socket socket = listener.accept (); executor.submit (handleRequestrunnable baru (soket)); }} akhirnya {listener.close (); }}Di sini, alih -alih secara langsung menggunakan metode Executors.newfixedThreadPool untuk membuat kumpulan utas, kami membangun objek ThreadPoolExecutor sendiri dan membatasi panjang antrian kerja menjadi 16 elemen.
Jika semua utas sibuk, tugas baru akan diisi ke dalam antrian. Karena antrian membatasi ukuran menjadi 16 elemen, jika batas ini terlampaui, ia perlu ditangani oleh parameter terakhir saat membangun objek ThreadPoolExecutor. Dalam contoh, discardpolicy digunakan, yaitu, ketika antrian mencapai batas atas, tugas baru akan dibuang. Selain pertama kalinya, ada juga kebijakan abort (abortpolicy) dan kebijakan eksekusi penelepon (callerrunspolicy). Yang pertama akan melempar pengecualian, sedangkan yang terakhir akan menjalankan tugas di utas penelepon.
Untuk aplikasi web, kebijakan default optimal harus meninggalkan atau membatalkan kebijakan dan mengembalikan kesalahan kepada klien (seperti kesalahan HTTP 503). Tentu saja, dimungkinkan juga untuk menghindari meninggalkan permintaan klien dengan meningkatkan panjang antrian kerja, tetapi permintaan pengguna umumnya tidak mau menunggu lama, dan ini akan mengkonsumsi lebih banyak sumber daya server. Tujuan dari antrian kerja bukan untuk menanggapi permintaan klien tanpa batas, tetapi untuk menghaluskan dan meledak permintaan. Biasanya, antrian kerja harus kosong.
Penyetelan Hitung Thread
Contoh sebelumnya menunjukkan cara membuat dan menggunakan kumpulan utas, tetapi masalah inti dengan menggunakan kumpulan utas adalah berapa banyak utas yang harus digunakan. Pertama, kita perlu memastikan bahwa ketika batas utas tercapai, sumber daya tidak akan habis. Sumber daya di sini termasuk memori (heap and stack), jumlah pegangan file terbuka, jumlah koneksi TCP, jumlah koneksi basis data jarak jauh, dan sumber daya terbatas lainnya. Secara khusus, jika tugas berulir intensif secara komputasi, jumlah core CPU juga merupakan salah satu keterbatasan sumber daya. Secara umum, jumlah utas tidak boleh melebihi jumlah core CPU.
Karena pemilihan jumlah utas tergantung pada jenis aplikasi, mungkin perlu banyak pengujian kinerja sebelum hasil yang optimal dapat diperoleh. Tentu saja, Anda juga dapat meningkatkan kinerja aplikasi Anda dengan meningkatkan jumlah sumber daya. Misalnya, memodifikasi ukuran memori heap JVM, atau memodifikasi batas atas pegangan file sistem operasi, dll. Kemudian, penyesuaian ini pada akhirnya akan mencapai batas atas teoritis.
Hukum Sedikit
Hukum Little menggambarkan hubungan antara tiga variabel dalam sistem yang stabil.
Di mana L mewakili jumlah rata -rata permintaan, λ mewakili frekuensi permintaan, dan W mewakili waktu rata -rata untuk menanggapi permintaan tersebut. Misalnya, jika jumlah permintaan per detik adalah 10 dan setiap waktu pemrosesan permintaan adalah 1 detik, maka setiap saat ada 10 permintaan yang sedang diproses. Kembali ke topik kami, itu membutuhkan 10 utas untuk diproses. Jika waktu pemrosesan satu permintaan berlipat ganda, jumlah utas yang diproses juga akan berlipat ganda, menjadi 20.
Setelah memahami dampak dari waktu pemrosesan pada efisiensi pemrosesan permintaan, kami akan menemukan bahwa batas atas teoritis mungkin bukan nilai optimal untuk ukuran kumpulan benang. Batas atas kumpulan utas juga membutuhkan waktu pemrosesan tugas referensi.
Dengan asumsi bahwa JVM dapat memproses 1000 tugas secara paralel, jika setiap waktu pemrosesan permintaan tidak melebihi 30 detik, maka dalam kasus terburuk, paling banyak 33,3 permintaan per detik dapat diproses. Namun, jika setiap permintaan hanya membutuhkan 500 milidetik, aplikasi dapat memproses 2000 permintaan per detik.
Pool benang terpisah
Dalam layanan mikro atau arsitektur berorientasi layanan (SOA), akses ke beberapa layanan backend biasanya diperlukan. Jika salah satu layanan melakukan terdegradasi, itu dapat menyebabkan kumpulan utas kehabisan benang, yang memengaruhi permintaan ke layanan lain.
Cara yang efektif untuk menangani kegagalan layanan backend adalah dengan mengisolasi kumpulan utas yang digunakan oleh setiap layanan. Dalam mode ini, masih ada kumpulan utas yang dikirim yang mengirimkan tugas ke kumpulan utas permintaan backend yang berbeda. Kolam utas ini mungkin tidak memiliki beban karena backend yang lambat, dan mentransfer beban ke kumpulan utas yang meminta backend lambat.
Selain itu, mode pengumpulan multi-threaded juga perlu menghindari masalah kebuntuan. Jika setiap utas memblokir sambil menunggu hasil dari permintaan yang tidak diproses, kebuntuan terjadi. Oleh karena itu, dalam mode kolam multithreaded, perlu untuk memahami tugas yang dieksekusi oleh setiap kumpulan utas dan ketergantungan di antara mereka, untuk menghindari masalah kebuntuan sebanyak mungkin.
Meringkaskan
Bahkan jika kumpulan utas tidak digunakan secara langsung dalam aplikasi, mereka cenderung digunakan secara tidak langsung oleh server aplikasi atau kerangka kerja dalam aplikasi. Kerangka kerja seperti Tomcat, JBoss, Undertow, Dropwizard, dll. Semua menyediakan opsi untuk menyetel kumpulan utas (kumpulan utas yang digunakan oleh eksekusi servlet).
Saya harap artikel ini dapat meningkatkan pemahaman Anda tentang kumpulan utas dan membantu Anda belajar.