LongAdder dan LongAccumulator di Java

1. Ikhtisar

Pada artikel ini, kita akan melihat dua konstruksi dari paket java.util.concurrent : LongAdder dan LongAccumulator.

Keduanya dibuat agar sangat efisien dalam lingkungan multi-utas dan keduanya memanfaatkan taktik yang sangat cerdas agar bebas kunci dan tetap aman utas.

2. LongAdder

Mari kita pertimbangkan beberapa logika yang sangat sering menaikkan beberapa nilai, di mana menggunakan AtomicLong dapat menjadi penghambat. Ini menggunakan operasi bandingkan-dan-tukar, yang - dalam perselisihan berat - dapat menyebabkan banyak siklus CPU yang terbuang percuma.

LongAdder , di sisi lain, menggunakan trik yang sangat cerdik untuk mengurangi perselisihan di antara utas, saat ini meningkatkannya.

Saat kita ingin menambah sebuah instance dari LongAdder, kita perlu memanggil metode increment () . Implementasi itu menyimpan berbagai penghitung yang dapat tumbuh sesuai permintaan .

Jadi, ketika lebih banyak thread yang memanggil increment () , arraynya akan lebih panjang. Setiap record dalam larik dapat diperbarui secara terpisah - mengurangi perselisihan. Karena fakta itu, LongAdder adalah cara yang sangat efisien untuk meningkatkan penghitung dari beberapa utas.

Mari buat instance kelas LongAdder dan perbarui dari banyak 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 penghitung di LongAdder tidak tersedia sampai kita memanggil metode sum () . Metode itu akan mengulangi semua nilai dari larik di bawahnya, dan menjumlahkan nilai-nilai yang mengembalikan nilai yang sesuai. Kita perlu berhati-hati karena panggilan ke metode sum () bisa sangat mahal:

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

Terkadang, setelah memanggil sum () , kami ingin menghapus semua status yang terkait dengan instance LongAdder dan mulai menghitung dari awal. Kita bisa menggunakan metode sumThenReset () untuk mencapai itu:

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

Perhatikan bahwa panggilan berikutnya ke metode sum () mengembalikan nol yang berarti bahwa status berhasil disetel ulang.

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

3. Akumulator Panjang

LongAccumulator juga merupakan kelas yang sangat menarik - yang memungkinkan kita menerapkan algoritme bebas kunci dalam sejumlah skenario. Misalnya, ini bisa digunakan untuk mengumpulkan hasil sesuai dengan LongBinaryOperator yang disediakan - ini bekerja mirip dengan operasi reduce () dari Stream API.

Instance dari LongAccumulator bisa dibuat dengan menyediakan LongBinaryOperator dan nilai awal ke konstruktornya. Penting untuk diingat bahwa LongAccumulator akan bekerja dengan benar jika kita menyediakannya dengan fungsi komutatif di mana urutan akumulasi tidak menjadi masalah.

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

Kami menciptakan LongAccumulator WHI ch akan menambah nilai baru dengan nilai yang sudah dalam akumulator. Kami menyetel nilai awal LongAccumulator ke nol, jadi dalam panggilan pertama metode akumulasi () , nilai sebelumnya akan memiliki nilai nol.

Mari panggil metode akumulasi () dari beberapa 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 meneruskan angka sebagai argumen ke metode akumulasi () . Metode itu akan memanggil fungsi sum () kita .

The LongAccumulator menggunakan implementasi membandingkan-dan-swap - yang mengarah ke ini semantik yang menarik.

Pertama, ia menjalankan tindakan yang didefinisikan sebagai LongBinaryOperator, dan kemudian memeriksa apakah nilai sebelumnya berubah. Jika sudah diubah, aksi dijalankan lagi dengan nilai baru. Jika tidak, itu berhasil mengubah nilai yang disimpan di akumulator.

Kami sekarang dapat menyatakan bahwa jumlah semua nilai dari semua iterasi adalah 20200 :

assertEquals(accumulator.get(), 20200);

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

4. Striping Dinamis

Semua implementasi adder dan akumulator di Java diwarisi dari kelas dasar yang menarik yang disebut Striped64. Alih-alih hanya menggunakan satu nilai untuk mempertahankan status saat ini, kelas ini menggunakan larik status untuk mendistribusikan pertentangan ke lokasi memori yang berbeda.

Berikut gambaran sederhana tentang apa yang dilakukan Striped64 :

Utas yang berbeda memperbarui lokasi memori yang berbeda. Karena kita menggunakan larik (yaitu, garis) status, gagasan ini disebut garis dinamis. Menariknya, Striped64 dinamai ide ini dan fakta bahwa ia berfungsi pada tipe data 64-bit.

Kami mengharapkan striping dinamis untuk meningkatkan kinerja secara keseluruhan. Namun, cara JVM mengalokasikan status ini mungkin memiliki efek kontraproduktif.

Untuk lebih spesifiknya, JVM dapat mengalokasikan status tersebut di dekat satu sama lain di heap. Ini berarti bahwa beberapa status dapat berada di baris cache CPU yang sama. Oleh karena itu, memperbarui satu lokasi memori dapat menyebabkan cache hilang ke status terdekatnya . Fenomena yang dikenal sebagai false sharing ini akan merusak kinerja .

Untuk mencegah berbagi palsu. yang Striped64 pelaksanaan menambahkan cukup bantalan sekitar masing-masing negara untuk memastikan bahwa setiap negara berada di baris cache sendiri:

The @Contended penjelasan bertanggung jawab untuk menambahkan bantalan ini. Padding meningkatkan kinerja dengan mengorbankan lebih banyak konsumsi memori.

5. Kesimpulan

Dalam tutorial singkat ini, kami telah melihat LongAdder dan LongAccumulator dan kami telah menunjukkan cara menggunakan kedua konstruksi untuk mengimplementasikan solusi yang sangat efisien dan bebas kunci.

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di proyek GitHub - ini adalah proyek Maven, jadi semestinya mudah untuk mengimpor dan menjalankannya apa adanya.