1. Pendahuluan multi-threading
Dalam pemrograman, kami tidak dapat menghindari masalah pemrograman multi-threading, karena pemrosesan bersamaan diperlukan di sebagian besar sistem bisnis. Jika dalam skenario bersamaan, multi-threading sangat penting. Selain itu, selama wawancara kami, pewawancara biasanya mengajukan pertanyaan kepada kami tentang multi-threading, seperti: bagaimana membuat utas? Kami biasanya menjawab dengan cara ini, ada dua metode utama. Yang pertama adalah: mewarisi kelas utas dan menulis ulang metode run; Yang kedua adalah: menerapkan antarmuka runnable dan menulis ulang metode run. Maka pewawancara pasti akan bertanya apa kelebihan dan kerugian dari kedua metode ini. Tidak peduli apa, kita akan sampai pada kesimpulan, yaitu cara penggunaan kedua, karena menganjurkan objek yang lebih sedikit warisan dan mencoba menggunakan kombinasi sebanyak mungkin.
Pada saat ini, kita mungkin juga memikirkan apa yang harus dilakukan jika kita ingin mendapatkan nilai pengembalian multi-thread? Berdasarkan pengetahuan yang telah kami pelajari lebih banyak, kami akan berpikir untuk mengimplementasikan antarmuka yang dapat dipanggil dan menulis ulang metode panggilan. Bagaimana banyak utas digunakan dalam proyek aktual? Berapa banyak cara yang mereka miliki?
Pertama, mari kita lihat contoh:
Ini adalah metode sederhana untuk membuat multi-thread, yang mudah dimengerti. Dalam contoh, menurut skenario bisnis yang berbeda, kita dapat meneruskan parameter yang berbeda ke thread () untuk mengimplementasikan berbagai logika bisnis. Namun, masalah yang diekspos dengan metode ini membuat multi-thread adalah membuat utas berulang kali, dan harus dihancurkan setelah membuat utas. Jika persyaratan untuk skenario bersamaan rendah, metode ini tampaknya baik-baik saja, tetapi dalam skenario konkurensi yang tinggi, metode ini tidak dimungkinkan, karena membuat utas sangat memakan sumber daya. Jadi menurut pengalaman, cara yang benar adalah dengan menggunakan Teknologi Pool Thread. JDK menyediakan berbagai jenis kumpulan benang untuk kami pilih. Untuk metode tertentu, Anda dapat memeriksa dokumentasi JDK.
Apa yang perlu kami perhatikan dalam kode ini adalah bahwa parameter yang dilewati mewakili jumlah utas yang kami konfigurasikan. Apakah semakin baik? Pasti tidak. Karena ketika mengkonfigurasi jumlah utas, kita harus sepenuhnya mempertimbangkan kinerja server. Jika ada lebih banyak konfigurasi utas, kinerja server mungkin tidak bagus. Biasanya, perhitungan yang diselesaikan oleh mesin ditentukan oleh jumlah utas. Ketika jumlah utas mencapai puncak, perhitungan tidak dapat dilakukan. Jika logika bisnis yang mengkonsumsi CPU (lebih banyak perhitungan), jumlah utas dan inti akan mencapai puncaknya. Jika logika bisnis yang mengkonsumsi I/O (basis data operasi, mengunggah file, mengunduh, dll.), Semakin banyak utas, semakin banyak utas, itu akan membantu meningkatkan kinerja dalam arti tertentu.
Formula lain untuk mengatur jumlah utas:
Y = n*((a+b)/a), di mana n: Jumlah inti CPU, A: Waktu perhitungan program ketika utas dieksekusi, B: waktu pemblokiran program ketika utas dieksekusi. Dengan rumus ini, konfigurasi jumlah utas dari kumpulan utas akan dibatasi, dan kami dapat mengonfigurasinya secara fleksibel sesuai dengan situasi aktual mesin.
2. Optimalisasi Multithreaded dan Perbandingan Kinerja
Teknologi threading digunakan dalam proyek terbaru, dan saya mengalami banyak masalah selama penggunaan. Mengambil keuntungan dari popularitas, saya akan memilah perbandingan kinerja dari beberapa kerangka kerja multi-threaded. Yang telah kami kuasai secara kasar dibagi menjadi tiga jenis: Tipe pertama: ThreadPool (Thread Pool) + CountdownLatch (Program Counter), Jenis Kedua: Kerangka kerja Fork/Join, dan jenis ketiga dari aliran paralel JDK8. Berikut adalah ringkasan komparatif dari kinerja multi-threading dari metode ini.
Pertama, asumsikan skenario bisnis di mana beberapa objek file dihasilkan dalam memori. Di sini, 30.000 Thread Sleep secara tentatif ditentukan untuk mensimulasikan logika bisnis pemrosesan bisnis untuk membandingkan kinerja multi-threading dari metode ini.
1) Single Threaded
Metode ini sangat sederhana, tetapi program ini sangat memakan waktu selama pemrosesan dan akan digunakan untuk waktu yang lama, karena setiap utas menunggu utas saat ini dieksekusi sebelum akan dieksekusi. Ini tidak ada hubungannya dengan multi-threads, sehingga efisiensinya sangat rendah.
Pertama -tama buat objek file, kodenya adalah sebagai berikut:
kelas publik fileInfo {private string filename; // nama file private string filetype; // file type private string filesize; // file ukuran private string filemd5; // code md5 private string fileversionno; // nomor versi file public fileInfo () {super (); } public fileInfo (string fileName, string filetype, string filesize, string filemd5, string fileversionno) {super (); this.filename = fileName; this.filetype = filetype; this.filesize = fileseze; this.filemd5 = filemd5; this.fileversionno = fileversionno; } public String getFileName () {return fileName; } public void setFileName (string fileName) {this.fileName = fileName; } public string getFileType () {return filetype; } public void setFileType (string filetype) {this.fileType = filetype; } public string getFileSize () {return filessize; } public void setFileSize (string filesize) {this.filesize = filesize; } public string getFilemD5 () {return filemd5; } public void setFileMd5 (String filemd5) {this.filemd5 = filemd5; } public string getFileVonsionNo () {return fileversionno; } public void setFilEversionNo (string fileversionno) {this.filevonsionno = fileversionno; }Kemudian, simulasikan pemrosesan bisnis, buat 30.000 objek file, tidur utas untuk 1ms, dan set 1000ms sebelumnya, dan menemukan bahwa waktunya sangat lama, dan seluruh gerhana macet, jadi ubah waktu menjadi 1ms.
Tes Kelas Publik {Private Static List <FILEInfo> FileList = ArrayList baru <FILEInfo> (); public static void main (string [] args) melempar InterruptedException {createFileInfo (); Long StartTime = System.CurrentTimeMillis (); untuk (fileInfo fi: fileList) {thread.sleep (1); } long endtime = system.currentTimeMillis (); System.out.println ("Single Thread yang memakan waktu:"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {fileList.add (FileInfo baru ("Foto depan kartu ID", "jpg", "101522", "md5"+i, "1")); }}}Hasil tes adalah sebagai berikut:
Dapat dilihat bahwa menghasilkan 30.000 objek file membutuhkan waktu lama, hampir 1 menit, dan efisiensinya relatif rendah.
2) Threadpool (Thread Pool) +Countdownlatch (Program Counter)
Seperti namanya, Countdownlatch adalah penghitung utas. Proses eksekusi adalah sebagai berikut: Pertama, metode AWAIT () dipanggil di utas utama, dan utas utama diblokir, dan kemudian penghitung program diteruskan ke objek utas sebagai parameter. Akhirnya, setelah setiap utas selesai mengeksekusi tugas, metode Countdown () dipanggil untuk menunjukkan penyelesaian tugas. Setelah Countdown () dieksekusi beberapa kali, utas utama menunggu () akan tidak valid. Proses implementasi adalah sebagai berikut:
Public Class Test2 {Private Static ExecutorService Executor = Executors.newfixedThreadPool (100); Private Static CountdownLatch Countdownlatch = Countdownlatch baru (100); Daftar statis pribadi <FILEINFO> FileList = ArrayList baru <FILEINFO> (); Daftar statis pribadi <Daftar <FILEINFO>> DAFTAR = NEW ARRAYLIST <> (); public static void main (string [] args) melempar InterruptedException {createFileInfo (); addlist (); Long StartTime = System.CurrentTimeMillis (); int i = 0; untuk (daftar <FILEInfo> fi: list) {executor.submit (new filerunnable (countDownlatch, fi, i)); i ++; } countDownlatch.Await (); Long Endtime = System.CurrentTimeMillis (); executor.shutdown (); System.out.println (i+"Threads Butuh waktu:"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {fileList.add (FileInfo baru ("Foto kartu ID depan", "jpg", "101522", "md5"+i, "1")); }} private static void addList () {for (int i = 0; i <100; i ++) {list.add (fileList); }}}Kelas Filerunnable:
/** * pemrosesan multithreaded * @author wangsj * * @param <t> */kelas publik filerunnable <t> mengimplementasikan runnable {countdownlatch private countDownlatch; Daftar Pribadi <T> Daftar; int private i; public filerunnable (Countdownlatch Countdownlatch, daftar <T> Daftar, int i) {super (); this.countDownlatch = Countdownlatch; this.list = daftar; this.i = i; } @Override public void run () {for (t t: list) {try {thread.sleep (1); } catch (InterruptedException e) {E.PrintStackTrace (); } countDownlatch.countdown (); }}}Hasil tes adalah sebagai berikut:
3) Kerangka kerja garpu/bergabung
JDK dimulai dengan versi 7, dan kerangka kerja Fork/Joint muncul. Dari perspektif literal, Fork berpisah dan bergabung adalah merger, jadi gagasan kerangka kerja ini adalah. Pisahkan tugas melalui Fork, dan kemudian bergabung untuk menggabungkan hasil setelah karakter split dieksekusi dan diringkas. Misalnya, kami ingin menghitung beberapa angka yang ditambahkan terus menerus, 2+4+5+7 =? , Bagaimana cara menggunakan kerangka kerja garpu/bergabung untuk menyelesaikannya? Idenya adalah untuk membagi tugas molekuler. Kami dapat membagi operasi ini menjadi dua subtugas, satu menghitung 2+4 dan yang lainnya menghitung 5+7. Ini adalah proses garpu. Setelah perhitungan selesai, hasil perhitungan kedua subtugas ini dirangkum dan jumlah diperoleh. Ini adalah proses bergabung.
Gagasan Eksekusi Kerangka kerja/bergabung: Pertama, bagi tugas dan gunakan kelas Fork untuk membagi tugas besar menjadi beberapa subtugas. Proses segmentasi ini perlu ditentukan sesuai dengan situasi aktual sampai tugas yang dibagi cukup kecil. Kemudian, kelas bergabung mengeksekusi tugas, dan subtugas yang terbagi berada dalam antrian yang berbeda. Beberapa utas mendapatkan tugas dari antrian dan menjalankannya. Hasil eksekusi ditempatkan dalam antrian terpisah. Akhirnya, utas dimulai, hasilnya diperoleh dalam antrian dan hasilnya digabungkan.
Beberapa kelas digunakan untuk menggunakan kerangka kerja garpu/bergabung. Untuk penggunaan kelas, Anda dapat merujuk ke JDK API. Menggunakan kerangka kerja ini, Anda perlu mewarisi kelas forkjointask. Biasanya, Anda hanya perlu mewarisi rekursivetask atau rekursiveAction subkelasnya. Recursivetask digunakan untuk adegan dengan hasil pengembalian, dan RecursiveAction digunakan untuk adegan tanpa hasil pengembalian. Eksekusi forkjointask membutuhkan eksekusi forkjoinpool, yang digunakan untuk mempertahankan subtugas yang terbagi yang ditambahkan ke antrian tugas yang berbeda.
Ini kode implementasinya:
Test kelas publik3 {Private Static List <FILEInfo> FileList = ArrayList baru <FILEINFO> (); // Private Static Forkjoinpool forkjoinpool = new forkjoinpool (100); // pekerjaan statis privat <filinfo> Job = pekerjaan baru <> (FileList.Size ()/100, FILELIST <FILINFO> PEKERJAAN BARU <> (Filelist.Size ()/100, FILELIST <FILINFO> Pekerjaan baru <> (FileList.Size ()/100, FILELIST <FILINFO> public static void main (string [] args) {createFileInfo (); Long StartTime = System.CurrentTimeMillis (); Forkjoinpool forkjoinpool = new forkjoinpool (100); // Pisahkan pekerjaan tugas <FILEINFO> JOB = Pekerjaan Baru <> (FileList.Size ()/100, FileList); // kirimkan tugas dan kembalikan hasil forkjointask <integer> fjtresult = forkjoinpool.submit (pekerjaan); // blokir while (! Job.isdone ()) {System.out.println ("Tugas selesai!"); } long endtime = system.currentTimeMillis (); System.out.println ("Fork/Joint Framework waktu yang memakan waktu:"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {fileList.add (FileInfo baru ("Foto kartu ID depan", "jpg", "101522", "md5"+i, "1")); }}}/** * Jalankan kelas tugas * @Author wangsj * */Pekerjaan kelas publik <T> memperluas Recursivetask <Integer> {private static final long serialVersionuid = 1L; Hitungan int pribadi; Daftar Pribadi <T> JOBLIST; Pekerjaan Publik (Int Count, Daftar <T> JOBLIST) {super (); this.count = count; this.JoBlist = JOBLIST; } /*** Jalankan tugas, mirip dengan metode run yang mengimplementasikan antarmuka runnable* /@Override dilindungi integer compute () {// belah tugas if (joBlist.size () <= count) {executeJob (); return joBlist.size (); } else {// Lanjutkan membuat tugas sampai dapat didekomposisi dan dieksekusi daftar <Recursivetask <long>> fork = new LinkedList <Recursivetask <long>> (); // Pisahkan tugas nukleik, di sini metode dikotomi digunakan int countjob = joBlist.size ()/2; Daftar <T> LeftList = JOBLIST.Sublist (0, CountJob); Daftar <T> RightList = JOBLIST.Sublist (CountJob, JOBLIST.SIZE ()); // Tetapkan tugas pekerjaan leftjob = pekerjaan baru <> (hitung, leftList); Job rightJob = pekerjaan baru <> (Count, RightList); // jalankan tugas leftjob.fork (); rightjob.fork (); return integer.parseint (leftjob.join (). tostring ()) +integer.parseint (rightjob.join (). tostring ()); }} / *** Jalankan metode tugas* / private void executeJob () {for (t job: joBlist) {coba {thread.sleep (1); } catch (InterruptedException e) {E.PrintStackTrace (); }}}Hasil tes adalah sebagai berikut:
4) streaming paralel JDK8
Aliran paralel adalah salah satu fitur baru JDK8. Idenya adalah untuk mengubah aliran yang dieksekusi secara berurutan menjadi aliran bersamaan, yang diimplementasikan dengan memanggil metode paralel (). Aliran paralel membagi aliran menjadi beberapa blok data, menggunakan utas yang berbeda untuk memproses aliran blok data yang berbeda, dan akhirnya menggabungkan hasil pemrosesan dari setiap blok aliran data, mirip dengan kerangka kerja fork/gabungan.
Aliran paralel menggunakan forkjoinpool kumpulan utas publik secara default. Jumlah utas adalah nilai default yang digunakan. Menurut jumlah inti mesin, kita dapat menyesuaikan ukuran utas dengan tepat. Menyesuaikan jumlah utas dicapai dengan cara berikut.
System.setProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");Berikut ini adalah proses implementasi kode, yang sangat sederhana:
test kelas publik4 {Daftar statis privat <FILEInfo> FileList = ArrayList baru <FILEInfo> (); public static void main (string [] args) {// system.setProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100"); createFileInfo (); Long StartTime = System.CurrentTimeMillis (); filelist.parallelstream (). foreach (e -> {try {thread.sleep (1);} catch (interruptedException f) {f.printstacktrace ();}}); Long Endtime = System.CurrentTimeMillis (); System.out.println ("JDK8 Waktu streaming paralel:"+(endtime-starttime)+"ms");} private static void createFileInfo () {for (int i = 0; i <30000; i ++) {fileList.add (fileinfo baru ("foto depan id depan kartu "," jpg "," 101522 "," md5 "+i," 1 ")); }}}Berikut ini adalah tes. Jumlah kumpulan utas tidak diatur untuk pertama kalinya. Default digunakan. Hasil tes adalah sebagai berikut:
Kami melihat bahwa hasilnya tidak terlalu ideal dan membutuhkan waktu lama. Selanjutnya, atur jumlah kumpulan utas, yaitu, tambahkan kode berikut:
System.setProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");Kemudian tes dilakukan, dan hasilnya adalah sebagai berikut:
Kali ini butuh lebih sedikit waktu dan sangat ideal.
3. Ringkasan
Untuk meringkas situasi di atas, menggunakan satu utas sebagai referensi, yang memakan waktu terpanjang adalah kerangka kerja garpu/gabungan asli. Meskipun jumlah kumpulan utas dikonfigurasi di sini, aliran paralel JDK8 dengan jumlah kumpulan utas lebih buruk. Streaming paralel mengimplementasikan kode ini sederhana dan mudah dimengerti, dan kami tidak perlu menulis ekstra untuk loop. Kita dapat menyelesaikan semua metode paralelstream, dan jumlah kode sangat berkurang. Faktanya, lapisan yang mendasari streaming paralel masih merupakan kerangka kerja garpu/bergabung, yang mengharuskan kita untuk menggunakan berbagai teknologi secara fleksibel selama proses pengembangan untuk membedakan kelebihan dan kerugian dari berbagai teknologi, sehingga dapat melayani kita dengan lebih baik.