Panduan untuk ThreadLocalRandom di Java

1. Ikhtisar

Menghasilkan nilai acak adalah tugas yang sangat umum. Inilah mengapa Java menyediakan kelas java.util.Random .

Namun, kelas ini tidak bekerja dengan baik dalam lingkungan multi-thread.

Dengan cara yang disederhanakan, alasan kinerja Acak yang buruk di lingkungan multi-utas adalah karena perselisihan - mengingat bahwa beberapa utas berbagi contoh Acak yang sama .

Untuk mengatasi batasan itu, Java memperkenalkan kelas java.util.concurrent.ThreadLocalRandom di JDK 7 - untuk menghasilkan nomor acak dalam lingkungan multi-thread .

Mari kita lihat bagaimana kinerja ThreadLocalRandom dan bagaimana menggunakannya dalam aplikasi dunia nyata.

2. ThreadLocalRandom Over Random

ThreadLocalRandom adalah kombinasi dari kelas ThreadLocal dan Random (lebih lanjut tentang ini nanti) dan diisolasi ke utas saat ini. Dengan demikian, ini mencapai kinerja yang lebih baik dalam lingkungan multithread dengan hanya menghindari akses bersamaan ke contoh Random .

Nomor acak yang diperoleh oleh satu thread tidak terpengaruh oleh thread lainnya, sedangkan java.util.Random menyediakan nomor acak secara global.

Juga, tidak seperti Random, ThreadLocalRandom tidak mendukung pengaturan seed secara eksplisit. Sebaliknya, ini mengganti metode setSeed (long seed) yang diwarisi dari Random untuk selalu menampilkan UnsupportedOperationException jika dipanggil.

2.1. Kontensi Benang

Sejauh ini, kami telah menetapkan bahwa kelas Random berkinerja buruk di lingkungan yang sangat serentak. Untuk lebih memahami ini, mari kita lihat bagaimana salah satu operasi utamanya, next (int) , diimplementasikan:

private final AtomicLong seed; protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend) & mask; } while (!seed.compareAndSet(oldseed, nextseed)); return (int)(nextseed >>> (48 - bits)); }

Ini adalah implementasi Java untuk algoritma Linear Congruential Generator. Jelas bahwa semua utas berbagi variabel instance benih yang sama .

Untuk menghasilkan sekumpulan bit acak berikutnya, pertama-tama mencoba mengubah nilai seed bersama secara atomik melalui CompareAndSet atau singkatnya CAS .

Ketika beberapa utas mencoba memperbarui benih secara bersamaan menggunakan CAS, satu utas menang dan memperbarui benih, dan sisanya kalah. Untaian yang hilang akan mencoba proses yang sama berulang kali hingga mendapat kesempatan untuk memperbarui nilai dan akhirnya menghasilkan nomor acak.

Algoritme ini bebas kunci, dan utas yang berbeda dapat berkembang secara bersamaan. Namun, jika pertentangannya tinggi, jumlah kegagalan CAS dan percobaan ulang akan mengganggu kinerja keseluruhan secara signifikan.

Di sisi lain, ThreadLocalRandom sepenuhnya menghapus perselisihan ini, karena setiap utas memiliki contoh Random sendiri dan, akibatnya, benih terbatasnya sendiri .

Sekarang mari kita lihat beberapa cara untuk menghasilkan nilai int, long dan double acak .

3. Menghasilkan Nilai Acak Menggunakan ThreadLocalRandom

Sesuai dengan dokumentasi Oracle, kita hanya perlu memanggil metode ThreadLocalRandom.current () , dan itu akan mengembalikan instance ThreadLocalRandom untuk utas saat ini . Kami kemudian dapat menghasilkan nilai acak dengan menjalankan metode instance kelas yang tersedia.

Mari buat nilai int acak tanpa batas:

int unboundedRandomValue = ThreadLocalRandom.current().nextInt());

Selanjutnya, mari kita lihat bagaimana kita dapat menghasilkan nilai int terbatas acak , yang berarti nilai antara batas bawah dan atas yang diberikan.

Berikut adalah contoh menghasilkan nilai int acak antara 0 dan 100:

int boundedRandomValue = ThreadLocalRandom.current().nextInt(0, 100);

Harap dicatat, 0 adalah batas bawah inklusif dan 100 adalah batas atas eksklusif.

Kita bisa menghasilkan nilai acak untuk long dan double dengan memanggil metode nextLong () dan nextDouble () dengan cara yang sama seperti yang ditunjukkan pada contoh di atas.

Java 8 juga menambahkan metode nextGaussian () untuk menghasilkan nilai terdistribusi normal berikutnya dengan mean 0,0 dan deviasi standar 1,0 dari urutan generator.

Seperti kelas Random , kita juga dapat menggunakan metode doubles (), ints () dan longs () untuk menghasilkan aliran nilai acak.

4. Membandingkan ThreadLocalRandom dan Random Menggunakan JMH

Mari kita lihat bagaimana kita dapat menghasilkan nilai acak dalam lingkungan multi-threaded, dengan menggunakan dua kelas, lalu membandingkan kinerjanya menggunakan JMH.

Pertama, mari buat contoh di mana semua utas berbagi satu contoh Acak. Di sini, kami mengirimkan tugas untuk menghasilkan nilai acak menggunakan instance Random ke ExecutorService:

ExecutorService executor = Executors.newWorkStealingPool(); List
    
      callables = new ArrayList(); Random random = new Random(); for (int i = 0; i { return random.nextInt(); }); } executor.invokeAll(callables);
    

Mari kita periksa performa kode di atas menggunakan benchmarking JMH:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingRandom avgt 20 771.613 ± 222.220 us/op

Demikian pula, sekarang mari gunakan ThreadLocalRandom alih-alih instance Random , yang menggunakan satu instance ThreadLocalRandom untuk setiap utas di kumpulan:

ExecutorService executor = Executors.newWorkStealingPool(); List
    
      callables = new ArrayList(); for (int i = 0; i { return ThreadLocalRandom.current().nextInt(); }); } executor.invokeAll(callables);
    

Inilah hasil penggunaan ThreadLocalRandom:

# Run complete. Total time: 00:00:36 Benchmark Mode Cnt Score Error Units ThreadLocalRandomBenchMarker.randomValuesUsingThreadLocalRandom avgt 20 624.911 ± 113.268 us/op

Akhirnya, dengan membandingkan hasil JMH di atas untuk Random dan ThreadLocalRandom , kita dapat dengan jelas melihat bahwa rata-rata waktu yang dibutuhkan untuk menghasilkan 1000 nilai acak menggunakan Random adalah 772 mikrodetik, sedangkan menggunakan ThreadLocalRandom sekitar 625 mikrodetik.

Dengan demikian, kita dapat menyimpulkan bahwa ThreadLocalRandom lebih efisien dalam lingkungan yang sangat serentak .

Untuk mempelajari lebih lanjut tentang JMH , lihat artikel kami sebelumnya di sini.

5. Detail Implementasi

Ini adalah model mental yang baik untuk menganggap ThreadLocalRandom sebagai kombinasi kelas ThreadLocal dan Random . Faktanya, model mental ini sejalan dengan implementasi aktual sebelum Java 8.

Pada Java 8, bagaimanapun, keselarasan ini benar-benar rusak karena ThreadLocalRandom menjadi tunggal . Beginilah tampilan metode current () di Java 8+:

static final ThreadLocalRandom instance = new ThreadLocalRandom(); public static ThreadLocalRandom current() { if (U.getInt(Thread.currentThread(), PROBE) == 0) localInit(); return instance; }

Memang benar bahwa berbagi satu contoh Acak global mengarah pada kinerja yang kurang optimal dalam persaingan yang tinggi. Namun, menggunakan satu instance khusus per utas juga berlebihan.

Alih-alih contoh khusus Random per utas, setiap utas hanya perlu mempertahankan nilai benihnya sendiri . Mulai Java 8, kelas Thread itu sendiri telah dipasang untuk mempertahankan nilai seed :

public class Thread implements Runnable { // omitted @jdk.internal.vm.annotation.Contended("tlr") long threadLocalRandomSeed; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomProbe; @jdk.internal.vm.annotation.Contended("tlr") int threadLocalRandomSecondarySeed; }

The threadLocalRandomSeed variabel bertanggung jawab untuk menjaga nilai benih saat ini untuk ThreadLocalRandom. Selain itu, seed sekunder, threadLocalRandomSecondarySeed , biasanya digunakan secara internal oleh orang-orang seperti ForkJoinPool.

Implementasi ini menggabungkan beberapa pengoptimalan untuk membuat ThreadLocalRandom lebih berkinerja:

  • Avoiding false sharing by using the @Contented annotation, which basically adds enough padding to isolate the contended variables in their own cache lines
  • Using sun.misc.Unsafe to update these three variables instead of using the Reflection API
  • Avoiding extra hashtable lookups associated with the ThreadLocal implementation

6. Conclusion

This article illustrated the difference between java.util.Random and java.util.concurrent.ThreadLocalRandom.

We also saw the advantage of ThreadLocalRandom over Random in a multithreaded environment, as well as performance and how we can generate random values using the class.

ThreadLocalRandom adalah tambahan sederhana untuk JDK, tetapi dapat membuat dampak penting dalam aplikasi yang sangat serentak.

Dan, seperti biasa, implementasi dari semua contoh ini dapat ditemukan di GitHub.