본문 바로가기

Programing/JVM(Java, Kotlin)

ThreadLocalRandom and Random

ThreadLocalRandom

자바7에서 추가된 기능중에 ThreadLocalRandom 가 있다.

스레드 별로 난수 생성을 고립시키는 랜덤 클래스이다.

java.lang.Math#random 처럼 ThreadLocalRandom 역시 내부에 고정된 holder를 통해 난수를 생성한다.

 

문제는 멀티스레드에서 이 메서드를 사용할 경우 적절하게 동기화를 시켜야하는데,
많은 수의 스레드가 난수를 생성하려고 할 경우 각자의 랜덤 생성기를 가지게 하는 방법을 통해
서로간의 경합으로 인해 성능이 떨어지는 것을 방지하라고 주석에 적혀있다.

 

ThreadLocalRandom 의 경우 current() 라는 정적 메서드를 통해 객체를 얻도록 되어 있다.

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

최초에는 localInit()이 호출되고 동일 스레드가 호출하면 바로 instance를 반환한다.

다른 스레드에 의해 current()가 호출되면 localInit()가 또 다시 호출이된다.

UNSAFE에서 getInt를 해오는데, 0이면 설정이 안되어 잇다고 보면 된다. Thread와 랜덤으로 바뀌는 PROBE 값 두개를 바탕으로 sun.misc.Unsafe 에 저장을 해놓는다.

http://mishadoff.com/blog/java-magic-part-4-sun-dot-misc-dot-unsafe/

http://rangken.github.io/blog/2015/sun.misc.unSafe/

static final void localInit() {
    int p = probeGenerator.addAndGet(PROBE_INCREMENT);
    int probe = (p == 0) ? 1 : p; // skip 0
    long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
    Thread t = Thread.currentThread();
    UNSAFE.putLong(t, SEED, seed);
    UNSAFE.putInt(t, PROBE, probe);
}

probeGenerator와 seeder는 모두 AtomicLong 타입이고, seeder 의 초깃값은 seederjava.util.secureRandomSeed 이 true일 경우에는 

SecureRandom을 통해 시드를 생성하고, 그 외에는 시간을 기반으로 생성한다.

이런 스레드의 

 

threadLocalRandomSeed: 232

threadLocalRandomProbe: 240

threadLocalRandomSecondarySeed: 244

seedUniquifier: 8006678197202707420

 

Random

또한 Java 7에서는 Random 클래스도 seed 값 설정 부분이 바뀌었다.

기존에는 Java 6 에는 아래와 같이 정의했었다.

public Random()
{
  this(System.currentTimeMillis());
}

public Random(long seed)
{
  setSeed(seed);
}

이렇게 되면 현재 시간을 시드 값으로 설정하게 된다.

 

Java 7에서는 개선된 시드 설정으로 바뀌게 된다.

public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}

private static long seedUniquifier() {
    // L'Ecuyer, "Tables of Linear Congruential Generators of
    // Different Sizes and Good Lattice Structure", 1999
    for (;;) {
        long current = seedUniquifier.get();
        long next = current * 181783497276652981L;
        if (seedUniquifier.compareAndSet(current, next))
            return next;
    }
}

시간 단위도 밀리초에서 나노초로 세밀해졌다.

Bugs

JDK-6955840 : ThreadLocalRandom bug - overridden setSeed(long) method is not invoked for java.util.Random(long)

  • Submitted: 2010-05-26
  • Resolved: 2011-05-18

JDK-7051516 : ThreadLocalRandom seed is never initialized so all instances generate the same sequence

  • Submitted: 2011-06-04
  • Resolved: 2012-05-13

 

JDK-6937857 : Concurrent calls to new Random() not random enough

  • Resolved: 2011-03-08
  • Submitted: 2010-03-24
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;

public class RandomSeedCollisions {
    public static void main(String[] args) throws Throwable {
        class RandomCollector implements Runnable {
            long[] randoms = new long[1<<16];
            public void run() {
                for (int i = 0; i < randoms.length; i++)
                    randoms[i] = new Random().nextLong();
            }};
        final int threadCount = 2;
        List<RandomCollector> collectors = new ArrayList<RandomCollector>();
        List<Thread> threads = new ArrayList<Thread>();
        for (int i = 0; i < threadCount; i++) {
            RandomCollector r = new RandomCollector();
            collectors.add(r);
            threads.add(new Thread(r));
        }
        for (Thread thread : threads)
            thread.start();
        for (Thread thread : threads)
            thread.join();
        int collisions = 0;
        HashSet<Long> s = new HashSet<Long>();
        for (RandomCollector r : collectors) {
            for (long x : r.randoms) {
                if (s.contains(x))
                    collisions++;
                s.add(x);
            }
        }
        System.out.printf("collisions=%d%n", collisions);
        if (collisions > 10)
            throw new Error("too many collisions");
    }
}