Java 多线程下如何使用Random生成随机数。

做游戏后台的,就避免不了生成随机数,随机物品给玩家,就会存在大量的调用随机方法的可能。我们探讨下哪种写法比较合适。

java.util.Random

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

Random 使用 AtomicLong CAS (compare-and-set)操作来更新它的seed,尽管很多非阻塞式算法中使用了非阻塞式原语,CAS在资源高度竞争时的表现依然糟糕。我们写几个对比测试下。

测试代码:

 private static class RandomTask implements Runnable
    {
        private final Random rnd;
        protected final int id;
        private final long count;
        private final CountDownLatch latch;

        private RandomTask(Random rnd, int id, long count, CountDownLatch latch) {
            this.rnd = rnd;
            this.id = id;
            this.count = count;
            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 < count; ++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 final long COUNT = 10000000;
    private static final int THREADS = 2;

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

 

1.单实例下的Random,多个线程同时共享同一个实例。

 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();
        }
    }

THREADS = 2 时:

Random start
Thread #1 Time = 0.429 sec, sum = -1213943545
Thread #0 Time = 0.453 sec, sum = 1316120331

THREADS = 4 时:

Random start
Thread #3 Time = 0.945 sec, sum = 637064032
Thread #1 Time = 1.159 sec, sum = -599415297
Thread #2 Time = 1.24 sec, sum = 912228943
Thread #0 Time = 1.3 sec, sum = -1598737659

简单的测试下,竞争越激烈,运行时间越长。

2. 运用数组 缓存多个Random实例,减少多线程下的竞争资源。

 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();
        }
    }

参数padding 来控制数组缓存的大小。

THREADS = 4 时:

Random start
 Random[] with no padding
Thread #0 Time = 0.55 sec, sum = 2024089153
Thread #3 Time = 0.611 sec, sum = 2024089153
Thread #1 Time = 0.617 sec, sum = 2024089153
Thread #2 Time = 0.671 sec, sum = 2024089153
 Random[] with padding
Thread #1 Time = 0.411 sec, sum = 2024089153
Thread #0 Time = 0.419 sec, sum = 2024089153
Thread #3 Time = 0.505 sec, sum = 2024089153
Thread #2 Time = 0.455 sec, sum = 2024089153

3.ThreadLocalRandom

在程序运行的线程数低于CPU的线程数时性能没有下降,当程序运行的线程数超过CPU的线程数时性能才线性的降低。另一个要注意的重点是,单一线程执行的效率没有单实例Random高——无竞争的CAS操作仍然表现糟糕。

THREADS = 4 时:

Random start
ThreadLocalRandom
Thread #3 Time = 0.029 sec, sum = -1502647470
Thread #0 Time = 0.043 sec, sum = -435913192
Thread #2 Time = 0.023 sec, sum = 1787470510
Thread #1 Time = 0.087 sec, sum = 683332778

相比较前面几个例子,多线程竞争下,性能提升明显。

4.ThreadLocal 实现当前线程共享一个实例。

private static void testTL_Random( final int threads, final long cnt )
    {
        final CountDownLatch latch = new CountDownLatch( threads );
        final ThreadLocal rnd = new ThreadLocal() {
            @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();
        }
    }

THREADS = 4 时:

Random start
ThreadLocal
Thread #1 Time = 0.402 sec, sum = 2024089153
Thread #2 Time = 0.374 sec, sum = 2024089153
Thread #0 Time = 0.448 sec, sum = 2024089153
Thread #3 Time = 0.458 sec, sum = 2024089153

java.util.Random实例装入ThreadLocal后执行的效率有些不一样,当线程数超过CPU核心数时性能会有个下降。

本人能力有限,只能通过测试作对比,来看出之间的区别,为啥造成这样就要深入理解了。

总结:

  • 不要在多个线程间共享一个java.util.Random实例,而该把它放入ThreadLocal之中。
  • Java7以上我们更推荐使用java.util.concurrent.ThreadLocalRandom

 

你可能感兴趣的:(游戏后端开发,基础知识)