Random 대신 ThreadLocalRandom을 써야 하는 이유

sojukang·2022년 2월 15일
4

결론

java.util.Random은 멀티 쓰레드 환경에서 하나의 인스턴스에서 전역적으로 의사 난수(pseudo random)를 반환한다. 따라서 같은 시간에 동시 요청이 들어올 경우 경합 상태에서 성능에 문제가 생길 수 있다. 반면 JDK 7부터 도입된 java.util.concurrent.ThreadLocalRandomjava.util.Random를 상속하여 멀티 쓰레드 환경에서 서로 다른 인스턴스들에 의해 의사 난수를 반환하므로 동시성 문제에 안전하다.(랜덤 특성상 같은 수가 나와도 설계상 문제가 아닌 정상 작동)

Java가 난수를 생성하는 원리

기본적으로 컴퓨터는 수학적으로 완전한 난수를 생성할 수 없다. 컴퓨터는 동일 입력에 동일 출력이 보장되어야 하는 결정적 유한 오토마타(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;
        }
    }

Random이 멀티 쓰레드에서 위험한 이유

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

이때 이 과정을 여러 쓰레드가 동시에 경합 - 패배 - 재도전을 반복할 경우 성능상의 심각한 문제가 발생할 수 있다. 랜덤으로 할인 쿠폰을 반환하는 이벤트에서 수만 명의 요청이 동시에 몰린다면? 먹통이 된 서버에 화가 난 이용자들의 욕설에서 개발자의 멘탈을 구하려면 다른 방법이 필요하다.

ThreadLocalRandom을 써야하는 이유

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.baeldung.com/java-thread-local-random#:~:text=The%20random%20number%20obtained%20by,support%20setting%20the%20seed%20explicitly.

https://www.concretepage.com/java/jdk7/threadlocalrandom-java-example

https://namocom.tistory.com/733

https://www.4te.co.kr/945#:~:text=%EC%BD%94%EB%93%9C%20%EB%82%B4%EC%97%90%EC%84%9C%20%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94,%EC%9D%84%20%EA%B8%B0%EC%B4%88%EB%A1%9C%20%EB%A7%8C%EB%93%A4%EA%B2%8C%20%EB%90%9C%EB%8B%A4.

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

profile
기계공학과 개발어린이

3개의 댓글

comment-user-thumbnail
2022년 2월 16일

멀티 쓰레드 상황에서는 Random을 사용하는 것을 피해야겠네요! 덕분에 ThreadLocalRandom에 대해서 알게 되었어요 고마워요 소주캉!

답글 달기
comment-user-thumbnail
2022년 11월 26일

과제하다 ThreadLocalRandom vs Random을 결정할 때 이 글을 보았읍니다... 역시 소주캉

답글 달기
comment-user-thumbnail
2023년 10월 22일

감사합니다.

답글 달기