원자적 연산이란?
컴퓨터 과학에서 사용하는 원자적 연산(atomic operation)의 의미는 해당 연산이 더 이상 나눌 수 없는 단위로 수행된 다는 것을 의미한다. 즉, 원자적 연산은 중단되지 않고, 다른 연산과 간섭 없이 완전히 실행되거나 전혀 실행되지 않는 성질을 가지고 있다. 쉽게 이야기해서 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산이라는 뜻이다.
참고: 과거에 원자는 더 이상 나눌 수 없는 가장 작은 단위로 여겨졌다. 그래서 더는 나눌 수 없다는 뜻으로 **원자적 연산**이라는 단어를 사용한다. 물론 현대 물리학에서는 원자가 더 작은 입자들로 구성되어 있다는 것이 밝혀졌다. 하지만 원자적 연산이라는 단어는 그대로 사용한다.
예를 들어서 다음과 같은 필드가 있을 때, (volatile int i = 0;) 다음 연산은 둘로 쪼갤 수 없는 원자적 연산이다. (i = 1) 왜냐하면 이 연산은 다음 단 하나의 순서로 실행되기 때문이다. 1. 오른쪽에 있는 1의 값은 왼쪽의 i 변수에 대입한다. 하지만 다음 연산은 원자적 연산이 아니다.(i = i + 1;) 왜냐하면 이 연산은 다음 순서로 나누어 실행되기 때문이다.
- 오른쪽에 있는 i의 값을 읽는다. i의 값은 10이다.
- 읽은 10에 1을 더해서 11을 만든다.
- 더한 11을 왼쪽의 i 변수에 대입한다.
원자적 연산은 멀티스레드 상황에서 아무런 문제가 발생하지 않는다. 하지만 원자적 연산이 아닌 경우에는 synchronized 블록이나 Lock 등을 사용해서 안전한 임계 영역을 만들어야 한다.
i++ 과 같은 연산도 원자적 연산이 아니다. 왜냐하면 이 연산은 앞서 살펴본 i = i + 1을 축약한 것이기 때문이다. 결과적으로 i++ 은 i = i + 1와 똑같이 동작한다.
시작
원자적이지 않은 연산을 멀티스레드 환경에서 실행하면 어떤 문제가 발생하는지 코드로 알아보자. IncrementInteger는 숫자 값을 하나씩 증가시키는 기능을 제공한다. 예를 들어서 지금까지 접속한 사용자의 수 등을 계산할 때 사용할 수 있다.

- IncrementInteger는 값을 증가하는 기능을 가진 숫자 기능을 제공하는 인터페이스다.
- increment() : 값을 하나 증가
- get() : 값을 조회

- IncrementInteger 인터페이스의 가장 기본 구현이다.
- increment()를 호출하면 value++ 를 통해서 값을 하나 증가한다.
- value 값은 인스턴스의 필드이기 때문에, 여러 스레드가 공유할 수 있다. 이렇게 공유 가능한 자원에 ++와 같은 원자적이지 않은 연산을 사용하면 멀티스레드 상황에 문제가 될 수 있다.

- THREAD_COUNT 수만큼 스레드를 생성하고 incrementInteger.increment()를 호출한다.
- 스레드를 1000개 생성했다면, increment() 메서드도 1000번 호출하기 때문에 결과는 1000이 되어야 한다.
- 참고로 스레드가 너무 빨리 실행되기 때문에, 여러 스레드가 동시에 실행되는 상황을 확인하기 어렵다. 그래서 run() 메서드에 sleep(10)을 두어서, 최대한 많은 스레드가 동시에 increment()를 호출하도록 한다.

- 실행 결과를 보면 기대한 1000이 아니라 다른 숫자가 보인다. 아마도 실행 환경에 따라서 다르겠지만 1000이 아니라 조금 더 적은 숫자가 보일 것이다. 물론 실행 환경에 따라서 1000이 보일 수도 있다.
- 이 문제는 앞서 설명한 것처럼 여러 스레드가 동시에 원자적이지 않은 value++ 을 호출했기 때문에 발생한다.
Volatile, Synchronized

- BasicInteger와 같고 volatile 만 적용해 주었다.


- 실행 결과를 보면 VolatileInteger 도 여전히 1000이 아니라 더 작은 숫자가 나온다.
- volatile 은 여러 CPU 사이에 발생하는 캐시 메모리와 메인 메모리가 동기화되지 않는 문제를 해결할 뿐이다.
- volatile을 사용하면 CPU의 캐시 메모리를 무시하고, 메인 메모리를 직접 사용하도록 한다. 하지만 지금 이 문제는 캐시 메모리가 영향을 줄 수는 있지만, 캐시 메모리를 사용하지 않고, 메인 메모리를 직접 사용해도 여전히 발생하는 문제이다.
- 이 문제는 연산 자체가 나누어져 있기 때문에 발생한다. volatile 은 연산 차제를 원자적으로 묶어주는 기능이 아니다.
- 이렇게 연산 자체가 나누어진 경우에는 synchronized 블록이나 Lock 등을 사용해서 안전한 임계 영역을 만들어야 한다.

- value++ 연산은 synchronized를 통해 임계 영역 안에서 안전하게 수행된다. 쉽게 이야기해서 한 번에 하나의 스레드만 해당 연산을 수행할 수 있다.


- synchronized를 통해 안전한 임계 영역을 만들고 value++ 연산을 수행했더니 정확히 1000이라는 결과가 나왔다.
- 1000개의 스레드가 안전하게 value++ 연산을 수행한 것이다.
AtomicInteger
자바는 앞서 만든 SyncInteger와 같이 멀티스레드 상황에서 안전하게 증가 연산을 수행할 수 있는 AtomicInteger라는 클래스를 제공한다. 이름 그대로 원자적인 Integer라는 뜻이다.

- new AtomicInteger(0) : 초기값을 지정한다. 생략하면 0부터 시작한다.
- incrementAndGet() : 값을 하나 증가하고 증가된 결과를 반환한다.
- get() : 현재 값을 반환한다.


- 실행 결과를 보면 AtomicInteger를 사용하면 MyAtomicInteger의 결과도 1000인 것을 확인할 수 있다. 1000개의 스레드가 안전하게 증가 연산을 수행한 것이다.
- AtomicInteger는 멀티스레드 상황에 안전하고 또 다양한 값 증가, 감소 연산을 제공한다. 특정 값을 증가하거나 감소해야 하는데 여러 스레드가 해당 값을 공유해야 한다면, AtomicInteger를 사용하면 된다.
- 참고로 AtomicInteger , AtomicLong , AtomicBoolean 등 다양한 AtomicXxx 클래스가 존재한다.
성능 테스트

- 단일 연산은 너무 빠르기 때문에 성능을 확인하려면 어느 정도 반복적인 연산이 필요하다.
- 각각 COUNT 만큼 반복해서 연산을 수행해 보자. 여기서는 1억 번 값 증가 연산을 수행했다

- BasicInteger
- 가장 빠르다.
- CPU 캐시를 적극 사용한다. CPU 캐시의 위력을 알 수 있다.
- 안전한 임계 영역도 없고, volatile 도 사용하지 않기 때문에 멀티스레드 상황에는 사용할 수 없다. 단일 스레드가 사용하는 경우에 효율적이다.
- VolatileInteger
- volatile을 사용해서 CPU 캐시를 사용하지 않고 메인 메모리를 사용한다.
- 안전한 임계 영역이 없기 때문에 멀티스레드 상황에는 사용할 수 없다.
- 단일 스레드가 사용하기에는 BasicInteger 보다 느리다. 그리고 멀티스레드 상황에도 안전하지 않다.
- SyncInteger
- synchronized를 사용한 안전한 임계 영역이 있기 때문에 멀티스레드 상황에도 안전하게 사용할 수 있다.
- MyAtomicInteger 보다 성능이 느리다.
- MyAtomicInteger
- 자바가 제공하는 AtomicInteger 를 사용한다. 멀티스레드 상황에 안전하게 사용할 수 있다.
- 성능도 synchronized , Lock(ReentrantLock)을을 사용하는 경우보다 1.5 ~ 2배 정도 빠르다.
SyncInteger처럼 사용하는 경우보다, AtomicInteger 가 더 빠른 이유는 무엇일까?
i++ 연산은 원자적인 연산이 아니다. 따라서 분명히 synchronized , Lock(ReentrantLock)와 락을 통해 안전한 임계 영역을 만들어야 할 것 같다. 놀랍게도 AtomicInteger 가 제공하는 incrementAndGet() 메서드는 락을 사용하지 않고, 원자적 연산을 만들어낸다.
'멀티스레드와 동시성' 카테고리의 다른 글
| Ch10. 동기화와 원자적 연산(CAS) - CAS 락 구현 (0) | 2024.09.15 |
|---|---|
| Ch10. 동기화와 원자적 연산(CAS) - CAS 연산 (0) | 2024.09.15 |
| Ch09. 생산자 소비자 문제 - BlockingQueue (0) | 2024.08.27 |
| Ch09. 생산자 소비자 문제 - 스레드의 대기 (0) | 2024.08.24 |
| Ch09. 생산자 소비자 문제 - 생산자/소비자 대기 공간 분리 (0) | 2024.08.24 |