Java - Atomic Type

동시성 제어

Posted by Yan on September 8, 2024

Java Atomic Type에 대해서

최근 메세지 큐 시스템을 제어하는 프레임워크 repo를 보다가 AtomicInteger라는 타입을 보게 되었다. 처음 보는 타입이라 흥미로워서 서치해보았다.

멅티 스레딩 프로그래밍에서 고려할 동시성 문제를 고려하면, 경쟁상태race condition을 고려해야 한다. 그럴 때 사용하는 것이 Atomic Type이다.

개념

  • 여러 스레드가 공유된 변수를 동시에 업데이트하거나 수정해야 할 때, 두 개 이상의 스레드가 변수를 동시에 접근하고 수정하는 상황에서 데이터 일관성을 보장한다.
  • Atomic Type은 java.util.concurrent.atomic 패키지에 있다.
  • 종류: AtomicInteger, AtomicLong, AtomicBoolean, AtomicReference
  • synchronized 블록이나 락을 걸지 않고도 단일 변수에 대해 스레드 안전한 연산을 수행할 수 있게 해준다.

synchronized 블록이란?

  • 자원에 한 번에 하나의 스레드만 접근할 수 있도록 락(lock)을 거는 방식.
  • 여러 스레드가 하나의 자원에 접근할 때, 동시 접근으로 인해 발생할 수 있는 데이터 불일치 문제(경합 상태)를 방지하기 위해 사용된다.
동작방식
  • synchronized 키워드를 사용하여 특정 코드 블록을 감싸면, 해당 코드가 실행되는 동안은 다른 스레드가 그 블록에 접근하지 못하도록 막는다.
  • 자원이 공유될 때 이 블록을 사용하면 하나의 스레드만 해당 자원에 접근할 수 있고, 다른 스레드는 현재 스레드가 작업을 완료할 때까지 기다린다.
1
2
3
4
5
6
7
8
9
10
11
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}
  • 한 스레드가 increment()를 호출하여 count 값을 증가시키는 동안 다른 스레드는 해당 블록에 들어올 수 없고, 첫 번째 스레드가 작업을 끝낼 때까지 기다린다.
  • 이렇게 하면 여러 스레드가 count를 동시에 업데이트하는 상황에서 데이터 불일치가 발생하지 않도록 보호할 수 있다.
synchronized의 단점
  • 성능 저하: synchronized 블록은 한 번에 하나의 스레드만 자원에 접근할 수 있도록 제한하므로, 여러 스레드가 동시에 자원을 사용하려 할 때 성능 병목(bottleneck)이 발생할 수 있다.
  • 경쟁 상황: 여러 스레드가 락을 얻기 위해 기다리는 동안 경합이 발생할 수 있으며, 특히 락을 잡고 있는 스레드가 오래 작업을 하면 대기 시간이 길어진다.
Atomic type과의 차이점
  • Atomic 타입은 synchronized 블록 대신 비교 및 교환(Compare and Swap, CAS)과 같은 하드웨어 지원 원자적 연산을 사용하여 자원에 대한 동시 접근을 제어한다.
  • 이는 락 없이도 스레드 안전성을 제공하므로, 성능 면에서 더 유리한 경우가 많다.
  • synchronized 블록은 자원 접근을 블로킹하는 방식이고, Atomic 타입은 자원 접근을 비블로킹으로 처리하는 방식이다.

Atomic Type 주요 특징

  • 락 프리 매커니즘: Atomic 타입의 연산은 낮은 수준의 원자적 머신 명령 CAS Compare And Swap을 사용하여 구현되며, 이를 통해 블로킹 없이 업데이트가 가능하다.
    • CAS를 하드웨어(CPU)의 도움을 받아 한 번에 단 하나의 스레드만 변수의 값을 변경할 수 있도록 제공
  • 스레드 안정성: incrementAndGet()compareAndSet()같은 연산을 락 없이도 스레드 안전하게 수행할 수 있다.
  • 효율성: 전통적인 synchronized 블록보다 가벼운 원자적 업데이트가 필요할 때 효율적이다.
CAS 방식
  • 변수의 값을 변경하기 전에 기존에 가지고 있던 값이 내가 예상하던 값과 같은 경우에만 새로운 값으로 할당하는 방법

Atomic Type 예시

AtomicBoolean

  • public final boolean compareAndSet(boolean expect, boolean update)
    • expect : the expected value
    • update : the new value
    • return : true if successful. False return indicates that the actual value was not equal to the expected value.
  • public final boolean getAndSet(boolean newValue)
    • Atomically sets to the given value and returns the previous value.
    • parameter: new value
    • return : previous value

AtomicInteger

예시) 웹 서비스에서 특정 엔드포인트가 얼마나 자주 호출되었는지 추적하는 카운터를 여러 스레드가 동시에 접근하는 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.concurrent.atomic.AtomicInteger;

public class EndpointHitCounter {
    private AtomicInteger hitCount = new AtomicInteger(0);

    public void incrementHitCount() {
        hitCount.incrementAndGet();
    }

    public int getHitCount() {
        return hitCount.get();
    }
}
  • incrementAndGet()은 원자적 연산으로, 여러 스레드가 동시에 incrementHitCount()를 호출하더라도 카운터가 올바르게 업데이트되도록 보장힌다.
  • 명시적으로 동기화하거나 락을 사용할 필요가 없으므로, 연산이 빠르고 효율적이다.

AtomicReference

  • 원자적(atomic)으로 참조(reference) 변수를 조작할 수 있는 클래스

예시) 다중 스레드 환경에서 공유된 설정 객체를 락 없이 업데이트하고 싶을 때

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.concurrent.atomic.AtomicReference;

class Configuration {
    private String setting;
    // Constructor, getters, and setters
}

public class ConfigUpdater {
    private AtomicReference<Configuration> config = new AtomicReference<>(new Configuration());

    public void updateConfiguration(Configuration newConfig) {
        config.set(newConfig);
    }

    public Configuration getCurrentConfig() {
        return config.get();
    }
}

  • AtomicReference는 참조가 원자적으로 업데이트되도록 하여, 여러 스레드에서 업데이트 시 경합 조건을 방지할 수 있다.
reference

An Introduction to Atomic Variables in Java