生成伪随机数据

Java里有伪随机型和安全型两种随机数生成器。伪随机生成器根据特定公式将seed转换成新的伪随机数据的一部分。安全随机生成器在底层依赖到操作系统提供的随机事件来生成数据。

安全随机生成器

  • 需要生成加密性强的随机数据的时候才用它;
  • 生成速度慢;
  • 如果需要生成(Linux /dev/random 就是个这样的安全随机生成器)大量随机数据,可能会产生堵塞需要等待外部中断事件。

而伪随机生成器,只依赖于”seed”的初始值。如果你给生成算法提供相同的seed,可以得到一样的伪随机序列。一般情况下,由于它是计算密集型的(不依赖于任何IO设备),因此生成速度更快。接下来,我们将回顾伪随机生成器的进化史。

java.util.Random

java.util.Random 从Java 1.0开始就存在了。它是一个线程安全类,理论上可以通过它同时在多个线程中获得互不相同的随机数。这样的线程安全是通过AtomicLong实现的。

Random 使用 AtomicLong CAS (compare-and-set)操作来更新它的seed,尽管很多非阻塞式算法中使用了非阻塞式原语,CAS在资源高度竞争时的表现依然糟糕。在后面的测试结果中你可以看到它的糟糕表现。

java.util.concurrent.ThreadLocalRandom

Java 7增加了java.util.concurrent.ThreadLocalRandom 并企图将它与 java.util.Random 结合以克服所有的性能问题。ThreadLocalRandom类继承自java.util.Random。

ThreadLocalRandom 的主要实现细节:

  • 它使用一个普通的 long 而不是使用 Random 中的 AtomicLong 作为seed。
  • 你不能自己创建ThreadLocalRandom实例,因为它的构造函数没有设置为public。可以使用它的静态工厂ThreadLocalRandom.current(),这个工厂方法调用了内置的ThreadLocal<
    ThreadLocalRandom>。
  • 它是CPU缓存感知式的,使用8个 long 虚拟域来填充64位L1高速缓存行。

所有这些改变都是很重要的,在接下来的测试中你将会感受到。

测试

我们将进行下面5种测试:

  1. 一个单独的java.util.Random被N个线程共享
  2. ThreadLocal< Random>
  3. java.util.concurrent.ThreadLocalRandom
  4. java.util.Random[],其中每个线程N使用一个数组下标为N的Random。
  5. java.util.Random[],其中每个线程N使用一个数组下标为N * 2的Random。

所有的测试都使用了封装在RandomTask类里的方法。每个方案都说明了如何使用随机生成器。

package com.ykp.concurrent;

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomGenerator {
    private static final long COUNT = 10000000;
    private static final int THREADS = 2;

    public static void main(String[] args) {
        System.out.println("Shared Random");
        testRandom(THREADS, COUNT);
        // System.out.println("ThreadLocal<Random>");
        // testTL_Random(THREADS, COUNT);
        // System.out.println("ThreadLocalRandom");
        // testTLRandom(THREADS, COUNT);
        // System.out.println("Shared Random[] with no padding");
        // testRandomArray(THREADS, COUNT, 1);
        // System.out.println("Shared Random[] with padding");
        // testRandomArray(THREADS, COUNT, 2);
    }

    // runner for all tests
    private static class RandomTask implements Runnable {
        private final Random rnd;
        protected final int id;
        private final long cnt;
        private final CountDownLatch latch;

        private RandomTask(Random rnd, int id, long cnt, CountDownLatch latch) {
            this.rnd = rnd;
            this.id = id;
            this.cnt = cnt;
            this.latch = latch;
        }

        protected Random getRandom() {
            return rnd;
        }

        @Override
        public void run() {
            try {
                final Random r = getRandom();
                latch.countDown();
                latch.await();
                final long start = System.currentTimeMillis();
                int sum = 0;
                for (long j = 0; j < cnt; ++j) {
                    sum += r.nextInt();
                }
                final long time = System.currentTimeMillis() - start;
                System.out.println("Thread #" + id + " Time = " + time / 1000.0
                        + " sec, sum = " + sum);
            } catch (InterruptedException e) {
            }
        }
    }

    private static void testRandom(final int threads, final long cnt) {
        // 可以保证所有线程同时开始执行
        final CountDownLatch latch = new CountDownLatch(threads);
        final Random r = new Random(100);
        for (int i = 0; i < threads; ++i) {
            final Thread thread = new Thread(new RandomTask(r, i, cnt, latch));
            thread.start();
        }
    }

    private static void testRandomArray(final int threads, final long cnt,
            final int padding) {
        final CountDownLatch latch = new CountDownLatch(threads);
        final Random[] rnd = new Random[threads * padding];
        for (int i = 0; i < threads * padding; ++i)
            // allocate together
            rnd[i] = new Random(100);
        for (int i = 0; i < threads; ++i) {
            final Thread thread = new Thread(new RandomTask(rnd[i * padding],
                    i, cnt, latch));
            thread.start();
        }
    }

    private static void testTLRandom(final int threads, final long cnt) {
        final CountDownLatch latch = new CountDownLatch(threads);
        for (int i = 0; i < threads; ++i) {
            final Thread thread = new Thread(
                    new RandomTask(null, i, cnt, latch) {
                        // 改变RandowTask中获取Random的方式
                        @Override
                        protected Random getRandom() {
                            return ThreadLocalRandom.current();
                        }
                    });
            thread.start();
        }
    }

    private static void testTL_Random(final int threads, final long cnt) {
        final CountDownLatch latch = new CountDownLatch(threads);
        final ThreadLocal<Random> rnd = new ThreadLocal<Random>() {
            @Override
            protected Random initialValue() {
                return new Random(100);
            }
        };
        for (int i = 0; i < threads; ++i) {
            final Thread thread = new Thread(
                    new RandomTask(null, i, cnt, latch) {
                        @Override
                        protected Random getRandom() {
                            return rnd.get();
                        }
                    });
            thread.start();
        }
    }
}

测试结果

Shared java.util.Random
第一个测试使用的是共享的java.util.Random实例。高争夺的CAS操作严重影响了它的性能。仅仅开两个线程都会受争夺的影响,然后现实中很少会发生这种争夺的情况。下面是所有线程的最小和最大运行时间。

“Shared” java.util.concurrent.ThreadLocalRandom
接下来的测试使用第二个类——java.util.concurrent.ThreadLocalRandom。 如你所见,在程序运行的线程数低于CPU的线程数时性能没有下降,当程序运行的线程数超过CPU的线程数时性能才线性的降低。另一个要注意的重点是,单一线程执行的效率是第一个案例的3倍——无竞争的CAS操作仍然表现糟糕。

“Shared” ThreadLocal《java.util.Random》
把java.util.Random实例装入ThreadLocal后执行的效率有些不一样,当线程数超过CPU核心数时性能就下降了——听起来像是CAS操作不能执行那么多单元。不过接下来的性能下降是线性的,和第二个案例很相似。

Array of java.util.Random
最后我想要检查CPU缓存行对ThreadLocalRandom 的改善作用和模拟java.util.Random在缺乏这种功能下的情况。你需要做的就是创建可以被很多线程使用的java.util.Random实例,我用java.util.Random[]来实现此目的并用array[N]表示第N个线程。

总结

  • 任何情况下都不要在多个线程间共享一个java.util.Random实例,而该把它放入ThreadLocal之中。
  • Java7在所有情形下都更推荐使用java.util.concurrent.ThreadLocalRandom——它向下兼容已有的代码且运营成本更低。
GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐