Fokus artikel ini adalah pada masalah kinerja aplikasi multithreaded. Kami pertama -tama akan mendefinisikan kinerja dan skalabilitas, dan kemudian dengan hati -hati mempelajari aturan Amdahl. Dalam konten berikut, kami akan memeriksa cara menggunakan metode teknis yang berbeda untuk mengurangi kompetisi kunci dan cara mengimplementasikannya dengan kode.
1. Kinerja
Kita semua tahu bahwa multithreading dapat digunakan untuk meningkatkan kinerja program, dan alasan di balik ini adalah karena kami memiliki CPU multi-core atau beberapa CPU. Setiap inti CPU dapat menyelesaikan tugas dengan sendirinya, jadi memecahkan tugas besar menjadi serangkaian tugas kecil yang dapat dijalankan secara independen satu sama lain dapat meningkatkan kinerja keseluruhan program. Anda bisa memberi contoh. Misalnya, ada program yang mengubah ukuran semua gambar dalam folder pada hard disk, dan penerapan teknologi multi-threading dapat meningkatkan kinerjanya. Menggunakan pendekatan berulir tunggal hanya dapat melintasi semua file gambar secara berurutan dan melakukan modifikasi. Jika CPU kami memiliki banyak inti, tidak ada keraguan bahwa ia hanya dapat menggunakan salah satunya. Menggunakan multi-threading, kita dapat meminta utas produser memindai sistem file untuk menambahkan setiap gambar ke antrian, dan kemudian menggunakan beberapa utas pekerja untuk melakukan tugas-tugas ini. Jika jumlah utas pekerja sama dengan jumlah total core CPU, kami dapat memastikan bahwa setiap inti CPU memiliki pekerjaan yang harus dilakukan sampai semua tugas dieksekusi.
Untuk program lain yang membutuhkan lebih banyak IO Waits, kinerja keseluruhan juga dapat ditingkatkan menggunakan teknologi multi-threading. Misalkan kita ingin menulis program seperti itu yang perlu kita perayangi semua file HTML dari situs web tertentu dan menyimpannya di disk lokal. Program ini dapat dimulai dari halaman web tertentu, kemudian menguraikan semua tautan ke situs web ini di halaman web ini, dan kemudian merangkak tautan ini secara bergantian, sehingga ia berulang. Karena perlu beberapa saat untuk menunggu dari saat kami memulai permintaan ke situs web jarak jauh ke saat kami menerima semua data halaman web, kami dapat menyerahkan tugas ini ke beberapa utas untuk dieksekusi. Biarkan satu atau sedikit lebih banyak utas parse halaman HTML yang diterima dan masukkan tautan yang ditemukan ke dalam antrian, meninggalkan semua utas lainnya yang bertanggung jawab untuk meminta halaman. Berbeda dengan contoh sebelumnya, dalam contoh ini, Anda masih bisa mendapatkan peningkatan kinerja bahkan jika Anda menggunakan lebih banyak utas daripada jumlah core CPU.
Dua contoh di atas memberi tahu kami bahwa kinerja tinggi adalah melakukan sebanyak mungkin hal dalam jendela waktu singkat. Ini tentu saja penjelasan paling klasik dari istilah kinerja. Tetapi pada saat yang sama, menggunakan utas juga dapat meningkatkan kecepatan respons program kami dengan baik. Bayangkan kami memiliki aplikasi antarmuka grafis seperti itu, dengan kotak input di atas dan tombol bernama "proses" di bawah kotak input. Ketika pengguna menekan tombol ini, aplikasi perlu membuat ulang status tombol (tombol tampaknya ditekan, dan kembali ke keadaan aslinya ketika tombol mouse kiri dilepaskan), dan mulai memproses input pengguna. Jika tugas ini memakan waktu untuk memproses input pengguna, program satu threaded tidak akan dapat terus menanggapi tindakan input pengguna lain, seperti pengguna mengklik acara mouse atau penunjuk mouse yang memindahkan acara yang dikirimkan dari sistem operasi, dll. Tanggapan terhadap peristiwa ini perlu menjadi utas independen untuk merespons.
Skalabilitas berarti bahwa program memiliki kemampuan untuk mendapatkan kinerja yang lebih tinggi dengan menambahkan sumber daya komputasi. Bayangkan bahwa kita perlu menyesuaikan ukuran banyak gambar, karena jumlah inti CPU dari mesin kita terbatas, meningkatkan jumlah utas tidak selalu meningkatkan kinerja yang sesuai. Sebaliknya, karena penjadwal perlu bertanggung jawab atas pembuatan dan penutupan lebih banyak utas, itu juga akan menempati sumber daya CPU, yang dapat mengurangi kinerja.
1.1 Aturan Amdahl
Paragraf sebelumnya menyebutkan bahwa dalam beberapa kasus, menambahkan sumber daya komputasi tambahan dapat meningkatkan kinerja keseluruhan program. Untuk menghitung berapa banyak peningkatan kinerja yang bisa kita dapatkan ketika kita menambahkan sumber daya tambahan, perlu untuk memeriksa bagian mana dari program yang dijalankan secara seri (atau sinkron) dan bagian mana yang dijalankan secara paralel. Jika kita mengukur proporsi kode yang perlu dieksekusi secara serempak ke B (misalnya, jumlah baris kode yang perlu dieksekusi secara serempak) dan mencatat jumlah total inti CPU sebagai n, maka menurut undang -undang Amdahl, batas atas perbaikan kinerja yang dapat kita peroleh adalah:
Jika N cenderung tak terhingga, (1-B)/N konvergen ke 0. Oleh karena itu, kita dapat mengabaikan nilai ekspresi ini, sehingga jumlah bit peningkatan kinerja menyatu menjadi 1/b, di mana B mewakili proporsi kode yang harus dijalankan secara serempak. Jika B sama dengan 0,5, itu berarti bahwa setengah dari kode program tidak dapat berjalan secara paralel, dan timbal balik 0,5 adalah 2, jadi bahkan jika kami menambahkan inti CPU yang tak terhitung jumlahnya, kami mendapatkan peningkatan kinerja maksimal 2x. Misalkan kita telah memodifikasi program sekarang, dan setelah modifikasi, hanya kode 0,25 yang harus dijalankan secara serempak. Sekarang 1/0.25 = 4 berarti bahwa jika program kami berjalan pada perangkat keras dengan sejumlah besar CPU, itu akan sekitar 4 kali lebih cepat daripada pada perangkat keras inti tunggal.
Di sisi lain, melalui undang -undang Amdahl, kami juga dapat menghitung proporsi kode sinkronisasi bahwa program tersebut harus didasarkan pada target speedup yang ingin kami peroleh. Jika kita ingin mencapai kecepatan 100 kali, dan 1/100 = 0,01 berarti jumlah kode maksimum yang dijalankan oleh program kami secara sinkron tidak dapat melebihi 1%.
Untuk meringkas undang -undang Amdahl, kita dapat melihat bahwa peningkatan kinerja maksimum yang kita dapatkan dengan menambahkan CPU tambahan tergantung pada seberapa kecil proporsi program mengeksekusi bagian dari kode secara serempak. Meskipun pada kenyataannya, tidak selalu mudah untuk menghitung rasio ini, apalagi menghadapi beberapa aplikasi sistem komersial besar, undang -undang Amdahl memberi kita inspirasi penting, yaitu, kita harus mempertimbangkan kode yang harus dieksekusi secara sinkron dan mencoba mengurangi bagian kode ini.
1.2 Efek pada kinerja
Seperti yang ditulis artikel di sini, kami telah membuat poin bahwa menambahkan lebih banyak utas dapat meningkatkan kinerja program dan responsif. Tetapi di sisi lain, tidak mudah untuk mencapai manfaat ini, dan juga membutuhkan beberapa harga. Penggunaan utas juga akan memengaruhi peningkatan kinerja.
Pertama, dampak pertama berasal dari saat penciptaan utas. Selama pembuatan utas, JVM perlu berlaku untuk sumber daya yang sesuai dari sistem operasi yang mendasarinya dan menginisialisasi struktur data dalam penjadwal untuk menentukan urutan utas eksekusi.
Jika jumlah utas Anda sama dengan jumlah inti CPU, setiap utas akan berjalan pada inti sehingga mereka mungkin tidak sering terganggu. Tetapi pada kenyataannya, ketika program Anda berjalan, sistem operasi juga akan memiliki beberapa operasinya sendiri yang perlu diproses oleh CPU. Jadi, bahkan dalam hal ini, utas Anda akan terganggu dan menunggu sistem operasi untuk melanjutkan operasinya. Ketika jumlah utas Anda melebihi jumlah core CPU, situasinya mungkin menjadi lebih buruk. Dalam hal ini, penjadwal proses JVM akan mengganggu utas tertentu untuk memungkinkan utas lain dieksekusi. Saat utas diaktifkan, keadaan saat ini dari utas yang sedang berjalan perlu disimpan sehingga status data dapat dipulihkan lain kali dijalankan. Tidak hanya itu, penjadwal juga akan memperbarui struktur data internalnya sendiri, yang juga membutuhkan siklus CPU. Semua ini berarti bahwa pengalihan konteks antara utas mengkonsumsi sumber daya komputasi CPU, sehingga membawa overhead kinerja dibandingkan dengan yang dalam satu kasus berulir.
Overhead lain yang dibawa oleh program multithreaded berasal dari perlindungan akses sinkron dari data bersama. Kita dapat menggunakan kata kunci yang disinkronkan untuk perlindungan sinkronisasi, atau kita dapat menggunakan kata kunci yang mudah menguap untuk berbagi data antara beberapa utas. Jika lebih dari satu utas ingin mengakses struktur data bersama, suatu pertengkaran akan terjadi. Pada saat ini, JVM perlu memutuskan proses mana yang pertama dan proses mana yang ada di belakang. Jika utas yang akan dieksekusi bukan utas yang sedang berjalan, switching utas terjadi. Utas saat ini perlu menunggu sampai berhasil memperoleh objek kunci. JVM dapat memutuskan bagaimana melakukan "tunggu" ini. Jika JVM berharap lebih pendek untuk berhasil memperoleh objek yang terkunci, JVM dapat menggunakan metode tunggu yang agresif, seperti terus -menerus mencoba memperoleh objek yang terkunci sampai berhasil. Dalam hal ini, metode ini mungkin lebih efisien, karena masih lebih cepat untuk membandingkan switching konteks proses. Memindahkan utas menunggu kembali ke antrian eksekusi juga akan membawa overhead tambahan.
Oleh karena itu, kita harus mencoba yang terbaik untuk menghindari switching konteks yang disebabkan oleh kompetisi kunci. Bagian berikut akan menjelaskan dua cara untuk mengurangi terjadinya kompetisi tersebut.
1.3 Kompetisi Kunci
Seperti yang disebutkan di bagian sebelumnya, akses yang bersaing ke kunci oleh dua atau lebih utas akan membawa overhead komputasi tambahan karena kompetisi terjadi untuk memaksa penjadwal memasuki keadaan menunggu yang agresif, atau membiarkannya melakukan keadaan menunggu, menyebabkan dua sakelar konteks. Ada beberapa kasus di mana konsekuensi dari kompetisi kunci dapat dikurangi oleh:
1. Kurangi lingkup kunci;
2. Kurangi frekuensi kunci yang perlu diperoleh;
3. Coba gunakan operasi kunci optimis yang didukung oleh perangkat keras daripada disinkronkan;
4. Cobalah untuk menggunakan disinkronkan sesedikit mungkin;
5. Kurangi penggunaan cache objek
1.3.1 Mengurangi domain sinkronisasi
Jika kode memegang kunci untuk lebih dari yang diperlukan, maka metode pertama ini dapat diterapkan. Biasanya kita dapat memindahkan satu atau lebih baris kode dari area sinkronisasi untuk mengurangi waktu utas saat ini menahan kunci. Semakin sedikit kode yang berjalan di area sinkronisasi, semakin awal utas saat ini akan melepaskan kunci, memungkinkan utas lain untuk memperoleh kunci sebelumnya. Ini konsisten dengan hukum Amdahl, karena melakukannya mengurangi jumlah kode yang perlu dieksekusi secara serempak.
Untuk pemahaman yang lebih baik, lihat kode sumber berikut:
ReducelockDuration kelas publik mengimplementasikan runnable {private static final int number_of_threads = 5; peta akhir private static <string, integer> peta = hashmap baru <string, integer> (); public void run () {for (int i = 0; i <10000; i ++) {disinkronkan (peta) {uuid randomuuid = uuid.randomuuid (); Nilai integer = integer.valueof (42); String key = randomuuid.toString (); peta.put (tombol, nilai); } Thread.yield (); }} public static void main (string [] args) melempar interruptedException {thread [] threads = utas baru [number_of_threads]; untuk (int i = 0; i <number_of_threads; i ++) {threads [i] = utas baru (reduceLockDuration baru ()); } Long startMillis = system.currentTimeMillis (); untuk (int i = 0; i <number_of_threads; i ++) {threads [i] .start (); } untuk (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrentTimeMillis ()-StartMillis)+"ms"); }}Dalam contoh di atas, kami membiarkan lima utas bersaing untuk mengakses instance peta bersama. Untuk hanya satu utas yang dapat mengakses instance peta secara bersamaan, kami menempatkan operasi penambahan kunci/nilai ke peta ke dalam blok kode yang dilindungi yang disinkronkan. Ketika kami dengan hati -hati melihat kode ini, kami dapat melihat bahwa beberapa kalimat kode yang menghitung kunci dan nilai tidak perlu dieksekusi secara serempak. Kunci dan nilainya hanya milik utas yang saat ini menjalankan kode ini. Ini hanya bermakna bagi utas saat ini dan tidak akan dimodifikasi oleh utas lain. Oleh karena itu, kita dapat memindahkan kalimat -kalimat ini dari perlindungan sinkronisasi. sebagai berikut:
public void run () {for (int i = 0; i <10000; i ++) {uuid randomuuid = uuid.randomuuid (); Nilai integer = integer.valueof (42); String key = randomuuid.toString (); sinkronisasi (peta) {peta.put (tombol, nilai); } Thread.yield (); }}Efek mengurangi kode sinkronisasi dapat diukur. Di mesin saya, waktu eksekusi seluruh program dikurangi dari 420ms menjadi 370ms. Lihatlah, hanya memindahkan tiga baris kode dari blok perlindungan sinkronisasi dapat mengurangi waktu menjalankan program sebesar 11%. Kode thread.yield () adalah untuk menginduksi switching konteks utas, karena kode ini akan memberi tahu JVM bahwa utas saat ini ingin menyerahkan sumber daya komputasi yang saat ini digunakan sehingga utas lain yang menunggu untuk dijalankan dapat dijalankan. Ini juga akan menyebabkan lebih banyak kompetisi kunci, karena jika ini bukan masalahnya, utas akan menempati inti tertentu lebih lama, sehingga mengurangi switching konteks utas.
1.3.2 Kunci terpisah
Cara lain untuk mengurangi kompetisi kunci adalah dengan menyebarkan blok kode yang dilindungi kunci ke sejumlah blok perlindungan yang lebih kecil. Metode ini akan berfungsi jika Anda menggunakan kunci dalam program Anda untuk melindungi beberapa objek yang berbeda. Misalkan kami ingin menghitung beberapa data melalui program, dan mengimplementasikan kelas hitungan sederhana untuk memegang beberapa indikator statistik yang berbeda, dan mewakili mereka dengan variabel penghitungan dasar (tipe panjang). Karena program kami multi-threaded, kami perlu melindungi operasi yang mengakses variabel-variabel ini secara sinkron, karena tindakan ini berasal dari utas yang berbeda. Cara termudah untuk mencapai hal ini adalah dengan menambahkan kata kunci yang disinkronkan ke setiap fungsi yang mengakses variabel -variabel ini.
kelas public static counteronelock mengimplementasikan Counter {private long customercount = 0; Private Long ShippingCount = 0; public disinkronkan void incrementcustomer () {customercount ++; } public yang disinkronkan void incrementshipping () {ShippingCount ++; } publik yang disinkronkan getCustomerCount () {return customercount; } public yang disinkronkan long getshippingCount () {return shippingCount; }}Ini berarti bahwa setiap modifikasi dari variabel -variabel ini akan menyebabkan penguncian ke instance counter lainnya. Jika utas lain ingin memanggil metode kenaikan pada variabel lain yang berbeda, mereka hanya dapat menunggu utas sebelumnya untuk melepaskan kontrol kunci sebelum mereka memiliki kesempatan untuk menyelesaikannya. Dalam hal ini, menggunakan perlindungan tersinkronisasi terpisah untuk setiap variabel yang berbeda akan meningkatkan efisiensi eksekusi.
Public Static Class CounterSeParateLock mengimplementasikan Counter {Private Static Final Object customerLock = new Object (); Private Static Final Object ShippingLock = Objek Baru (); Private Long CustomerCount = 0; Private Long ShippingCount = 0; public void incrementCustomer () {disinkronkan (customerLock) {customercount ++; }} public void incrementshipping () {disinkronkan (pengiriman) {ShippingCount ++; }} public long getCustomerCount () {disinkronkan (customerLock) {return customercount; }} public long getShippingCount () {disinkronkan (pengiriman) {return shippingCount; }}}Implementasi ini memperkenalkan objek yang disinkronkan terpisah untuk setiap metrik jumlah. Oleh karena itu, ketika utas ingin meningkatkan jumlah pelanggan, ia harus menunggu utas lain yang meningkatkan jumlah pelanggan untuk menyelesaikan, daripada menunggu utas lain yang meningkatkan jumlah pengiriman untuk menyelesaikannya.
Dengan menggunakan kelas -kelas berikut, kami dapat dengan mudah menghitung peningkatan kinerja yang dibawa oleh kunci split.
Kelas Publik Locksplitting mengimplementasikan runnable {private static final int number_of_threads = 5; konter pribadi; counter antarmuka publik {void incrementcustomer (); void incrementshipping (); long getCustomerCount (); long getshippingcount (); } public static class counterOnelock mengimplementasikan counter {...} public static class counterseParateLock mengimplementasikan counter {...} public locksplitting (counter counter) {this.counter = counter; } public void run () {for (int i = 0; i <100000; i ++) {if (threadlocalrandom.current (). nextBoolean ()) {counter.incrementCustomer (); } else {counter.incrementshipping (); }}} public static void main (string [] args) melempar interruptedException {thread [] threads = utas baru [number_of_threads]; Counter counter = counteronelock baru (); untuk (int i = 0; i <number_of_threads; i ++) {threads [i] = utas baru (locksplitting baru (counter)); } Long startMillis = system.currentTimeMillis (); untuk (int i = 0; i <number_of_threads; i ++) {threads [i] .start (); } untuk (int i = 0; i <number_of_threads; i ++) {threads [i] .join (); } System.out.println ((System.CurrentTimeMillis () - StartMillis) + "ms"); }}Pada mesin saya, metode implementasi satu kunci tunggal membutuhkan rata -rata 56 ms, dan implementasi dua kunci terpisah adalah 38 ms. Memakan waktu berkurang sekitar 32%.
Cara lain untuk meningkatkan adalah bahwa kita bahkan dapat melangkah lebih jauh untuk melindungi membaca dan menulis dengan kunci yang berbeda. Kelas kontra asli masing -masing menyediakan metode untuk membaca dan menulis indikator penghitungan. Namun, pada kenyataannya, operasi baca tidak memerlukan perlindungan sinkronisasi. Kita dapat yakin bahwa beberapa utas dapat membaca nilai indikator saat ini secara paralel. Pada saat yang sama, operasi tulis harus dilindungi secara sinkron. Paket Java.util.concurrent menyediakan implementasi antarmuka ReadWritelock, yang dapat dengan mudah mencapai perbedaan ini.
Implementasi ReentrantReadWritelock mempertahankan dua kunci yang berbeda, satu melindungi operasi baca dan yang lainnya melindungi operasi penulisan. Kedua kunci memiliki operasi untuk memperoleh dan melepaskan kunci. Kunci tulis hanya dapat berhasil diperoleh ketika tidak ada yang mendapatkan kunci baca. Sebaliknya, selama kunci tulis tidak diperoleh, kunci baca dapat diperoleh oleh banyak utas secara bersamaan. Untuk menunjukkan pendekatan ini, kelas kontra berikut menggunakan ReadWritelock, sebagai berikut:
kelas statis public counterreadwritelock mengimplementasikan Counter {private final reentrantreadwritelock customerlock = baru reentrantreadwritelock (); Private Final Lockwritelock = customerlock.writelock (); kunci final private customeReadlock = customerlock.readlock (); final private reentrantreadwritelock pengirimanlock = baru reentrantreadwritelock (); Private Final Lock ShippingWritelock = ShippingLock.Writelock (); Private Final Lock ShippingReadlock = ShippingLock.readlock (); Private Long CustomerCount = 0; Private Long ShippingCount = 0; public void incrementcustomer () {customerwritelock.lock (); CustomerCount ++; customerwritelock.unlock (); } public void incrementshipping () {ShippingWritelock.lock (); pengirimancount ++; Shippingwritelock.unlock (); } public long getCustomerCount () {customereReadlock.lock (); jumlah panjang = customercount; customerreadlock.unlock (); jumlah pengembalian; } public long getShippingCount () {dryringReadlock.lock (); Hitungan Panjang = Pengiriman COUNT; ShippingReadlock.unlock (); jumlah pengembalian; }}Semua operasi yang dibaca dilindungi oleh kunci baca, dan semua operasi tulis dilindungi oleh kunci tulis. Jika operasi yang dibaca yang dilakukan dalam program ini jauh lebih besar daripada operasi penulisan, implementasi ini dapat membawa peningkatan kinerja yang lebih besar daripada bagian sebelumnya karena operasi yang dibaca dapat dilakukan secara bersamaan.
1.3.3 Kunci Pemisahan
Contoh di atas menunjukkan cara memisahkan satu kunci ke dalam beberapa kunci terpisah sehingga setiap utas bisa mendapatkan kunci objek yang akan mereka modifikasi. Tetapi di sisi lain, metode ini juga meningkatkan kompleksitas program, dan dapat menyebabkan kebuntuan jika diterapkan secara tidak tepat.
Kunci detasemen adalah metode yang mirip dengan kunci detasemen, tetapi kunci detasemen adalah menambahkan kunci untuk melindungi cuplikan atau objek kode yang berbeda, sedangkan kunci detasemen adalah menggunakan kunci yang berbeda untuk melindungi rentang nilai yang berbeda. ConcurrenthashMap di Java.util.util. Dalam hal implementasi, ConcurrenthashMap menggunakan 16 kunci yang berbeda secara internal, alih -alih merangkum hashmap yang dilindungi secara sinkron. Masing-masing dari 16 kunci bertanggung jawab untuk melindungi akses sinkron ke sepersepuluh bit ember (ember). Dengan cara ini, ketika utas yang berbeda ingin memasukkan kunci ke dalam segmen yang berbeda, operasi yang sesuai akan dilindungi oleh kunci yang berbeda. Tapi itu juga akan membawa beberapa masalah buruk, seperti penyelesaian operasi tertentu sekarang membutuhkan banyak kunci, bukan satu kunci. Jika Anda ingin menyalin seluruh peta, semua 16 kunci perlu diperoleh untuk diselesaikan.
1.3.4 Operasi Atom
Cara lain untuk mengurangi kompetisi kunci adalah dengan menggunakan operasi atom, yang akan menguraikan prinsip -prinsip dalam artikel lain. Paket java.util.current menyediakan kelas yang dienkapsulasi secara atom untuk beberapa tipe data dasar yang umum digunakan. Implementasi kelas operasi atom didasarkan pada fungsi "perbandingan permutasi" (CAS) yang disediakan oleh prosesor. Operasi CAS hanya akan melakukan operasi pembaruan ketika nilai register saat ini sama dengan nilai lama yang disediakan oleh operasi.
Prinsip ini dapat digunakan untuk meningkatkan nilai variabel dengan cara yang optimis. Jika utas kami mengetahui nilai saat ini, ia akan mencoba menggunakan operasi CAS untuk melakukan operasi kenaikan. Jika utas lain telah memodifikasi nilai variabel selama periode ini, nilai arus yang disediakan oleh utas berbeda dari nilai riil. Pada saat ini, JVM mencoba mendapatkan kembali nilai saat ini dan mencoba lagi, mengulanginya lagi sampai berhasil. Meskipun operasi looping akan menyia -nyiakan beberapa siklus CPU, manfaat dari melakukan ini adalah bahwa kita tidak memerlukan segala bentuk kontrol sinkronisasi.
Implementasi kelas kontra di bawah ini menggunakan operasi atom. Seperti yang Anda lihat, tidak ada kode yang disinkronkan yang digunakan.
kelas public static counteratomic mengimplementasikan Counter {private atomiclong customercount = new atomiclong (); Atomiclong ShippingCount pribadi = atomiclong baru (); public void incrementcustomer () {customercount.incrementandget (); } public void incrementshipping () {ShippingCount.incrementandget (); } public long getCustomerCount () {return customercount.get (); } public long getShippingCount () {return shippingCount.get (); }}Dibandingkan dengan kelas counterseparateLock, waktu berjalan rata -rata telah berkurang dari 39ms menjadi 16ms, yaitu sekitar 58%.
1.3.5 Hindari segmen kode hotspot
Daftar Implementasi yang khas mencatat jumlah elemen yang terkandung dalam daftar itu sendiri dengan mempertahankan variabel dalam konten. Setiap kali elemen dihapus atau ditambahkan dari daftar, nilai variabel ini akan berubah. Jika daftar digunakan dalam aplikasi tunggal, metode ini dapat dimengerti. Setiap kali Anda menelepon ukuran (), Anda bisa mengembalikan nilai setelah perhitungan terakhir. Jika variabel penghitungan ini tidak dipertahankan secara internal berdasarkan daftar, setiap panggilan ke size () akan menyebabkan daftar untuk melakukan traverse dan menghitung jumlah elemen.
Metode optimasi yang digunakan oleh banyak struktur data ini akan menjadi masalah ketika berada di lingkungan multi-utas. Misalkan kita berbagi daftar di antara banyak utas, dan beberapa utas secara bersamaan menambahkan atau menghapus elemen ke dalam daftar, dan meminta panjang yang besar. Pada saat ini, variabel jumlah dalam daftar menjadi sumber daya bersama, sehingga semua akses ke sana harus diproses secara serempak. Oleh karena itu, variabel penghitungan menjadi hot spot di seluruh implementasi daftar.
Cuplikan kode berikut menunjukkan masalah ini:
Public Static Class CarrepositoryWithCounter mengimplementasikan carrepository {private Map <String, Car> cars = new HashMap <String, car> (); peta pribadi <string, car> truk = hashmap baru <string, car> (); objek pribadi carcountsync = objek baru (); carcount int private = 0; public void addCar (mobil mobil) {if (car.getLicencePlate (). startswith ("c")) {disinkronkan (cars) {car foundcar = cars.get (car.getlicencePlate ()); if (foundCar == null) {cars.put (car.getlicencePlate (), car); disinkronkan (carcountsync) {carcount ++; }}}} else {disinkronkan (truk) {car foundCar = trucks.get (car.getLicencePlate ()); if (foundCar == null) {trucks.put (car.getLicencePlate (), car); disinkronkan (carcountsync) {carcount ++; }}}}}} public int getCarcount () {disinkronkan (carcountsync) {return carcount; }}}Implementasi Carrepository di atas memiliki dua variabel daftar di dalamnya, satu digunakan untuk menempatkan elemen cuci mobil dan yang lainnya digunakan untuk menempatkan elemen truk. Pada saat yang sama, ia menyediakan metode untuk menanyakan ukuran total dari dua daftar ini. Metode optimasi yang digunakan adalah bahwa setiap kali elemen mobil ditambahkan, nilai variabel penghitungan internal akan ditingkatkan. Pada saat yang sama, operasi bertambah dilindungi oleh disinkronkan, dan hal yang sama berlaku untuk mengembalikan nilai jumlah.
Untuk menghindari overhead sinkronisasi kode tambahan ini, lihat implementasi lain dari Carrepository di bawah ini: ia tidak lagi menggunakan variabel penghitungan internal, tetapi menghitung nilai ini secara real time dalam metode mengembalikan jumlah total mobil. sebagai berikut:
Public Static Class CarrepositoryWithOutCounter mengimplementasikan carrepository {private Map <String, car> cars = new HashMap <string, car> (); peta pribadi <string, car> truk = hashmap baru <string, car> (); public void addCar (mobil mobil) {if (car.getLicencePlate (). startswith ("c")) {disinkronkan (cars) {car foundcar = cars.get (car.getlicencePlate ()); if (foundCar == null) {cars.put (car.getlicencePlate (), car); }}} else {disinkronkan (truk) {car foundcar = trucks.get (car.getLicencePlate ()); if (foundCar == null) {trucks.put (car.getLicencePlate (), car); }}}}} int int getCarcount () {disinkronkan (cars) {disinkronkan (truk) {return cars.size () + trucks.size (); }}}}Sekarang, hanya dalam metode getCarcount (), akses kedua daftar perlu perlindungan sinkronisasi. Seperti implementasi sebelumnya, overhead sinkronisasi setiap kali elemen baru ditambahkan tidak ada lagi.
1.3.6 Hindari penggunaan kembali cache objek
Dalam versi pertama Java VM, overhead menggunakan kata kunci baru untuk membuat objek baru relatif tinggi, sehingga banyak pengembang terbiasa menggunakan mode reuse objek. Untuk menghindari pembuatan objek berulang -ulang lagi dan lagi, pengembang mempertahankan kumpulan buffer. Setelah setiap pembuatan instance objek, mereka dapat disimpan di kumpulan buffer. Lain kali utas lain perlu menggunakannya, mereka dapat diambil langsung dari kumpulan buffer.
Sekilas, metode ini sangat masuk akal, tetapi pola ini dapat menyebabkan masalah dalam aplikasi multithreaded. Karena kumpulan buffer objek dibagi di antara banyak utas, semua operasi utas saat mengakses objek di dalamnya membutuhkan perlindungan sinkron. Overhead dari sinkronisasi ini lebih besar dari penciptaan objek itu sendiri. Tentu saja, membuat terlalu banyak objek akan meningkatkan beban pengumpulan sampah, tetapi bahkan memperhitungkannya, masih lebih baik untuk menghindari peningkatan kinerja yang dibawa dengan menyinkronkan kode daripada menggunakan kumpulan cache objek.
Skema optimasi yang dijelaskan dalam artikel ini sekali lagi menunjukkan bahwa setiap metode optimasi yang mungkin harus dievaluasi dengan cermat ketika itu benar -benar diterapkan. Solusi optimasi yang belum matang tampaknya masuk akal di permukaan, tetapi pada kenyataannya itu kemungkinan akan menjadi hambatan kinerja pada gilirannya.