Java ‐ CAS - thought-corner/Backend-PlayGround GitHub Wiki
CAS 연산
- 락은 특정 자원을 보호하기 위해 쓰레드가 해당 자원에 접근하는 것을 제한한다.
- 락이 걸려 있는 동안 다른 쓰레드들은 해당 자원에 접근할 수 없고, 락이 해제될 때까지 대기해야 한다.
- 또한 락 기반 접근에서는 락을 획득하고 해제하는 데 시간이 소요된다.
- 락을 사용하는 방식은 직관적이나 상대적으로 무거운 방식이라는 것을 알고 있어야 한다.
* 락이 있는가 확인한다.
* 락을 획득하고 임계 영역에 들어간다.
* 작업을 수행한다.
* 락을 반납한다.
- 예를 들어, 10,000번의 연산이 있다면 10,000번의 연산 모두 같은 과정을 반복한다.
- 이런 문제를 해결하기 위해 락을 걸지 않고 원자적이 연산을 수행할 수 있는 방법이 있는데, 이것을 CAS(Compare-And-Swap, Compare-And-Set) 연산이라고 한다.
- 이 방법은 락을 사용하지 않기 때문에 락 프리(Rock-Free) 알고리즘 기법이라고 한다. 참고로 CAS 연산은 락을 완전히 대체하는 것은 아니고 작은 단위의 일부 영역에 적용할 수 있다.
- 기본은 락을 사용하고 특별한 경우에 CAS를 적용할 수 있다고 생각하면 된다.
실행 순서 분석
- CAS 연산은 원자적이지 않은 2개의 연산을 CPU 하드웨어 차원에서 특별하게 하나의 원자적인 연산으로 묶어서 제공하는 기능이다.
- 이것은 소프트웨어가 제공하는 기능이 아니라 하드웨어가 제공하는 기능이다.
- 대부분의 현대 CPU들은 CAS 연산을 위한 명령어를 제공한다.
1. CAS(Compare-And-Swap)의 정의
- CAS는 멀티쓰레드 환경에서 락(Lock)을 걸지 않고도 공유 데이터의 원자성을 보장하기 위해 사용하는 메모리 연산이다.
- 주로
synchronized나ReentrantLock과 같은 비관적 락 대신 자원을 점유하지 않고 성공할 때까지 재시도하는 낙관적 락 방식의 기초가 된다.2. 동작 원리(3개의 인자)
- V(Memory Location) : 업데이트할 메모리 주소
- A(Expected Value) : 내가 예상하는 현재 값
- B(New Value) : 변경하고자 하는 새로운 값
메모리의 현재 값(V)이 내가 예상한 값(A)과 일치하면 새로운 값(B)으로 교체하고
true를 반환한다. 만약 일치하지 않는다면 아무 작업도 하지 않고false를 반환한다.3. 왜 CAS를 쓰는가?(장점과 성능)
- Non-blocking(Lock-free) :
synchronized키워드는 쓰레드를 Blocking 상태로 만들고 컨텍스트 스위칭 오버헤드를 발생시키지만 CAS는 쓰레드를 멈추지 않는다.- 하드웨어 지원 : CAS는 소프트웨어 로직이 아니라 CPU 수준의 원자적 명령어로 구현된다. 따라서 중간에 다른 쓰레드가 끼어들 틈이 없다.
- 효율성 : 충돌이 적은 환경에서 락을 거는 것보다 훨씬 빠른 성능을 보여준다.
정리
- CAS를 사용하면 락을 사용하지 않지만, 대신에 다른 쓰레드가 값을 먼저 증가해서 문제가 발생하는 경우 루프를 돌며 재시도를 하는 방식을 사용한다.
* 현재 변수의 값을 읽어온다.
* 변수의 값을 1 증가시킨다.
* 원래 값이 같은지 확인한다.(CAS 연산 사용)
* 동일하다면 증가된 값을 변수에 저장하고 종료한다.
* 동일하지 않다면 다른 쓰레드가 값을 변경한 것이므로 다시 처음으로 돌아가 위 과정을 반복한다.
- 두 쓰레드가 동시에 실행되면서 충돌이 발생할 때마다 반복해서 다시 시도하므로, 결과적으로 락 없이 데이터를 안전하게 변경할 수 있다.
- CAS를 사용하는 방식은 충돌이 드물게 발생하는 환경에서는 락을 사용하지 않으므로 높은 성능을 발휘할 수 있다.
- 이는 락을 사용하는 방식과 비교했을 때 쓰레드가 락을 획득하기 위해 대기하지 않기 때문에 대기 시간과 오버헤드가 줄어드는 장점이 있다.
- 그러나 충돌이 빈번히 발생하는 환경에서는 성능에 문제가 될 수 있다. 여러 쓰레드가 자주 동시에 동일한 변수의 값을 변경하려고 시도할 때 CAS는 자주 실패하고 재시도해야 하므로 성능 저하가 발생할 수 있다. 이런 상황에서 반복문을 계속 돌기 때문에 CPU 자원을 많이 소모하게 된다.
CAS와 Lock의 비교
- Lock(비관적 접근 방식)
- 데이터에 접근하기 전에 항상 락을 획득
- 다른 쓰레드의 접근을 막음
- 다른 쓰레드가 방해할 것이라는 가정을 전제
- CAS(낙관적 접근법)
- 락을 사용하지 않고 데이터에 바로 접근
- 충돌이 발생하면 그 때마다 재시도
- 대부분의 경우 충돌이 없을 것이라는 가정을 전제
CAS 락
- 원자적 연산은 쓰레드 입장에서 쪼갤 수 없는 하나의 연산이다. 따라서 여러 쓰레드가 동시에 실행해도 안전하다.
- 동기화 락을 사용할 경우
RUNNABLE상태의 쓰레드가BLOCKED,WAITING상태에서 다시RUNNABLE상태로 이동한다. 이 사이에 CPU 자원을 거의 사용하지 않을 수 있다. - 그래서 동기화 락을 사용하는 방식보다 쓰레드를
RUNNABLE로 살려둔 상태에서 계속 락 획득을 반복 체크하는 것이 더 효율적인 경우 이런 방식을 사용해야 한다. - 이 방식은 쓰레드 상태가 변경되지 않기 때문에 매우 빠르게 락을 획득할 수 있고 또 바로 실행할 수 있다는 장점이 있다.
- 안전한 임계 영역이 필요하지만 연산이 길지 않고 매우 짧게 끝날 때 사용해야 한다.
Spin Lock(스핀 락)
- 쓰레드가 락이 해제되기를 기다리면서 반복문을 통해 계속해서 확인하는 모습이 마치 제자리에서 회전하는 것처럼 보인다고 해서 스핀 락이라고 부른다.
- 그리고 이런 방식에서 쓰레드가 락을 획득할 때까지 대기하는 것을 스핀 대기 또는 CPU 자원을 계속 사용하면서 바쁘게 대기한다고 해서 바쁜 대기라고 한다.
- 이런 스핀 락 방식은 아주 짧은 CPU 연산을 수행할 때 사용해야 효율적이다. 잘못 사용하게 되면 오히려 CPU 자원을 더 많이 사용할 수 있다.