[JAVA] Atomic 과 동시성

신명철·2022년 10월 31일
1

JAVA

목록 보기
14/14

들어가며

해당 포스트는 Effective JAVA Item.78Atomicvolatile 에 대해서 공부한 과정을 기록하기 위해 작성했다.

Atomic

공유 중인 가변 데이터에 여러 개의 스레드가 동시에 접근하게 되면 Race Condition이 발생할 수도 있고, 가시성 문제가 발생할수 있다. 일반적으로 JAVA 에서는 동기화에 대한 문제를 synchronized, volatile, Atomic.class 를 사용해서 해결한다.

synchronized 는 선언된 메서드의 코드 섹션 전체에 락을 걸고 접근하는 스레드들은 block or suspended 상태로 변경되게 된다. 스레드들이 blocking 되는 과정과 다시 resuming 되는 과정에서 시스템의 자원을 소모하게 된다. 100개의 스레드가 동시에 접근을 한다면, 99개의 스레드가 이러한 과정을 거치게 되는 것이다. 바로 이 부분에서 성능 저하가 발생한다.

Atomic의 핵심은 이러한 소모 비용을 줄이는 non-blocking 방식을 사용한다는 점에서 차이점이 존재한다. 즉, 어떤 스레드도 suspended 되지 않기 때문에 context switch를 피할 수 있다.

CAS(Compare And Set) 알고리즘

Atmoic의 동작 과정은 다음과 같은 방식의 CAS 알고리즘을 사용한다.

  1. 인자로 기존 값과 변경할 값을 전달한다.
  2. 기존 값이 현재 메모리가 가지고 있는 값과 같다면 변경할 값을 반영해서 true를 반환한다.
  3. 그렇지 않다면, 현재 메모리가 가지고 있는 값을 반영하지 않고 false를 반환한다.

기존 값과 현재 메모리가 가지고 있는 값이 다른 경우는 어떤 경우일까? 아래 그림을 보자.

스레드는 메모리의 데이터를 가져다가 캐시에 올려놓고 연산을 하게 된다. 연산을 마치면 연산 결과를 다시 메모리에 반영시키는 것이다. 만약 연산을 수행하는 스레드가 여러 개라면 메모리의 데이터스레드의 데이터가 불일치하는 상황이 발생하는데 이러한 경우 false 를 반환한다는 것이다.

AtmoicInteger.incrementAndGet()

AtomicIntegerincrementAndGet 메서드를 보면 내부적으로 CAS 알고리즘을 구현하고 있는 것을 알 수 있다.

public class AtomicInteger extends Number implements java.io.Serializable {

        private static final Unsafe U = Unsafe.getUnsafe();
        private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
        private volatile int value;

        public final int incrementAndGet() {
                return U.getAndAddInt(this, VALUE, 1) + 1;
        }
}

public final class Unsafe {
        @HotSpotIntrinsicCandidate
        public final int getAndAddInt(Object o, long offset, int delta) {
                int v;
                do {
                        v = getIntVolatile(o, offset);
                } while (!weakCompareAndSetInt(o, offset, v, v + delta));
                        return v;
        }
}

AtomicInteger는 weakCompareAndSetInt 의 return 값이 true 로 반환할 될 때까지 무한 루프를 돌면서 기다린다. 그러면 여기서 의문이 하나 생긴다. 의미없이 무한 루프를 돌면서 CPU를 붙잡고 있는데 synchronized 보다 나을까 하는 의문이다.

앞서 언급했듯 synchronized 는 스레드가 suspendingresuming 하는데에 자원 소모가 발생하고, 이 과정은 lock이 걸린 모든 스레드가 공통적으로 겪는 과정이다. 그렇기 때문에 비록 무한 루프를 돌지만 true를 return 받는 즉시 다음 작업을 수행할 수 있는 Atomic이 성능적으로 우수하다.

  • 여기서 눈에 띄는 점은 int value에 붙어있는 volatile 이라는 키워드다.

volatile?
volatile 은 배타적 수행과는 상관없지만 항상 최근에 기록된 값을 읽는 것을 보장해준다. 메인 메모리에 있는 값을 직접 읽어서, 캐시 메모리를 사용하는 구조상 발생할 수 있는 가시성의 문제를 해결해줄 수 있다.

Atomic의 volatile 한정자

class AtomicInteger {

    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    public AtomicInteger() {
    }

    public final int get() {
        return value;
    }

    public final void set(int newValue) {
        value = newValue;
    }

	...
}
  • get()과 set()은 그 자체로 원자 연산이다.

get()과 set()은 그 자체로 atomic 연산이기 때문에 race condition이 발생하지 않는다. 따라서 volatile 한정자를 사용해서 가시성에 대한 문제만을 해결해준것이다.


참고

profile
내 머릿속 지우개

0개의 댓글