Java/[인프런 김영한 실전 자바 - 고급편]

[인프런 김영한 실전 자바 - 고급편] CAS - 동기화와 원자적 연산

h2boom 2024. 8. 6. 18:03

원자적 연산

  • 원자적 연산 : 해당 연산이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미한다.
    • 중단되지 않고 다른 연산과 간섭 없이 완전히 실행되거나 전혀 실행되지 않는 성질을 가진다.
    • 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산이라는 뜻

 

int i = 1; //원자적 연산

i = i + 1; //원자적 연산이 아니다.
  • i = 1;은 더 이상 쪼갤 수 없는 원자적 연산이다.
  • i = i + 1; 은 순서로 나눠 실행되기 때문에 원자적 연산이 아니다.
    1. 오른쪽 i의 값을 읽는다.
    2. 읽은 값에 1을 더한다.
    3. 계산된 값을 왼쪽 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 연산을 통해 값을 하나씩 증가하는 메소드를 직접 구현한 것이다.
    1. get()을 통해 현재 값을 불러온다.
    2. compareAndSet() 연산을 통해 get()을 통해 가져온 값과 현재 연산을 수행하는 시점의 값이 같은지 (= 어떤 스레드도 값을 변경하지 않았는지) 확인한다. 
    3. 1번, 2번 사이에 다른 스레드로부터 값에 변경이 없으면 연산을 수행하고 중간에 다른 스레드가 값을 변경했다면 루프를 돌면서 다시 1번 과정부터 진행한다. 

 

  • CAS를 사용하면 락을 사용하지는 않지만 다른 스레드가 값을 먼저 증가해서 문제가 발생하면 루프를 돌며 재시도를 하는 방식을 사용한다.
    • 충돌이 빈번하게 발생하는 환경에서는 성능에 문제가 될 수 있다.

 

  • CAS vs 동기화 락 방식
    • 동기화 락 방식
      • 비관적 접근법 - 처음부터 여러 스레드가 동시에 접근한다는 최악의 상황을 생각한다.
      • 데이터에 접근하기 전에 항상 락을 획득
      • 다른 스레드의 접근을 막는다.
      • 항상 "다른 스레드가 방해할 것이다" 라는 가정을 한다.
      • 장점 : 락을 사용함으로 하나의 스레드만 리소스에 접근할 수 있기에 충돌이 발생하지 않고 경쟁하는 경우에도 안정적으로 동작한다. 복잡한 상황에서도 일관성 있는 동작을 보장하며 대기하는 스레드는 CPU를 거의 사용하지 않는다.
      • 단점 : 스레드가 락을 획득하기 위해 대기해야하므로 대기 시간이 길어질 수 있고, 락 획득을 대기하는 시점과 락을 획득하는 시점에 스레드 상태가 변경됨으로 컨텍스트 스위칭이 발생해 오버헤드가 증가할 수 있다.
    • CAS 방식
      • 낙관적 접근법
      • 락을 사용하지 않고 데이터에 바로 접근
      • 충돌이 발생하면 그 때 재시도
      • "대부분 충돌이 없을 것이다" 라고 가정한다.
      • 장점 : 락을 걸지 않고도 값을 안전하게 업데이트함으로 충돌이 적은 환경에서 높은 성능을 발휘하며 락을 획득하기 위해 대기하는 시간이 없고, 스레드가 블로킹되지 않음으로 병렬 처리가 더 효율적일 수 있다.
      • 단점 : 충돌이 빈번한 경우 CAS는 루프를 계속 돌며 CPU 자원을 소모하면서 오버헤드가 발생할 수 있으며 스핀락과 유사한 성능 저하가 발생할 수 있다.
  • 많은 충돌이 발생하지 않는 경우에는 CAS 연산이 빠르다.
    • 간단한 CPU 연산은 너무 빨리 처리되기에 충돌이 자주 발생하지 않는다.
    • 대기하는 스레드가 CPU 자원을 계속 소모하기에 대기 시간이 아주~~~~~ 짧아야 한다.
    • 임계 영역이 필요하지만 연산이 아주~~~~~~ 짧은 경우에 사용해야 한다.

 

  • 스핀 락 : 스레드가 락이 해제되기를 기다리면서 반복문을 통해 계속해서 확인하는 방식
    • 스레드가 락을 획득할 때까지 대기하는 것을 스핀 대기라고 한다.
      CPU 자원을 계속 사용하면서 바쁘게 대기한다고 해서 바쁜 대기라고도 한다.
    • 스핀 락은 CAS를 사용해서 구현할 수 있다.

 

  • 실무 관점에서 보면 공유 자원을 사용할 때 충돌할 가능성보다 충돌하지 않을 가능성이 훨씬 높다.
    • 실무에서는 동기화 락 방식보다는 CAS 방식이 더 나은 성능을 보인다.
    • DB를 기다리거나 다른 서버의 요청을 기다리는 경우에서는 CAS보다 동기화 락을 사용하는 것이 더 효과적이다.
    • 직접 CAS 연산을 사용하는 경우는 없고 복잡한 동시성 라이브러리들이 CAS 연산을 사용한다.

출처 : [인프런 김영한 실전 자바 - 고급편]

https://www.inflearn.com/course/%EA%B9%80%EC%98%81%ED%95%9C%EC%9D%98-%EC%8B%A4%EC%A0%84-%EC%9E%90%EB%B0%94-%EA%B3%A0%EA%B8%89-1/dashboard

 

김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성 강의 | 김영한 - 인프런

김영한 | 멀티스레드와 동시성을 기초부터 실무 레벨까지 깊이있게 학습합니다., 국내 개발 분야 누적 수강생 1위, 제대로 만든 김영한의 실전 자바[사진][임베딩 영상]단순히 자바 문법을 안다?

www.inflearn.com