Java/[인프런 김영한 실전 자바 - 고급편]
[인프런 김영한 실전 자바 - 고급편] CAS - 동기화와 원자적 연산
h2boom
2024. 8. 6. 18:03
원자적 연산
- 원자적 연산 : 해당 연산이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미한다.
- 중단되지 않고 다른 연산과 간섭 없이 완전히 실행되거나 전혀 실행되지 않는 성질을 가진다.
- 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산이라는 뜻
int i = 1; //원자적 연산
i = i + 1; //원자적 연산이 아니다.
- i = 1;은 더 이상 쪼갤 수 없는 원자적 연산이다.
- i = i + 1; 은 순서로 나눠 실행되기 때문에 원자적 연산이 아니다.
- 오른쪽 i의 값을 읽는다.
- 읽은 값에 1을 더한다.
- 계산된 값을 왼쪽 i에 대입한다.
- 원자적 연산은 멀티스레드 상황에서 아무런 문제가 발생하지 않지만 원자적 연산이 아닌 경우에는 synchronized 블록이나 Lock을 사용해서 안전한 임계 영역을 만들어야 한다.
AtomicInteger
- AtomicInteger : 멀티스레드 상황에 안전하고 다양한 값 증감 연산을 제공한다.
- 다양한 AtomicXxx 클래스가 존재한다.
- 여러 스레드가 해당 값을 공유해서 특정 값을 증감해야 할 때 사용하는 클래스
- AtomicXxx는 synchronized, lock을 사용하는 방식보다 성능이 좋다.
- CAS 연산 즉 원자적인 연산을 수행하기 때문에 성능이 임계 영역을 지정하고 락을 얻고 반납하고 하는 과정보다 성능이 좋다.
CAS (Compare-And-Swap)
- CAS : 락을 걸지 않고 원자적인 연산을 수행하는 방법이며 락 프리(lock-free) 기법이라고도 한다.
- 락을 완전히 대체하지는 못하고 작은 단위의 일부 영역에 CAS를 적용할 수 있다.
- 기본은 락을 사용하고 특별한 경우에 CAS를 적용할 수 있다.
- AtomicXxx의 compareAndSet()을 통해 CAS 연산을 지원한다.
- 원자적으로 실행되는 메소드이다.
- 매개 변수의 왼쪽에는 기대하는 값, 오른쪽에는 변경하는 값을 대입한다.
- 기존 값이 기대하는 값과 일치한다면 변경하는 값으로 변경한다.
- 실제로 값을 비교하고 대입한다는 것은 원자적 연산이 아니다.
- CAS 연산은 원자적이지 않은 두 개의 연산을 CPU 하드웨어 차원에서 특별하게 하나의 원자적인 연산으로 묶어서 제공하는 기능이다.
- 소프트웨어가 아닌 하드웨어가 제공하는 기능이며 CPU들은 CAS 연산을 위한 명령어를 제공한다.
- 두 연산 과정을 묶어서 하나의 원자적인 명령으로 만들어버림으로 중간에 다른 스레드가 개입할 수 없게 한다.
- CPU가 두 연산 과정을 하나의 원자적인 명령으로 만들기 위해 다른 스레드가 값을 변경하지 못하도록 막는다.
- CPU 입장에서는 아주 찰나의 순간이기 때문에 성능에 큰 영향을 미치지 않는다.
private static int incrementAndGet(AtomicInteger atomicInteger) {
int getValue;
boolean result ;
do {
getValue = atomicInteger.get();
result = atomicInteger.compareAndSet(getValue, getValue + 1);
} while (!result);
return getValue + 1;
}
- 락을 사용하지 않고 CAS 연산을 통해 값을 하나씩 증가하는 메소드를 직접 구현한 것이다.
- get()을 통해 현재 값을 불러온다.
- compareAndSet() 연산을 통해 get()을 통해 가져온 값과 현재 연산을 수행하는 시점의 값이 같은지 (= 어떤 스레드도 값을 변경하지 않았는지) 확인한다.
- 1번, 2번 사이에 다른 스레드로부터 값에 변경이 없으면 연산을 수행하고 중간에 다른 스레드가 값을 변경했다면 루프를 돌면서 다시 1번 과정부터 진행한다.
- CAS를 사용하면 락을 사용하지는 않지만 다른 스레드가 값을 먼저 증가해서 문제가 발생하면 루프를 돌며 재시도를 하는 방식을 사용한다.
- 충돌이 빈번하게 발생하는 환경에서는 성능에 문제가 될 수 있다.
- CAS vs 동기화 락 방식
- 동기화 락 방식
- 비관적 접근법 - 처음부터 여러 스레드가 동시에 접근한다는 최악의 상황을 생각한다.
- 데이터에 접근하기 전에 항상 락을 획득
- 다른 스레드의 접근을 막는다.
- 항상 "다른 스레드가 방해할 것이다" 라는 가정을 한다.
- 장점 : 락을 사용함으로 하나의 스레드만 리소스에 접근할 수 있기에 충돌이 발생하지 않고 경쟁하는 경우에도 안정적으로 동작한다. 복잡한 상황에서도 일관성 있는 동작을 보장하며 대기하는 스레드는 CPU를 거의 사용하지 않는다.
- 단점 : 스레드가 락을 획득하기 위해 대기해야하므로 대기 시간이 길어질 수 있고, 락 획득을 대기하는 시점과 락을 획득하는 시점에 스레드 상태가 변경됨으로 컨텍스트 스위칭이 발생해 오버헤드가 증가할 수 있다.
- CAS 방식
- 낙관적 접근법
- 락을 사용하지 않고 데이터에 바로 접근
- 충돌이 발생하면 그 때 재시도
- "대부분 충돌이 없을 것이다" 라고 가정한다.
- 장점 : 락을 걸지 않고도 값을 안전하게 업데이트함으로 충돌이 적은 환경에서 높은 성능을 발휘하며 락을 획득하기 위해 대기하는 시간이 없고, 스레드가 블로킹되지 않음으로 병렬 처리가 더 효율적일 수 있다.
- 단점 : 충돌이 빈번한 경우 CAS는 루프를 계속 돌며 CPU 자원을 소모하면서 오버헤드가 발생할 수 있으며 스핀락과 유사한 성능 저하가 발생할 수 있다.
- 동기화 락 방식
- 많은 충돌이 발생하지 않는 경우에는 CAS 연산이 빠르다.
- 간단한 CPU 연산은 너무 빨리 처리되기에 충돌이 자주 발생하지 않는다.
- 대기하는 스레드가 CPU 자원을 계속 소모하기에 대기 시간이 아주~~~~~ 짧아야 한다.
- 임계 영역이 필요하지만 연산이 아주~~~~~~ 짧은 경우에 사용해야 한다.
- 스핀 락 : 스레드가 락이 해제되기를 기다리면서 반복문을 통해 계속해서 확인하는 방식
- 스레드가 락을 획득할 때까지 대기하는 것을 스핀 대기라고 한다.
CPU 자원을 계속 사용하면서 바쁘게 대기한다고 해서 바쁜 대기라고도 한다. - 스핀 락은 CAS를 사용해서 구현할 수 있다.
- 스레드가 락을 획득할 때까지 대기하는 것을 스핀 대기라고 한다.
- 실무 관점에서 보면 공유 자원을 사용할 때 충돌할 가능성보다 충돌하지 않을 가능성이 훨씬 높다.
- 실무에서는 동기화 락 방식보다는 CAS 방식이 더 나은 성능을 보인다.
- DB를 기다리거나 다른 서버의 요청을 기다리는 경우에서는 CAS보다 동기화 락을 사용하는 것이 더 효과적이다.
- 직접 CAS 연산을 사용하는 경우는 없고 복잡한 동시성 라이브러리들이 CAS 연산을 사용한다.
출처 : [인프런 김영한 실전 자바 - 고급편]
김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의 | 김영한 - 인프런
김영한 | 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다?
www.inflearn.com