
해당 연산이 더 이상 나눌 수없는 단위로 수행된 다는 것을 의미한다.
위의 1번의 예시는 원자적 연산의 논리를 가지며 2번은 그렇지 않다. 1번의 경우는 오른쪽에 있는 1의 값을 왼쪽의 변수에 담는 하나의 행위를 하고있고, 2번의 경우는 i를 읽고, 읽은 i에 1을 더하고 그 결과를 다시 i에 담는 3가지 프로세스가 존재 한다.
멀티 스레드 환경에서 문제가 되는 것은 2번의 경우이다. 쓰레드A가 i를 읽고 i에 값을 더하려는 그 시점 직전에 쓰레드B가 i의 값을 바꾸었다면, 쓰레드A의 값을 더하는 행위는 읽은 값과 다른 값을 보이는 값에 1을 더하는 행위를 하므로 최종적인 결과가 예상된 결과와 달라지게 된다.
그러므로 원자적 연산이 아닌 경우에는 synchronized나 Lock등을 사용해서 안전한 임계 영역을 만들어야 한다.
다음의 4가지 상황에 대한 속도 측정을 진행해본다.
i = i + 1의 기능을 가진 increment() 메서드를 1억번 실행한다.
상황 1 : int value 에 대해 value++ 1억번 실행
상황 2 : volatile int value에 대해 value++ 1억번 실행
상황 3 : int value에 대해 syncronized value++ 1억번 실행
상황 4 : AtomicInteger에 대해 atomicInteger.incrementAndGet 1억번 실행
결과는 다음과 같다.

i = i + 1이라는 멀티 스레드 환경에서 임계영역(i는 공유변수)에 해당되는 조건에 대해 문제가 생길 수 있는 상황1은 매우 빠른 속도를 보여준다. Volatile은 임계영역에 대한 동기화를 구현하지는 않지만 BasicInteger와의 차이를 보기 위해 설정했다. 약 60배 차이가 난다.
주목해야할 것은 syncronized를 사용한 것과 AtomicInteger를 사용한 경우에 해당한다. 둘은 임계영역의 동기화 문제를 해결해주지만 방식은 다른데, 전자의 경우 모니터 락을 이용한 방법이며 AtomicInteger는 모니터 락을 사용하지 않는 경우이다. BasicInteger 방법까지의 속도는 아니지만 AtomicInteger가 더 빠른 모습을 나타내고 있다.
락 기반 동기화 처리는 성공적으로 임계영역에 대한 멀티쓰레드 동기화 문제를 처리하지만, 복잡한 데에 단점이 있다.(synchronized, Lock(ReentrantLock))
락을 획득하고 해제 하는데 시간이 소요되며 이 절차가 연산마다 들어가야한다는 반복의 문제가 존재한다. 반복의 문제를 크게 보이게 하기 위해 위의 성능 테스트에서 1억번의 연산을 시도했었던 것이다.
CAS(Compare- And-Swap, Compare-And-Set)연산은 락을 사용하지 않기에 락 프리(lock-free)기법이라고도 한다. CAS 연산은 락을 완전히 대체해주지는 못하며, 작은 단위의 일부 영역에 적용할 수 있다.
AtomicInteger가 가진 메서드인 compareAndSet(value1, value2)은 AtomicInteger가 value1일 경우 value2를 설정한다는 간단한 논리로직이다.
간단하지만 이 논리로직은 원자적 연산이 아닌 것 처럼 보인다. 값을 먼저 확인하고 그 후 설정을 결정하기에 확인 시점과 결정 시점이 나뉘기 때문이다.
하지만 이 메서드는 원자적으로 실행된다.
CAS 연산은 하드웨어(CPU)의 지원을 이용한다. CPU차원에서 특별하게 하나의 원자적인 연산으로 묶어 실행하는 것이다.(대부분의 현대 CPU는 CAS연산을 위한 명령어를 제공한다.)
private static int incrementAndGet(AtomicInteger atomicInteger) {
int getValue;
boolean result;
do {
getValue = atomicInteger.get();
log("getValue: " + getValue);
result = atomicInteger.compareAndSet(getValue, getValue + 1);
log("result: " + result);
} while (!result);
return getValue + 1;
}
위의 코드는 AtomicInteger의 incrementAndGet메서드를 직접 구현한 것이다. do-while문을 통해 구현하였다.
3가지 프로세스에 대해서, 값을 우선 조회하고 값을 비교할 시점에 비교가 틀렸다면 while문을 계속 돌며 비교가 맞아 떨어져서 값을 설정하는 로직까지 성공했을 경우 while문을 탈출하여 리턴되는 구조이다.
여기서 2,3 순서의 프로세스를 CAS를 통해 원자적 연산으로 묶을 수 있는 것이다.
이렇게 조회 - (비교 + 설정)을 과정을 구성하여 조회와 비교에서의 멀티쓰레드 충돌이 날 경우만 while문으로 다시 시도를 통해 극복하면 락 프리한 동기화 로직을 얻을 수 있는 것이다.
락 방식은 비관적 접근법(Lock을 획득하지 못하면 연산의 시도가 허락되지 않음.), CAS 방식은 낙관적 접근법(일단 시도, 충돌하면 재시도)의 의의를 가진다.
충돌이 많이 없는 경우에는 락 획득과 해제를 하지 않는 CAS연산이 더 성능적으로 유리할 수 있다는 것이다. 작은 연산에 있어 CPU연산은 매우 빠르게 처리되기 때문에 충돌이 자주 발생하지 않는다.

1000개의 쓰레드를 생성해서 increment()를 시도했을 때 멀티쓰레드 환경에서 문제가 되는 위의 두 케이스에서도 1000개중 약 10~20개 정도의 충돌을 보인 점으로 이를 알 수 있다.
public class SpinLock {
private final AtomicBoolean lock = new AtomicBoolean(false);
public void lock() {
log("락 획득 시도");
while (!lock.compareAndSet(false, true)) {
log("락 획득 실패 - 스핀 대기");
}
log("락 획득 완료");
}
public void unlock() {
lock.set(false);
log("락 반납 완료");
}
}
Lock 인터페이스나 syncronized 전략을 사용하지 않는 CAS 락을 위와 같이 구현할 수 있다. CAS 활용 락 방식은 사실 Lock이 존재하지 않으며 단순히 while문을 반복해서 계속해서 체크하는 것이다. 그렇기에 스레드가 "오랫동안" CAS 락을 획득하지 못하면 오히려 비용이 더 발생할 수 있다.
Runnable task = new Runnable() {
@Override
public void run() {
spinLock.lock();
try {
sleep(1);
log("비즈니스 로직 실행");
} finally {
spinLock.unlock();
}
}
};
위에서 "오랫동안"에 대한 표현은 인간관점이 아니라 CPU관점이다 CPU는 1초에 수억건의 연산이 가능하기에 1ms도 CPU에게는 긴 시간일 수 있다.
위의 코드를 참고할 때, 스레드의 sleep을 1ms만 주어도 스레드1이 락을 획득하고 sleep하기 때문에 다른 스레드의 대기에 대한 while문이 돌아가는 횟수가 굉장히 많아진다. 그렇기에 CAS 락 방식을 효율적으로 이용하기 위해서는 스레드1이 빠르게 로직을 끝마쳐야한다. 그러지 못할 가능성이 높은 데이터베이스의 응답을 받는다던지, 다른 서버의 응답을 받는 로직에 대해서 CAS 락 방식은 비효율적일 가능성(CPU만 축낸다.)이 높다.

이러한 CAS로 구현된 락 방식을 스핀 락(Spin Lock)이라고도 한다.
일반적으로 동기화 락을 사용하고 아주 특별한 경우에 한하여 CAS를 활용한다. 버스에서 자리가 남는데 서있는 행위가 CAS이며 앉아있는 행위가 동기화 락을 사용하는 것이 되겠다. 버스에 사람이 없는데 우리가 잠시 서있는 것은 내리기 직전에 해당하며 만약 내리는 지점을 착각하여 한번 더 서있게 된다면, 앉아있는 것보다 같은 시간 대비 더 많은 에너지를 낭비하게 될 것이다.
실무적인 관점에서 대부분의 애플리케이션들은 공유자원 사용 시 충돌할 가능성보다 충돌하지 않을 가능성이 훨씬 높다. 탑 티어의 서비스의 피크시간에 대해 백 만건의 결제가 들어오는 서비스라고 가정하더라도, 이는 1초에 277건 정도이며 1시간을 놓고 충돌가능성이 있는 건을 고려한다면 몇 십건 정도일 것이다. 만약 결제 시도 수를 카운팅하는 간단한 작업을 코딩해야한다면 우리는 락을 걸고 시작하는 것보다, CAS와 같은 낙관적 방식이 더 나은 성능을 보일 것이다.
CAS 연산을 직접 사용할 일은 거의 없다. CAS 연산을 사용하는 라이브러리들을 잘 사용하기 위해 이해를 다지는 정도로 CAS의 논리에 대해 자연스러움만 인지하면 될 것 같다.