Spring Boot에서 AtomicReference 사용하기


AtomicReference

목적

Spring Mvc 프로젝트에서 멀티스레드가 접근하는 변수의 Atomic Operations 해보기.

개요

Spring Mvc 기반의 프로젝트에서 AtomicReference를 사용하기에 앞서 syncronized와 CAS 방식의 차이를 알아보고 더 효율적인 방식인 AtomicReference를 이용하여 멀티스레딩 상황에서의 원자적 연산을 해보겠다.

syncronized

syncronized 키워드를 사용해 블록을 형성하거나 메서드에 선언해두면 하나의 스레드의 실행을 보장할 수 있다.
syncronized 블록은 다른 스레드가 블록 안으로 들어오는것을 막게되고 이 스레드들은 block된다.
java에서는 monitor를 통해 스레드를 획득하게 되는데, 스레드가 blocked 상태로 변경되고 다시 resume 되는 것은 비용이 비싸다.

스레드의 상태 전이

Alt text

Atomic Operations

CAS(Compare-and-swap) 작업을 의미한다.
Alt text

CAS vs syncronized(thread locking 방식)

CAS는 현재 읽은값과 비교값이 다르면 계속 변경을 시도한다. 이러한 상황에서 스레드는 이론적으로 busy-wait 상태이고 cpu의 thread는 계속해서 비교를 시도할 것이다.
반면, syncronized를 사용할 때는 thread locking이 되는데 이때는 운영체제에 의해 blocked 상태가 된다. notify()를 통해 운영체제에서 다시 runnable 상태가 된다. 이는 context-switching을 의미한다.

AtomicReference SpringBoot 에서 사용해보기

AtomicReference 초기화는 타입파라미터로 LocalDatetime을 넣어준 뒤 스프링에서 관리되는 Bean의 전역변수로 생성했다.
아래 예시에서는 TypeParameter를 LocalDatetime으로 지정했는데, TypeParameter에는 LocalDateTime 뿐만 아니라 어떠한 객체타입이라도 모두 가능하다.

asyncMethod()는 5초마다 실행하게 된다. @Async 어노테이션을 사용하지 않는다면 메서드 내부의 동작이 5초 이상 걸리게 되었을 때 5초 뒤에 실행되기로 한 메서드의 실행이 되지 않게 된다.
지정한 시간에 실행하고 있는 스레드가 작업 중이어도 별도의 스레드를 통한 메서드 실행을 위해 @Async를 붙여줌으로써 별도의 스레드를 통한 동작이 가능하게 된다.
이제 전역변수를 여러개의 스레드가 동시에 접근할 수 있는 상황이 되었다.
멀티스레딩에서의 전역변수 값이 저장되어 있는 메인메모리 부터 값을 참조하지 않고 캐시메모리에 저장된 값을 참조하게 되는 가시성 이슈와 스레드간의 값을 덮어씌우게 되는 동시접근 이슈를 해결할 수 있는 방법으로 jdk의 AtomicReference를 이용하여 LocalDatetime을 업데이트해보겠다.

import java.util.concurrent.atomic.AtomicReference;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;

@Component
@Slf4j
public class Schedule {
    private final AtomicReference<LocalDateTime> prevTime = new AtomicReference<>();

    @Scheduled(fixedRate = 5000)
    @Async
    public void asyncMethod() {
            LocalDateTime periodStart = parseDate(migFromDate);
            LocalDateTime periodEnd = periodStart.plusDays(1);
            if(prevTime.get() != null) {
                periodStart = prevTime.getAndSet(periodStart.plusDays(1));
                if(periodStart.equals(parseDate(migToDate))) return;
                periodEnd = periodStart.plusDays(1);
            }
    }
}

참고