java.util.Random
은 멀티 쓰레드 환경에서 하나의 인스턴스에서 전역적으로 의사 난수(pseudo random)를 반환한다. 따라서 같은 시간에 동시 요청이 들어올 경우 경합 상태에서 성능에 문제가 생길 수 있다. 반면 JDK 7부터 도입된 java.util.concurrent.ThreadLocalRandom
은 java.util.Random
를 상속하여 멀티 쓰레드 환경에서 서로 다른 인스턴스들에 의해 의사 난수를 반환하므로 동시성 문제에 안전하다.(랜덤 특성상 같은 수가 나와도 설계상 문제가 아닌 정상 작동)
기본적으로 컴퓨터는 수학적으로 완전한 난수를 생성할 수 없다. 컴퓨터는 동일 입력에 동일 출력이 보장되어야 하는 결정적 유한 오토마타(Deterministic Finite Automata)이기 때문이다. 완전한 난수 생성을 위해 정밀 공학계에서는 복잡계의 열적 특성을 이용한 물리적 난수를 만들기도 하는데, 이는 소프트웨어 개발자로 마주할 경우는 아닐 것이다.
따라서 Java와 같은 언어는 시드(Seed)와 그에 해당하는 난수의 대응으로 난수를 결정한다. 해쉬 함수의 원리나 고등학교 수학의 상용 로그표와 유사하다. 이는 같은 시드가 주어질 경우 생성되는 난수는 같다는 것을 의미하고, 설계적으로 의도한 난수 생성을 위해서는 서로 다른 시드가 주어져야 한다는 것을 의미한다(학습, 테스트 목적으로 시드를 고정하여 반환되는 난수의 순서를 같게하는 경우도 있다).
Java는 Seed를 지정하지 않으면 컴퓨터의 현재 시간을 이용하여 난수에 대응한다.
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
// subclass might have overriden setSeed
this.seed = new AtomicLong();
setSeed(seed);
}
}
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 * 1181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
java.util.Random
은 멀티 쓰레드에서 하나의 Random 인스턴스를 공유하여 전역적으로 동작한다.
이때 같은 nanoTime에 멀티 쓰레드에서 요청이 들어오면 어떻게 될까? 같은 난수를 반환하는가?
다행히 그렇게 동작하지는 않는다. Random
의 의사 난수 생성은 선형 합동 생성기(Linear Congruential Generator)알고리즘으로 동작하는데, 하나의 쓰레드가 동시 경합에서 이기면 다른 쓰레드는 자신이 이길 때까지 계속 같은 동작을 반복하며 경합한다.
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));
}
이때 이 과정을 여러 쓰레드가 동시에 경합 - 패배 - 재도전을 반복할 경우 성능상의 심각한 문제가 발생할 수 있다. 랜덤으로 할인 쿠폰을 반환하는 이벤트에서 수만 명의 요청이 동시에 몰린다면? 먹통이 된 서버에 화가 난 이용자들의 욕설에서 개발자의 멘탈을 구하려면 다른 방법이 필요하다.
java.util.concurrent.ThreadLocalRandom
은 똑같이 Random API의 구현체이며, java.util.Random
를 상속받는다. ThreadLocalRandom
은 위의 동시성 문제를 해결하기 위해 각 쓰레드마다 생성된 인스턴스에서 각각 난수를 반환한다. 따라서 Random
과 같은 경합 문제가 발생하지 않아 안전하며, 성능상 이점이 있다. Random
대신 ThreadLocalRandom
을 쓰자.
public static ThreadLocalRandom current() {
if (U.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}
0~9의 정수를 반환할 수 있는 클래스를 Random
으로 만든다면
public class MyRandom {
private static final int RANDOM_BOUND = 10;
private static final Random random = new Random();
public int getNumber() {
return random.nextInt(RANDOM_BOUND);
}
}
동일한 역할을 하는 클래스를 ThreadLocalRandom
으로 만든다면 다음과 같이 만들 수 있다.
public class MyRandom {
private static final int RANDOM_BOUND = 10;
private static final ThreadLocalRandom random = ThreadLocalRandom.current();
public int getNumber() {
return random.nextInt(RANDOM_BOUND);
}
}
https://www.concretepage.com/java/jdk7/threadlocalrandom-java-example
https://namocom.tistory.com/733
https://needneo.tistory.com/70
https://ko.wikipedia.org/wiki/%EC%84%A0%ED%98%95_%ED%95%A9%EB%8F%99_%EC%83%9D%EC%84%B1%EA%B8%B0
멀티 쓰레드 상황에서는 Random을 사용하는 것을 피해야겠네요! 덕분에 ThreadLocalRandom에 대해서 알게 되었어요 고마워요 소주캉!