LongAdder dan LongAccumulator di Java

1. Gambaran keseluruhan

Dalam artikel ini, kita akan melihat dua konstruk dari java.util.concurrent pakej: LongAdder dan LongAccumulator.

Kedua-duanya diciptakan untuk menjadi sangat cekap dalam persekitaran multi-utas dan kedua-duanya menggunakan taktik yang sangat pintar untuk bebas kunci dan tetap selamat di dalam benang.

2. LongAdder

Mari kita fikirkan beberapa logik yang sering meningkatkan beberapa nilai, di mana menggunakan AtomicLong boleh menjadi hambatan. Ini menggunakan operasi membandingkan dan menukar, yang - dalam pertikaian berat - dapat menyebabkan banyak kitaran CPU terbuang.

LongAdder , di sisi lain, menggunakan helah yang sangat pintar untuk mengurangkan perbalahan antara benang, ketika ini menambahnya.

Apabila kita ingin menambah contoh LongAdder, kita perlu memanggil kaedah kenaikan () . Pelaksanaan itu menyimpan pelbagai kaunter yang dapat tumbuh berdasarkan permintaan .

Oleh itu, apabila lebih banyak utas memanggil kenaikan () , susunan akan lebih panjang. Setiap rekod dalam array boleh dikemas kini secara berasingan - mengurangkan perbalahan. Kerana kenyataan itu, LongAdder adalah cara yang sangat cekap untuk menambah penghitung dari beberapa utas.

Mari buat contoh kelas LongAdder dan kemas kini dari pelbagai utas :

LongAdder counter = new LongAdder(); ExecutorService executorService = Executors.newFixedThreadPool(8); int numberOfThreads = 4; int numberOfIncrements = 100; Runnable incrementAction = () -> IntStream .range(0, numberOfIncrements) .forEach(i -> counter.increment()); for (int i = 0; i < numberOfThreads; i++) { executorService.execute(incrementAction); }

Hasil kaunter di LongAdder tidak tersedia sehingga kita memanggil kaedah jumlah () . Kaedah itu akan mengulang semua nilai array bawah, dan menjumlahkan nilai-nilai yang mengembalikan nilai yang sepatutnya. Kita perlu berhati-hati kerana kaedah panggilan ke jumlah () boleh sangat mahal:

assertEquals(counter.sum(), numberOfIncrements * numberOfThreads);

Kadang-kadang, setelah kita memanggil jumlah () , kita ingin membersihkan semua keadaan yang dikaitkan dengan contoh LongAdder dan mula menghitung dari awal. Kita boleh menggunakan kaedah sumThenReset () untuk mencapainya:

assertEquals(counter.sumThenReset(), numberOfIncrements * numberOfThreads); assertEquals(counter.sum(), 0);

Perhatikan bahawa panggilan berikutnya ke kaedah jumlah () mengembalikan sifar yang bermaksud bahawa keadaan berjaya diset semula.

Selain itu, Java juga menyediakan DoubleAdder untuk mengekalkan penjumlahan nilai ganda dengan API yang serupa dengan LongAdder.

3. Pengumpul Panjang

LongAccumulator juga merupakan kelas yang sangat menarik - yang membolehkan kita menerapkan algoritma bebas kunci dalam beberapa senario. Sebagai contoh, ia boleh digunakan untuk mengumpulkan hasil mengikut LongBinaryOperator yang disediakan - ini berfungsi sama dengan operasi mengurangkan () dari Stream API.

Contoh LongAccumulator dapat dibuat dengan membekalkan LongBinaryOperator dan nilai awal kepada pembangunnya. Yang penting untuk diingat bahawa LongAccumulator akan berfungsi dengan betul jika kita membekalkannya dengan fungsi komutatif di mana urutan pengumpulan tidak menjadi masalah.

LongAccumulator accumulator = new LongAccumulator(Long::sum, 0L);

Kami membuat LongAccumulator whi ch akan menambah nilai baru kepada nilai yang sudah dalam penumpuk. Kami menetapkan nilai awal LongAccumulator ke sifar, jadi dalam panggilan pertama kaedah akumulasi () , nilai sebelumnya akan mempunyai nilai sifar.

Mari kita gunakan kaedah mengumpulkan () dari pelbagai utas:

int numberOfThreads = 4; int numberOfIncrements = 100; Runnable accumulateAction = () -> IntStream .rangeClosed(0, numberOfIncrements) .forEach(accumulator::accumulate); for (int i = 0; i < numberOfThreads; i++) { executorService.execute(accumulateAction); }

Perhatikan bagaimana kita memberikan nombor sebagai argumen kepada kaedah mengumpulkan () . Kaedah itu akan memanggil fungsi sum () kita .

The LongAccumulator menggunakan pelaksanaan membandingkan-dan-swap - yang membawa kepada ini semantik menarik.

Pertama, ia melaksanakan tindakan yang ditakrifkan sebagai LongBinaryOperator, dan kemudian memeriksa apakah Nilai sebelumnya berubah. Sekiranya ia diubah, tindakan tersebut akan dilaksanakan lagi dengan nilai baru. Sekiranya tidak, ia berjaya mengubah nilai yang disimpan di dalam penumpuk.

Kita sekarang dapat menegaskan bahawa jumlah semua nilai dari semua lelaran adalah 20200 :

assertEquals(accumulator.get(), 20200);

Menariknya, Java juga menyediakan DoubleAccumulator dengan tujuan dan API yang sama tetapi untuk nilai berganda .

4. Jalur Dinamik

Semua implementasi penambah dan penumpuk di Java diwarisi dari kelas asas menarik yang disebut Striped64. Daripada hanya menggunakan satu nilai untuk mengekalkan keadaan semasa, kelas ini menggunakan pelbagai keadaan untuk menyebarkan pertikaian ke lokasi memori yang berbeza.

Berikut adalah gambaran ringkas mengenai apa yang dilakukan oleh Striped64 :

Benang yang berbeza mengemas kini lokasi memori yang berbeza. Oleh kerana kita menggunakan susunan keadaan (iaitu, garis) keadaan, idea ini disebut jalur dinamik. Menariknya, Striped64 dinamai idea ini dan fakta bahawa ia berfungsi pada jenis data 64-bit.

Kami mengharapkan jalur dinamik dapat meningkatkan prestasi keseluruhan. Walau bagaimanapun, cara JVM memperuntukkan negeri-negeri ini mungkin memberi kesan yang tidak produktif.

Untuk lebih spesifik, JVM mungkin memperuntukkan negeri-negeri tersebut berdekatan satu sama lain di timbunan. Ini bermaksud bahawa beberapa keadaan dapat berada dalam baris cache CPU yang sama. Oleh itu, mengemas kini satu lokasi memori boleh menyebabkan cache ketinggalan ke keadaan berdekatan . Fenomena ini, yang dikenali sebagai perkongsian palsu, akan merugikan persembahan .

Untuk mengelakkan perkongsian palsu. yang Striped64 pelaksanaan menambah padding cukup di setiap negeri memastikan bahawa setiap negeri tetap berada di dalam talian cache sendiri:

The @Contended anotasi bertanggungjawab untuk menambah padding ini. Padding meningkatkan prestasi dengan mengorbankan penggunaan memori yang lebih banyak.

5. Kesimpulan

Dalam tutorial ringkas ini, kami melihat LongAdder dan LongAccumulator dan kami telah menunjukkan cara menggunakan kedua-dua konstruk tersebut untuk melaksanakan penyelesaian yang sangat cekap dan bebas kunci.

Pelaksanaan semua contoh dan coretan kod ini terdapat dalam projek GitHub - ini adalah projek Maven, jadi mudah untuk diimport dan dijalankan sebagaimana adanya.