Panduan ThreadLocalRandom di Java

1. Gambaran keseluruhan

Menjana nilai rawak adalah tugas yang sangat biasa. Inilah sebabnya mengapa Java menyediakan kelas java.util.Random .

Walau bagaimanapun, kelas ini tidak menunjukkan prestasi yang baik dalam persekitaran berbilang benang.

Dengan cara yang disederhanakan, alasan untuk prestasi Random yang buruk di persekitaran multi-utas adalah kerana perbalahan - memandangkan banyak utas berkongsi contoh Random yang sama .

Untuk mengatasi batasan itu, Java memperkenalkan kelas java.util.concurrent.ThreadLocalRandom di JDK 7 - untuk menghasilkan nombor rawak dalam persekitaran berbilang utas .

Mari lihat bagaimana prestasi ThreadLocalRandom dan bagaimana menggunakannya dalam aplikasi dunia nyata.

2. ThreadLocalRandom Over Random

ThreadLocalRandom adalah gabungan dari kelas ThreadLocal dan Random (lebih lanjut mengenai ini kemudian) dan diasingkan ke utas semasa. Oleh itu, ia mencapai prestasi yang lebih baik dalam persekitaran multithreaded dengan hanya mengelakkan akses bersamaan ke kejadian Random .

Nombor rawak yang diperoleh oleh satu utas tidak dipengaruhi oleh utas yang lain, sedangkan java.util.Random memberikan nombor rawak secara global.

Juga, tidak seperti Random, ThreadLocalRandom tidak menyokong penetapan benih secara eksplisit. Sebaliknya, ia mengatasi kaedah setSeed (benih panjang) yang diwarisi dari Random untuk selalu membuang UnsupportedOperationException jika dipanggil.

2.1. Isi Kandungan

Setakat ini, kami telah membuktikan bahawa kelas Rawak berprestasi rendah dalam persekitaran yang serentak. Untuk lebih memahami ini, mari kita lihat bagaimana salah satu operasi utamanya, seterusnya (int) , dilaksanakan:

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 pelaksanaan Java untuk algoritma Linear Congruential Generator. Sudah jelas bahawa semua utas berkongsi pemboleh ubah contoh benih yang sama .

Untuk menghasilkan sekumpulan bit rawak seterusnya, pertama kali cuba mengubah nilai benih bersama secara atom melalui perbandinganAndSet atau CAS secara ringkas.

Apabila banyak utas berusaha mengemas kini benih secara serentak menggunakan CAS, satu utas menang dan mengemas kini benih, dan selebihnya kalah. Kehilangan utas akan mencuba proses yang sama berulang kali sehingga mereka berpeluang untuk mengemas kini nilainya dan akhirnya menghasilkan nombor rawak.

Algoritma ini bebas kunci, dan utas yang berbeza dapat maju serentak. Namun, apabila pertikaiannya tinggi, jumlah kegagalan dan percubaan semula CAS akan merosakkan prestasi keseluruhan dengan ketara.

Sebaliknya, ThreadLocalRandom menghilangkan perselisihan ini, kerana setiap utas mempunyai contoh Random tersendiri dan, akibatnya, benih terkurungnya sendiri .

Sekarang mari kita lihat beberapa cara untuk menghasilkan nilai int, panjang dan berganda secara rawak .

3. Menjana Nilai Rawak Menggunakan ThreadLocalRandom

Sesuai dokumentasi Oracle, kita hanya perlu memanggil kaedah ThreadLocalRandom.current () , dan ia akan mengembalikan contoh ThreadLocalRandom untuk utas semasa . Kita kemudian dapat menghasilkan nilai rawak dengan menggunakan kaedah contoh yang ada di kelas.

Mari menjana nilai int rawak tanpa had:

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

Seterusnya, mari kita lihat bagaimana kita dapat menghasilkan nilai int terikat secara rawak , yang bermaksud nilai antara had bawah dan atas yang diberikan

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

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

Harap maklum, 0 adalah had bawah inklusif dan 100 adalah had atas eksklusif.

Kita boleh menjana nilai rawak untuk panjang dan dua kali ganda dengan menggunakan kaedah nextLong () dan nextDouble () dengan cara yang serupa seperti yang ditunjukkan dalam contoh di atas.

Java 8 juga menambahkan kaedah NextGaussian () untuk menghasilkan nilai seterusnya yang diedarkan normal dengan sisihan piawai 0.0 dan 1.0 standard dari urutan penjana.

Seperti kelas Rawak , kita juga dapat menggunakan kaedah ganda (), ints () dan long () untuk menghasilkan aliran nilai rawak.

4. Membandingkan ThreadLocalRandom dan Random Menggunakan JMH

Mari lihat bagaimana kita dapat menghasilkan nilai rawak dalam persekitaran berbilang utas, dengan menggunakan dua kelas, kemudian membandingkan prestasi mereka menggunakan JMH.

Pertama, mari buat contoh di mana semua utas berkongsi satu contoh Random. Di sini, kami menyerahkan tugas untuk menghasilkan nilai rawak menggunakan contoh Rawak 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 periksa prestasi kod di atas menggunakan penanda aras JMH:

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

Begitu juga, sekarang mari kita menggunakan ThreadLocalRandom dan bukan contoh Random , yang menggunakan satu contoh 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 melihat dengan jelas bahawa masa yang diambil untuk menghasilkan 1000 nilai rawak menggunakan Random adalah 772 mikrodetik, sedangkan menggunakan ThreadLocalRandom adalah sekitar 625 mikrodetik.

Oleh itu, kita dapat menyimpulkan bahawa ThreadLocalRandom lebih efisien dalam persekitaran yang serentak .

Untuk mengetahui lebih lanjut mengenai JMH , lihat artikel kami sebelumnya di sini.

5. Perincian Pelaksanaan

Ini adalah model mental yang baik untuk memikirkan ThreadLocalRandom sebagai gabungan kelas ThreadLocal dan Random . Sebenarnya, model mental ini diselaraskan dengan pelaksanaan sebenarnya sebelum Java 8.

Namun demikian, pada Java 8, penjajaran ini rusak sepenuhnya ketika ThreadLocalRandom menjadi singleton . Inilah cara kaedah semasa () 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 bahawa berkongsi satu contoh Rawak global membawa kepada prestasi yang tidak optimum dalam perbalahan tinggi. Walau bagaimanapun, menggunakan satu contoh khusus setiap utas juga berlebihan.

Instead of a dedicated instance of Random per thread, each thread only needs to maintain its own seed value. As of Java 8, the Thread class itself has been retrofitted to maintain the seed value:

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 variable is responsible for maintaining the current seed value for ThreadLocalRandom. Moreover, the secondary seed, threadLocalRandomSecondarySeed, is usually used internally by the likes of ForkJoinPool.

This implementation incorporates a few optimizations to make ThreadLocalRandom even more performant:

  • 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 ia dapat memberikan kesan yang ketara dalam aplikasi yang serentak.

Dan, seperti biasa, pelaksanaan semua contoh ini dapat dilihat di GitHub.