0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395  《 面试又翻车了》这次竟然和 Random 有关?
英语文化交流 > 技术博客 > 《 面试又翻车了》这次竟然和 Random 有关?
《 面试又翻车了》这次竟然和 Random 有关?
时间: 分类:技术博客

小强最近面试又翻车了,然而令他郁闷的是,这次竟然是栽到了自己经常在用的 Random 上......
面试问题

既然已经有了 Random 为什么还需要 ThreadLocalRandom?
正文

Random 是使用最广泛的随机数生成工具了,即使连 Math.random() 的底层也是用 Random 实现的Math.random() 源码如下:
《 面试又翻车了》这次竟然和 Random 有关?
可以看出 Math.random() 直接指向了 Random.nextDouble() 方法。
Random 使用

这开始之前,我们先来了解一下 Random 的使用。

Random random = new Random();
for (int i = 0; i < 3; i++) {
    // 生成 0-9 的随机整数
    random.nextInt(10);
}

以上程序的执行结果为:

1
0
7

Random 源码解析

可以看出 Random 是通过 nextInt() 方法生成随机整数的,那他的底层的是如何实现的呢?我们来看他的实现源码:

/**
 * 源码版本:JDK 11
 */
public int nextInt(int bound) {
    // 验证边界的合法性
    if (bound <= 0)
        throw new IllegalArgumentException(BadBound);
    // 根据老种子生成新种子
    int r = next(31);
    // 计算最大值
    int m = bound - 1;
    // 根据新种子计算随机数
    if ((bound & m) == 0)  // i.e., bound is a power of 2
        r = (int)((bound * (long)r) >> 31);
    else {
        for (int u = r;
                u - (r = u % bound) + m < 0;
                u = next(31))
            ;
    }
    return r;
}

从以上源码我们可以看出,整个源码最核心的部分有两块:
根据老种子生成新种子;
根据新种子计算出随机数。
根据新种子计算出随机数的代码已经很明确了,我们需要确认一下 next() 方法是如何实现的,继续看源码:

/**
 * 源码版本:JDK 11
 */
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)); // 使用 CAS 更新种子
    return (int)(nextseed >>> (48 - bits));
}

根据以上源码可以看出,在使用老种子去获取新种子的时候,如果是多线程操作,则同一时刻只会有一个线程 CAS (Conmpare And Swap,比较并交换) 成功,其他失败的线程会通过自旋等待获取新种子,因此会有一定的性能消耗。
当多线程使用同一个老种子来 CAS 的时候,只能有一个线程能够成功,而其他失败的线程只能通过自旋等待,这也是为什么 JDK 1.7 会引入 ThreadLocalRandom 的答案了,它主要为了提升多线程情况下 Random 的执行效率。
ThreadLocalRandom 使用

我们先来看 ThreadLocalRandom 的类关系图:
《 面试又翻车了》这次竟然和 Random 有关?
可以看出 ThreadLocalRandom 继承于 Random 类,先来看它的使用:

ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 3; i++) {
    // 生成 0-9 的随机数
    System.out.println(threadLocalRandom.nextInt(10));
}

以上程序的执行结果为:

1
7
5

可以看出 ThreadLocalRandom 和 Random 一样,都是通过 nextInt() 方法实现随机整数生成的。
ThreadLocalRandom 源码解析

接下来我们来看 ThreadLocalRandom 的随机数是如何生成的,源码如下:

/**
 * 源码版本:JDK 11
 */
public int nextInt(int bound) {
    if (bound <= 0)
        throw new IllegalArgumentException(BAD_BOUND);
    // 根据老种子生成新种子
    int r = mix32(nextSeed());
    int m = bound - 1;
    // 根据新种子计算算出随机数
    if ((bound & m) == 0) // power of two
        r &= m;
    else { // reject over-represented candidates
        for (int u = r >>> 1;
             u + m - (r = u % bound) < 0;
             u = mix32(nextSeed()) >>> 1)
            ;
    }
    return r;
}

从以上源码可以看出 ThreadLocalRandom 的 nextInt() 和 Random 的 nextInt() 在写法和实现思路都很像,他们主要的区别在 nextSeed() 方法上,源码如下:

/**
 * 源码版本:JDK 11
 */
final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    // 把当前线程作为参数生成一个新种子
    U.putLong(t = Thread.currentThread(), SEED,
              r = U.getLong(t, SEED) + GAMMA);
    return r;
}

@HotSpotIntrinsicCandidate
public native void putLong(Object o, long offset, long x);
从以上源码可以看出,ThreadLocalRandom 并不是像 Thread 那样使用 CAS 和自旋来获取新种子,而是在每个线程中使用每个线程中保存自己的老种子来生成新种子,因此就可以避免多线程竞争和自旋等待的时间,所以在多线程环境下性能更高。
ThreadLocalRandom 注意事项

在使用 ThreadLocalRandom 时需要注意一下,在多线程不能共享一个 ThreadLocalRandom 对象,否则会造成生成的随机数都相同,如下代码所示:

// 声明多线程
ExecutorService service = Executors.newCachedThreadPool();
// 共享 ThreadLocalRandom
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
for (int i = 0; i < 10; i++) {
    // 多线程执行随机数并打印结果
    service.submit(() -> {
        System.out.println(Thread.currentThread().getName() + ":" + threadLocalRandom.nextInt(10));
        ;
    });
}

以上程序执行结果如下:

pool-1-thread-2:4
pool-1-thread-1:4
pool-1-thread-3:4
pool-1-thread-10:4
pool-1-thread-6:4
pool-1-thread-7:4
pool-1-thread-4:4
pool-1-thread-9:4
pool-1-thread-8:4
pool-1-thread-5:4

Random VS ThreadLocalRandom

Random 生成获取新种子,如下图所示:
《 面试又翻车了》这次竟然和 Random 有关?
ThreadLocalRandom 生成获取新种子,如下图所示:
《 面试又翻车了》这次竟然和 Random 有关?

性能对比

接下来我们使用 Oracle 官方提供的性能测试工具 JMH (Java Microbenchmark Harness,JAVA 微基准测试套件),来测试一下 Random 和 ThreadLocalRandom 的吞吐量(单位时间内成功执行程序的数量):

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * JDK:11
 * Windows 10 I5-4460/16G
 */
@BenchmarkMode(Mode.Throughput) // 测试类型:吞吐量
//@Threads(16)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class RandomExample {
    public static void main(String[] args) throws RunnerException {
        // 启动基准测试
        Options opt = new OptionsBuilder()
                .include(RandomExample.class.getSimpleName()) // 要导入的测试类
                .warmupIterations(5) // 预热 5 轮
                .measurementIterations(10) // 度量10轮
                .forks(1)
                .build();
        new Runner(opt).run(); // 执行测试
    }

    /**
     * Random 性能测试
     */
    @Benchmark
    public void randomTest() {
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            // 生成 0-9 的随机数
            random.nextInt(10);
        }
    }
    /**
     * ThreadLocalRandom 性能测试
     */
    @Benchmark
    public void threadLocalRandomTest() {
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            threadLocalRandom.nextInt(10);
        }
    }
}

测试结果如下:
《 面试又翻车了》这次竟然和 Random 有关?
其中,Cnt 表示运行了多少次,Score 表示执行的成绩,Units 表示每秒的吞吐量。
从 JMH 测试的结果可以看出,ThreadLocalRandom 在并发情况下的吞吐量约是 Random 的 5 倍。
完整基准测试代码下载:https://github.com/vipstone/blog-example/blob/master/blog-example/src/main/java/com/example/RandomExample.java
总结

本文讲了 Random 和 ThreadLocalRandom 的使用以及源码分析,Random 是通过 CAS 和自旋的方式生成随机数,在多线程模式下同一时刻只能有一个线程通过 CAS 获取到新种子并生成随机数,其他线程只能自旋等待,所以有一定的性能损耗。而在 JDK 1.7 时新增了 ThreadLocalRandom 它的种子保存在各自的线程中,因此不会有自旋等待的过程,所以高并发情况下性能更优秀。
最后,我们通过官方提供的基准测试工具 JMH 得到的结果,ThreadLocalRandom 的性能大约是 Random 的 5 倍,所以在高并发情况下尽量使用 ThreadLocalRandom。
参考 & 鸣谢 《Java 并发编程之美》翟陆续
原创不易,期待你的素质三连,ღ( ´・ᴗ・` )比心~
【END】
近期热文

因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!
有人说:轻量级锁一定比重量级锁快!我忍不住笑了
如何模拟线程池溢出?线程池溢出后会怎样?「视频版」
关注下方二维码,订阅更多精彩内容
《 面试又翻车了》这次竟然和 Random 有关?

随机阅读

Copyright © 2017 英语文化交流 All Rights Reserved.