ABOUT ME

나의 개발 여정들의 기록들... https://github.com/hwangdaesun

Today
Yesterday
Total
  • Atomic에 대해서 알아보자
    스터디/JAVA 2022. 12. 26. 10:11
    728x90

     

    Java를 공부하다가 Atomic(원자성) 개념을 알게 되었습니다. 중요한 개념이기에 정리하고 넘어가려고 합니다.

     

    Atomic이란?

    "Atomic"의 사전적 의미는 원자적 연산(Atomic Operation)입니다. 즉, 하나의 연산 단위로써, 다른 연산의 간섭 없이 단독으로 수행되는 연산을 의미합니다. Java에서는 AtomicInteger, AtomicLong 등과 같은 Atomic 관련 클래스를 제공하고 있습니다.

    그렇다면 Java에서는 왜 Atomic 클래스를 제공하는 걸까요?


    동시성 프로그래밍에서 발생할  수 있는 문제



    컴퓨터에는 CPU와 RAM이 있으며, CPU는 연산 속도를 높이기 위해 Cache Memory를 사용합니다. 즉, CPU가 데이터를 처리할 때 RAM뿐만 아니라, CPU Cache Memory에서도 데이터를 읽고 연산합니다.

     

    그렇다면 CPU가 2개 이상이라면 어떻게 될까요?

    CPU가 여러 개라면 각 CPU마다 자체적인 Cache Memory가 존재하게 됩니다.

    다중 CPU 환경에서 발생하는 문제

    1. CPU1이 RAM에서 데이터를 읽어와 Cache Memory1에 저장한 후 연산을 수행합니다.
    2. 동시에 CPU2도 같은 데이터를 RAM에서 읽어와 Cache Memory2에 저장한 후 연산을 수행할 수 있습니다.
    3. 각 CPU가 연산을 마친 후 RAM에 결과를 반영하려고 할 때, 하나의 연산 결과가 사라질 수도 있습니다.

    즉, 동시에 여러 CPU가 같은 데이터를 처리할 경우, 어떤 값을 RAM에 반영해야 할지 모호한 문제가 발생합니다.
    이 문제는 CPU와 RAM 사이에 위치한 Cache Memory와 병렬성(Parallelism) 때문입니다.

    여러 스레드가 공유 자원에 접근하면서, 연산 결과가 충돌하거나 덮어씌워지는 동기화 문제가 발생할 수 있습니다.

    CAS 연산

     

    이러한 문제를 Atomic 클래스에서는 CAS(Compare-And-Set) 연산을 사용하여 해결합니다.

    CAS는 "비교 후 설정"이라는 의미로, 현재 CPU Cache Memory의 데이터와 RAM에 있는 데이터가 일치하는지 확인한 후, 일치하면 변경을 적용하고, 일치하지 않으면 다시 시도하는 방식입니다.

     

    이러한 방식 덕분에 여러 스레드가 공유 자원에 접근하더라도, 읽기/쓰기 작업에서 원자성이 보장될 수 있습니다.

    private static int incrementAndGet(AtomicInteger atomicInteger) {
        int getValue;
        boolean result;
        do {
            getValue = atomicInteger.get(); 
            log("getValue: " + getValue);
    		
            // CAS (Compare-And-Swap) 연산 수행 
            // getValue와 메모리에 저장된 값을 비교
            result = atomicInteger.compareAndSet(getValue, getValue + 1); 
            log("result: " + result);
    
        } while (!result); // CAS가 실패하면 반복
    
        return getValue + 1;
    }

     

     

    compareAndSet 메서드의 첫 번째 매개변수는 CPU Cache Memory에 저장된 값입니다.

    이 메서드는 현재 메모리에 저장된 값(getValue)과 비교하여,

    • 두 값이 일치하면 getValue + 1의 값을 메모리에 반영합니다.
    • 일치하지 않으면 변경하지 않고, 다시 while 문을 반복하면서 연산을 재시도합니다.

    즉, CAS 연산 방식은 "대부분의 경우 충돌이 발생하지 않을 것"이라는 가정하에 동작하는 방식입니다.


     

    락 vs CAS 비교

    CAS를 사용하는 방식은 충돌이 드물게 발생하는 환경에서 효과적입니다. 이 방식은 스레드를 BLOCKED 또는 WAITING 상태로 만들지 않고, 계속 RUNNABLE 상태를 유지할 수 있어 락을 사용하는 방식보다 효율적입니다.

    그러나 충돌이 빈번하게 발생하는 경우, CAS 연산은 while 루프를 반복하며 계속 재시도해야 하므로, CPU 자원을 과도하게 소모할 수 있습니다. 이런 경우에는 Lock을 사용하는 방식이 더 효과적일 수 있습니다.

    언제 사용해야할까

    CAS 연산은 아래와 같은 상황일 때 사용할 수 있습니다.

    1. 스레드 상태 전환 비용을 줄이고 싶을 때
      • RUNNABLE → BLOCKED/WAITING → RUNNABLE 상태로 변경하는 것보다,CAS를 사용해 RUNNABLE 상태를 유지하며 락을 획득하는 것이 더 효율적일 때 활용
    2. 연산이 단순하고 빠르게 끝날 때
      • 값을 증가시키거나 데이터를 추가하는 등, 연산이 짧아 출돌이 나는 경우가 극히 드물 때 사용



    지금까지 Atomic에 대해 정리해보았습니다. 부족한 글이지만 읽어주셔서 감사합니다.

    728x90
Designed by Tistory.