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는 참조가 원자적으로 업데이트되도록 하여, 여러 스레드에서 업데이트 시 경합 조건을 방지할 수 있다.