회원가입 시 사용자가 입력한 닉네임 뒤에 #0000 과 같은 Suffix를 붙여야한다는 요구사항이 있다.
뒤에 네 자리 숫자는 난수를 생성하기 위해 Random 객체를 사용했다.
public class IdentifierProvider {
private static final String NICKNAME_IDENTIFIER_PREFIX = "#";
private static final int DIGIT = 4;
private static final Random random = new Random();
public static String create(){
final StringBuilder identifier = new StringBuilder(NICKNAME_IDENTIFIER_PREFIX);
for (int i = 0; i <DIGIT ; i++) {
identifier.append(
random.nextInt(9)
);
}
return identifier.toString();
}
}
위 코드는 하나의 Random 클래스를 여러 스레드가 공유할 수 있는 환경이다.
문제는 여러 스레드가 Random 클래스 내부에 Seed값을 공유하면서 문제는 발생한다.
Random의 nextInt
우선 Random 클래스의 nextInt 메서드를 들여다 보자.
내부에서 next(31)을 호출한다.
@Override
public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BAD_BOUND);
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 { // reject over-represented candidates
for (int u = r;
u - (r = u % bound) + m < 0;
u = next(31))
;
}
return r;
}
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));
}
seed.get()으로 seed값을 가져오고 새로운 nextseed값을 생성한다.
후에 CAS(Compare And Set)연산을 통해서 확인절차를 거친다.
CAS 연산
CAS(comapreAndSet)는 스레드가 가지고 있는 oldseed의 값과 메인메모리에 있는 oldseed값을 비교한다.
만약 두 값이 같다면 새로운 nextseed를 seed에 할당하고 다르다면 계속해서 루프를 돌게된다.
어떤 경우에 문제가 발생하는지를 좀 더 정확히 살펴보자.
1. T1 스레드가 oldseed 값을 읽는다. seed.get()
2. 스케줄러에 의해서 T2스레드로 문맥이 전환된다.
3. T2스레드는 seed를 읽고 CAS까지 통과해 seed값을 변경한다.
4. T1 스레드가 다시 스케줄링되고 CAS 연산을 수행한다.
5. T1 스레드가 가지고 있는 oldseed값과 메인스레드의 oldseed 값이 다르다.(T2가 변경하고 나갔기 때문)
6. do while을 통해 성공할 때 까지 시도한다.
트래픽이 굉장히 많은 서비스에서 자주 이용하는 API에 Random 클래스를 공유하도록 설계하면
많은 스레드가 경합하게 되고 경합에서 실패하면 반복적인 CAS연산으로 동기화하려 하기 때문에 성능저하가 발생한다.
물론 스레드가 접근할 때 마다 새로운 Random 클래스를 사용하면 seed값을 공유하지 않지만 객체 생성 비용을 무시할 수 없다.
ThreadLocalRandom
자바에서는 이런 문제점을 인식하고 멀티스레드 환경에서 안전하고 성능이 Random 클래스보다 좋은 ThreadLocalRandom을 제공한다.
이름에서 보이듯 스레드 로컬을 사용해서 문제를 해결한다.
즉 스레드 로컬 필드에 seed값을 설정해서 여러 스레드가 접근해 오더라도 seed값을 공유하는 문제는 발생하지 않는다.
ThreadLocalRandom을 이용해서 코드를 작성하면
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class IdentifierProvider {
private static final String NICKNAME_IDENTIFIER_PREFIX = "#";
private static final int DIGIT = 4;
public static String create(){
final StringBuilder identifier = new StringBuilder(NICKNAME_IDENTIFIER_PREFIX);
for (int i = 0; i <DIGIT ; i++) {
identifier.append(
ThreadLocalRandom.current().nextInt(9)
);
}
return identifier.toString();
}
}
이렇게 사용할 수 있다.
current()가 호출되면 내부에서 스레드 로컬에 seed값을 생성하고 nextInt()를 통해 난수를 발생시킨다.
성능 비교
둘의 성능 차이가 궁금해서 테스트를 진행해봤다.
@Test
void 식별자_생성_동시성_테스트() throws InterruptedException {
// 스레드 준비
final int numOfThread = 30;
ExecutorService executorService = Executors.newFixedThreadPool(numOfThread);
long startTime = System.currentTimeMillis();
CountDownLatch latch = new CountDownLatch(1000000);
// Random 클래스 성능 측정
Random random = new Random();
for (int i = 0; i < 1000000; i++) {
executorService.execute(() -> random.nextInt(10));
latch.countDown();
}
latch.await();
long endTime = System.currentTimeMillis();
long durationTimeSec = endTime - startTime;
System.out.println(durationTimeSec + "m/s");
executorService = Executors.newFixedThreadPool(numOfThread);
startTime = System.currentTimeMillis();
latch = new CountDownLatch(1000000);
// ThreadLocalRandom 성능 측정
for (int i = 0; i < 1000000; i++) {
executorService.execute(() ->ThreadLocalRandom.current().nextInt(10));
latch.countDown();
}
latch.await();
endTime = System.currentTimeMillis();
durationTimeSec = endTime - startTime;
System.out.println(durationTimeSec + "m/s");
}
스레드는 30개 준비했고 100만번 난수를 발생시켰다.
차이는 아주 극명하게 나타났다.
마무리
싱글스레드 환경에서는 Random을 사용해도 무방하겠지만
멀티스레드 환경에서 난수를 발생시키고 싶다면 ThreadLocalRandom을 이용하는 편이 좋다.
'Java' 카테고리의 다른 글
Record Class 도입기 (0) | 2024.05.03 |
---|---|
테스트 코드 그리고 리팩토링 (0) | 2024.04.19 |
Java Integer Caching (0) | 2024.04.13 |
[우아한 테크 세미나] 우아한 객체지향 - 1 (0) | 2024.04.08 |
[WAS를 만들어보자 (3)] HttpMessageBody 추출하기 (1) | 2024.03.23 |