[Java/Kotlin] Concurrent API - Atomic type

Jay·2021년 3월 17일
0

Java&Kotlin

목록 보기
27/30
post-thumbnail

Atomic

우선, Atomicity(원자성)의 개념을 알아야 한다.
Atom = 더 이상 쪼갤 수 없는 단위

처음부터 끝까지 실행이 되거나 아예 아무것도 실행되지 않던가 하는 Action이다.

쇼핑몰에서의 '결제' -> '상품 재고 수량 변경' 은 하나만 이뤄져서는 안되고 둘 다 실행되거나 둘 다 실행되지 않아야 한다.
결제가 되었다면 재고에서 빼야 나중에 재고가 없는데 주문을 받지 않을 수 있다.
"중간에 멈춰선 안되는 연산의 중요성"?

여러 개의 작업을 쪼개서 번갈아가면서 실행하는 멀티 스레드 환경에서 비 원자 연산이 돌아가면 위와 같은 문제가 생길 수 있다.
작업 단위가 분리되면 안되는 연산 = Atomic operation이 필요하다.

멀티스레드 환경에서 동시성 문제를 제어할 수 있도록 java는 여러 형태로 Atomic Operation을 지원한다.
대표적으로 volatile, synchronization, Atomic 3가지가 있다.
Atomic Type의 경우, 예약어에 해당하는 앞의 두개와 다르게 java.util.concurrent.atomic 패키지에 정의된 클래스이다.

Atomic type

  • 단일 변수에 대해 Atomic Operations를 지원한다.
  • Wrapping Class의 일종으로 참조타입과 원시타입 두 종류의 변수에 모두 적용 가능하다.
  • 사용 시 내부적으로 Compare-And-Swap(CAS) 알고리즘을 사용해서 lock없이 동기화를 처리할 수 있다.

주요 Class

  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicIntegerArray
  • AtomicDoubleArray

주요 Method

  • get() : 현재 값을 반환
  • set(newValue) : newValue로 값을 업데이트 한다.
  • getAndSet(newValue) : 원자적으로 값을 업데이트하고 원래의 값을 반환한다.
  • compareAndSet(expect, update) : 현재 값이 예상하는 값과 동일하다면 값을 update 한 후 true를 반환한다. 예상 값과 다르다면 update는 생략하고 false 반환.
  • Number 타입의 경우 값의 연산을 할 수 있도록 addAndGet(delta), getAndAdd(delta), getAndDecrement(), getAndIncrement(), incrementAndGet() 등의 메서드를 추가로 제공한다.

Sample

class MyLock {
    private boolean locked = false;

    public boolean tryLock() {
        if (!locked) {
            locked = true;
            return true;
        }
        return false;
    }
}

MyLock 클래스에는 여러 개의 스레드가 lock을 얻기 위해 경쟁할 때 lock의 상태 관리하는 로직이 정의되어 있다.
위의 코드에서 개발자가 의도한 동작은 여러개의 스레드 중 오직 하나의 스레드만 락을 얻게 되는 것이다.

class TestRunnable implements Runnable {
    private final MyLock myLock;

    public TestRunnable(MyLock myLock) {
        this.myLock = myLock;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " - " + myLock.tryLock());
    }
}

실행하는 순간 lock획득을 시도하는 Runnable의 구현체를 정의한다.

public class Main {
    public static void main(String[] args) {
        final MyLock myLock = new MyLock(); // shared resource

        for (int i = 0; i < 100_00; i++) {
            new Thread(new TestRunnable(myLock)).start();
        }
    }
}

main에선 하나의 Lock 객체를 공유하는 10만개의 스레드를 생성하고 실행한다.

위에서 작성한 tryLock의 원자성이 보장되어야 한다.
변수 선언 시, 타입을 boolean에서 AtomicBoolean으로 바꿔보자.
locked가 false일 때만 값을 true로 변경하고 이미 값이 true라면 set은 생략한다.
compareAndSet 내부에서 값을 원자적으로 갱싱하기에 동시성 문제가 해결된다.

아래와 같이 바꿔보자.

class MyLock {
    private AtomicBoolean locked = new AtomicBoolean();

    public boolean tryLock() {
        if (!locked.get())  {
            // 비용이 큰 작업을 수행한다
            for (int i = 0; i < 100_000; i++) { }
        }

        return locked.compareAndSet(false, true);
    }
}

위에서 잠깐 언급한 Atomic type이 synchronized를 쓰지 않고도 동시성을 유지하는 CAS 알고리즘을 간단하게 정리하자면
Compare-And-Set이란 이름처럼 값을 넣었을 때, 해당 값과 메모리 상의 값이 일치하는 지 비교해서 다르다면 중간에 다른 스레드가 들어온 것으로 판단하여 write를 실패시킨다.
내부적으론 do-while문으로 되어 있어서 맨 처음 값만 write가 가능하고 이후는 체킹을 반복하게 된다.

Reference

profile
developer

0개의 댓글