멀티 스레드 - Lock , CAS 비교

희운·2025년 7월 25일

멀티스레드 (java)

목록 보기
5/5

CAS 연산을 사용하면 Lock 을 사용했을 때보다 성능상 좋았다.


  BasicInteger: ms=39
  VolatileInteger: ms=455
  SyncInteger: ms=625
  MyAtomicInteger: ms=367

이런 결과가 왜 이러난 것일까?

예를 들어 스레드 100개를 동시에 실행했을때 한 쓰레드가 Lock 을 소유하게 되면
다른 스레드 99개는 전부 CPU 자원을 사용하지 않는 상태가 된다.(BLOCKING , WAITING)

하지만 AtomicInteger 와 같은 클래스는 내부적으로 CAS( Compare and swap) 을 사용한다.
다음 코드를 보자. 실제 AtomicInteger 클래스는 아니고 같은 원리로 구현해 보았다.

package thread.cas;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class CasMainV3 {

    private static final int THREAD_COUNT = 2;

    public static void main(String[] args) throws InterruptedException {

        AtomicInteger atomicInteger = new AtomicInteger(0);

        System.out.println("start value = " + atomicInteger.get());


        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                incrementAndGet(atomicInteger);
            }
        };

        List<Thread> threads = new ArrayList<>();

        for (int i = 0;i < THREAD_COUNT; i++) {
            Thread thread = new Thread(runnable);
            threads.add(thread);
            thread.start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        int result = atomicInteger.get();
        System.out.println(atomicInteger.getClass().getName() + " result = " + result);


    }


    private static int incrementAndGet(AtomicInteger atomicInteger) {

        int getValue;
        boolean result;

        do {
            getValue = atomicInteger.get();
            log("getValue" + getValue);
            sleep(100);
            //  (CAS 연산의 핵심은 값이 바뀌었어(값이 읽은거랑 다르네)? -> 그럼 난 값을 바꾸지 않을꺼야 )
            //  ( 다르면 다시 시도)
            // 결국 내가 읽은 값과 같으면 값을 바꿔라 ㅎㅎ
            result = atomicInteger.compareAndSet(getValue, getValue + 1);
            log("result: " + result);

        } while (!result);

        return getValue + 1; // atomicInteger.get() 을 하면 다른 쓰레드가 값을 덮어 씌어버릴수도있어서 그러지마.
    }
}


sleep(100) 아래 주석을 달았지만, CAS 연산의 핵심은
result = atomicInteger.compareAndSet(getValue, getValue + 1); 이 부분이다.

읽은 값이랑 같으면 값을 바꾸고, 다르면 do-while 문을 반복한다.(CPU자원소모)

CompareAndSet은 참고로 비교하고 값을 연산하고 저장까지 하지만
CPU 하드웨어적으로 원자적 연산으로 취급한다.
여기서 동시성을 제어하기 위해서 CPU 자원을 소모하면 안좋은거 아닌가? 라는 생각이 들지만, 동시문 문제( 스레드 충돌) 은 간단한 원자적 연산 ( value ++ ) 일때는 CPU 의 연산속도가 매우매우 빠르게 때문에 충돌은 드물게 발생한다.

즉, 충돌이 드물게 발생을 하는데 LOCK 을 걸어서 싱글 스레드만 접근이 가능하게 하면 성능상 CAS 연산보다 안좋을 수 밖에 없다. 또한 LOCK 을 걸면 스레드의 상태가 변하기 때문에 거기서 발생하는 오버헤드 또한 크기 때문에 아무래도 성능상 좋지 않다.

하지만 위에서 말한것 처럼 CAS 연산은 , 스레드의 상태 변화가 없다.
100개의 스레드가 동시에 실행한다고 가정하면, 100개의 스레드가 모두 멀티스레드로 CPU 를 사용해 연산을 한다.

정리해보면
CAS(Compare-And-Swap)와 락(Lock) 방식의 비교

락(Lock) 방식

  • 비관적(pessimistic) 접근법
  • 데이터에 접근하기 전에 항상 락을 획득 다른 스레드의 접근을 막음
  • "다른 스레드가 방해할 것이다"라고 가정

CAS(Compare-And-Swap) 방식

  • 낙관적(optimistic) 접근법
  • 락을 사용하지 않고 데이터에 바로 접근 충돌이 발생하면 그때 재시도
  • "대부분의 경우 충돌이 없을 것이다"라고 가정

이를 학습한 경험으로 스레드간 충돌이 적으면(ex: value++) CAS 방식 즉 낙관적 락을 사용하는것이 좋을것이고, 충돌이 많이 발생하게 된다면, 락 방식을 고민해 볼 필요가 있다.

아래는 Thread 1000개 를 1를 증가하는 연산을 동시에 실행했을때 결과


BasicIntegerresult: 973 //충돌횟수 여기서는 37회정도발생
VolatileIntegerresult: 984 // 충돌 발생
SyncIntegerresult: 1000 //충돌 x
MyAtomicIntegerresult: 1000 // 충돌 x

앞서 스레드를 1000개를 동시에 실행했을때 중간에 sleep(100)을 줬음에도 불구하고 충돌이 1000번중 대략50번 밖에 발생하지 않았다.

정리하자면 , 간단한 cpu 연산에서는 lock 보다는 CAS 연산을 사용하는것이 효과적이다.

profile
기록하는 공간

0개의 댓글