백엔드 엔지니어 이재혁
[Java] Atomic과 CAS 연산 본문
Atomic(원자적) 연산이란, 더이상 쪼갤 수 없는 연산을 의미한다.
원자적 연산의 예시
`i=10;` 같은 것
원자적 연산이 아닌 것
`i++;` (`i = i + 1;`)
- i의 값을 읽는 과정
- 그 값에 1을 더하는 과정
- i에 그 값을 다시 대입하는 과정
세 가지 단계로 나눌 수 있다.
다음과 같이 `i++;` 작업을 두 스레드가 동시에 할 때를 살펴보자
처음에 i = 0이라고 가정하겠다.
스레드1: i = i + 1 연산 수행
스레드2: i = i + 1 연산 수행
스레드1: i의 값을 읽는다. i는 0이다.
스레드2: i의 값을 읽는다. i는 0이다.
스레드1: 읽은 0에 1을 더해서 1을 만든다.
스레드2: 읽은 0에 1을 더해서 1을 만든다.
스레드1: 더한 1을 왼쪽의 i변수에 대입한다.
스레드2: 더한 1을 왼쪽의 i변수에 대입한다.
결과: i의 값은 1이다.
멀티 스레드 상황에서는 위와 같이 원자적 연산이 아닌 연산을 동시에 실행하면 동시성 이슈가 발생할 수 있다.
정수 연산은 일상적으로 자주 사용하는 연산인 만큼 이런 단순한 `i++;` 코드에는 락을 사용하지 않는 방법이 있다면 좋을 것이다.
현대 CPU에는 그것을 도와주기 위해 하드웨어 단에서 원자적으로 만들어주는 CAS (Compare-And-Swap 혹은 Set) 연산이 존재한다.
Atomic 클래스
CPU의 CAS 연산을 사용하는 것이 `Atomic` 클래스이다.
이 클래스는 더하고 빼는 작업을 원자적 연산으로 처리해주는 기능을 제공한다.
`AtomicInteger`, `AtomicBoolean`, `AtomicLong` 등이 있다.
public class MyAtomicInteger implements IncrementInteger {
AtomicInteger atomicInteger = new AtomicInteger(0);
@Override
public void increment() {
atomicInteger.incrementAndGet();
}
@Override
public int get() {
return atomicInteger.get();
}
}
`AtomicInteger` 클래스는 `incrementAndGet` 메서드를 통해 1 증가 연산을 CAS 연산을 잘 이용해 락 없이 처리해주는 기능을 가지고 있다.
성능 비교
| 사용한 Integer | 소요 시간 ( i++ 연산 스레드 10억개 생성 및 실행) | 동시성 이슈 해결 |
| 기본 Integer (private int value) | 183ms | X |
| Volatile 사용 (volatile private int value) | 507ms | X |
| Synchronized 사용 (increment 메서드에 적용) | 991ms | O |
| AtomicInteger.incrementAndGet() 사용 (synchronized 없이*) |
576ms | O |
* `volatile` 키워드는 `AtomicInteger`의 내부 `value` 멤버 변수에 적용되어 있다.
CAS 연산을 활용했을 때, Synchronized를 사용한 것보다 약 42% 짧은 시간만에 작업을 완료했다.
CAS 연산의 작동 방식
CAS 연산을 실행하면, CPU는 명령이 끝날 때까지 자체적으로 현재 작업 중인 메모리 주소에 다른 스레드가 접근하지 못하도록 막는다.
Compare And Swap (혹은 Set)의 이름에서 알 수 있듯이, 이 연산은 비교하고 변경하는 기능을 가지고 있고, 이 것을 CPU 단에서 하나의 원자적 연산으로 처리해준다. 논리적으로는 두 개의 기능으로 나눌 수 있지만 CPU에서 지원하는 특수한 기능이라고 보면 된다.
락과 비교
락과 비슷한 개념으로 들려 헷갈릴 수 있다. 하지만 락과는 조금 다르다.
락은 소프트웨어적 관리, CAS 연산은 하드웨어적 관리라는 방식으로도 생각해볼 수 있을 것 같다.
가장 큰 차이점은 바로 스레드의 상태 변화에 있다.
| 상황 | 동시 접근이 차단된 구역에 멀티스레드가 동시에 접근 시도 중 |
| 락 사용 | 하나의 스레드만 "RUNNABLE" 상태 나머지 스레드는 "BLOCKED" 혹은 "WAITING" 등의 상태로 CPU 스케쥴러에서 빠짐 |
| CAS 연산 사용 | 모든 스레드는 "RUNNABLE" 상태 CPU 스케쥴러에 모두 들어가있고, CPU가 CAS 연산을 수행하는 찰나의 순간에만 동시 접근이 차단됨 |
정리
락을 사용하는 경우,
락을 관리하는 비용이 들어간다. (확인, 획득, 대기, 반납 등)
스레드의 상태를 변화시키는 컨텍스트 스위칭 오버헤드도 들어간다.
반면, CAS 연산은 개발자가 스레드를 직접 제어하지 않으면서 스레드-안전 하게 원자적 연산을 제공하는 기능이다.
'Java' 카테고리의 다른 글
| [JAVA] CAS 락 구현 (0) | 2025.06.10 |
|---|---|
| [Java] CAS 연산 활용 (1) | 2025.06.09 |
| [Spring] Spring의 주입 방식 (0) | 2025.06.09 |
| [Spring] Spring이 작동하는 방식 (0) | 2025.06.08 |
| [Spring] Spring이 등장한 이유 (0) | 2025.06.08 |