|
| 1 | +# 섹션 11. CAS - 동기화와 원자적 연산 |
| 2 | + |
| 3 | +## 원자적 연산 |
| 4 | + |
| 5 | +### 원자적 연산 |
| 6 | + |
| 7 | +해당 연산이 더 이상 나눌 수 없는 단위로 수행된다는 것을 의미 → 실행 or 실행 되지 않음 |
| 8 | + |
| 9 | +> 즉, 멀티스레드 상황에서 다른 스레드의 간섭 없이 안전하게 처리되는 연산 |
| 10 | +> |
| 11 | +
|
| 12 | +원자적 연산이 아니지만 멀티스레드 상황에서 문제가 발생할 것 같은 경우, 앞서 배운 `synchronized` 블럭 or `Lock` 등을 사용해 임계 영역 설정 |
| 13 | + |
| 14 | +### Volatile |
| 15 | + |
| 16 | +volatile는 여러 CPU 사이에 발생하는 캐시 메모리와 메인 메모리의 동기화 문제를 해결한다. |
| 17 | + |
| 18 | +- 원자적 연산이 아니여서 발생하는 문제를 해결해주지 않음 |
| 19 | + |
| 20 | +### Synchronized |
| 21 | + |
| 22 | +synchronized를 사용하면 안전하게 임계영역을 설정하여 블록 안의 연산을 원자적 연산으로 처리한다. |
| 23 | + |
| 24 | +### AtomicInteger |
| 25 | + |
| 26 | +`Synchronized` 블럭 안에서 증가, 감소 연산을 하듯이 AtomicInteger를 사용하면 원자적인 Integer를 사용할 수 있다. |
| 27 | + |
| 28 | +> AtomicInteger, AtomicLong, AtomicBoolean과 같이 다양한 AtomicXxx 클래스 존재 |
| 29 | +> |
| 30 | +
|
| 31 | +### 성능 비교 |
| 32 | + |
| 33 | +**일반적인 Integer를 사용한 증가연산** |
| 34 | + |
| 35 | +- 가장 빠르다 |
| 36 | +- CPU 캐시 적극 활용 |
| 37 | +- 안전한 임계영역이 없고, volatile도 사용하지 않으므로 멀티스레드에서는 사용 불가 |
| 38 | + |
| 39 | +**Volatile를 사용한 증가연산** |
| 40 | + |
| 41 | +- CPU 캐시를 사용하지 않고 메인 메모리 사용 |
| 42 | +- 임계 영역 사용 X → 멀티 스레드 환경에서 사용 불가 |
| 43 | +- 단일 스레드에선 일반 Integer보다 느림 |
| 44 | + |
| 45 | +**synchronized를 사용한 증가연산** |
| 46 | + |
| 47 | +- `Synchronized` 를 사용하므로 멀티스레드 환경에서도 안전 |
| 48 | +- 조금 느리다 |
| 49 | + |
| 50 | +**AtomicInteger** |
| 51 | + |
| 52 | +- 멀티스레드 상황에서 안전하게 사용 가능 |
| 53 | +- synchronized, Lock 사용 보다 성능이 빠름 |
| 54 | + |
| 55 | +> 왜? |
| 56 | +> |
| 57 | +
|
| 58 | +AtomicInteger는 synchronized와 Lock을 사용하지 않고 원자적 연산을 수행 |
| 59 | + |
| 60 | +## CAS 연산 |
| 61 | + |
| 62 | +### Lock 기반 방식의 문제점 |
| 63 | + |
| 64 | +락은 특정 자원을 보호하기 위해 스레드가 해당 자원에 대한 접근 제한 |
| 65 | + |
| 66 | +- 락 획득과 반납의 과정 반복 → 오버헤드 |
| 67 | + |
| 68 | +### CAS(Compare-And-Swap, Compare-And-Set) |
| 69 | + |
| 70 | +- 락을 사용하지 않기 때문에 lock-free |
| 71 | +- 락을 완전히 대체 X, 작은 단위의 일부 영역에 적용 |
| 72 | + |
| 73 | +<aside> |
| 74 | +💡 |
| 75 | + |
| 76 | +**compareAndSet(0, 1)** |
| 77 | + |
| 78 | +antomicInteger가 가지고 있는 값이 현재 0 이면, 이 값을 1로 변경하는 메서드 |
| 79 | + |
| 80 | +- 현재 값이 0 이면 1로 변경하고, true 반환 (성공 시) |
| 81 | +- 현재 값이 0 이 아니면 변경 X, false 반환 (실패 시) |
| 82 | + |
| 83 | +→ **이 메서드는 원자적으로 실행됨 (CPU 하드웨어 차원에서 제공하는 기능)** |
| 84 | + |
| 85 | +</aside> |
| 86 | + |
| 87 | +위 메서드는 두가지 과정으로 나눌 수 있는데 |
| 88 | + |
| 89 | +- x001의 값 확인 |
| 90 | +- 읽은 값이 0 이면 1로 변경 |
| 91 | + |
| 92 | +이 두 과정을 CPU가 원자적인 명령으로 만들기 위해 다른 스레드가 x001의 값을 변경하지 못하게 막음 |
| 93 | + |
| 94 | +## CAS 연산2 |
| 95 | + |
| 96 | +```java |
| 97 | +private static int incrementAndGet(AtomicInteger atomicInteger) { |
| 98 | + int getValue; |
| 99 | + boolean result; |
| 100 | + do { |
| 101 | + getValue = atomicInteger.get(); |
| 102 | + log("getValue: " + getValue); |
| 103 | + result = atomicInteger.compareAndSet(getValue, getValue + 1); |
| 104 | + log("result: " + result); |
| 105 | + } while (!result); |
| 106 | + return getValue + 1; |
| 107 | + } |
| 108 | +``` |
| 109 | + |
| 110 | +- 위 코드처럼 value값을 읽고, 읽은 value값이 메모리 value값과 같은지 확인을 한 후 값을 증가 |
| 111 | +- 증가하는 로직인 CAS 연산이므로 멀티스레드 환경에서도 안전 |
| 112 | + - CAS 성공 시 true 반환 후 do-while 탈출 |
| 113 | + - CAS 실패 시 false 반환 후 do-while 재시작 |
| 114 | + |
| 115 | +## CAS 연산3 |
| 116 | + |
| 117 | +2개의 스레드로 CAS 연산2의 코드를 실행하면 |
| 118 | + |
| 119 | +```java |
| 120 | + start value = 0 |
| 121 | + 18:13:37.623 [ Thread-1] getValue: 0 |
| 122 | + 18:13:37.623 [ Thread-0] getValue: 0 |
| 123 | + 18:13:37.625 [ Thread-1] result: true |
| 124 | + 18:13:37.625 [ Thread-0] result: false |
| 125 | + 18:13:37.731 [ Thread-0] getValue: 1 |
| 126 | + 18:13:37.731 [ Thread-0] result: true |
| 127 | + AtomicInteger resultValue: 2 |
| 128 | +``` |
| 129 | + |
| 130 | +- Thread-0의 첫번 째 시도는 실패를 하여 getValue의 값이 증가하지 않은 것을 볼 수 있다. |
| 131 | + - Thread-1이 먼저 값을 올려 value의 값이 변경되었기 때문 |
| 132 | + - false 반환 후 재시도 |
| 133 | +- Thread-0의 두번 째 시도는 성공하여 getValue의 값이 증가 |
| 134 | + |
| 135 | +### CAS 문제점 |
| 136 | + |
| 137 | +충돌이 빈번하게 발생하는 환경에서는 성능에 문제가 발생 |
| 138 | + |
| 139 | +- 여러 스레드가 자주 동시에 동일한 변수의 값을 변경하려 하는 경우 |
| 140 | + |
| 141 | +### CAS와 Lock 방식 비교 |
| 142 | + |
| 143 | +| Lock | CAS | |
| 144 | +| --- | --- | |
| 145 | +| 비관적 접근법 | 낙관적 접근법 | |
| 146 | +| 데이터에 접근하기 전 항상 락 획득 | 락 사용X 바로 데이터 접근 | |
| 147 | +| 다른 스레드의 접근 제한 | 충돌 발생 시 재시도 | |
| 148 | +| 다른 스레드가 방해할 것이다 가정 | 대부분은 충돌이 없을것이다 가정 | |
| 149 | + |
| 150 | +> 언제 CAS를 사용하는게 좋을 까? |
| 151 | +> |
| 152 | +
|
| 153 | +간단한 CPU 연산같이 빠르게 처리되면 충돌이 자주 발생하지 않으므로 빠른 처리가 가능한 연산 |
| 154 | + |
| 155 | +## CAS 락 구현 |
| 156 | + |
| 157 | +```java |
| 158 | +public class SpinLock { |
| 159 | + private final AtomicBoolean lock = new AtomicBoolean(false); |
| 160 | + public void lock() { |
| 161 | + log("락 획득 시도"); |
| 162 | + while (!lock.compareAndSet(false, true)) { |
| 163 | + // 락을 획득할 때 까지 스핀 대기(바쁜 대기) 한다. |
| 164 | + log("락 획득 실패 - 스핀 대기"); |
| 165 | + } |
| 166 | + log("락 획득 완료"); |
| 167 | + } |
| 168 | + public void unlock() { |
| 169 | + lock.set(false); |
| 170 | + log("락 반납 완료"); |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +- CAS를 사용하지 않은 스핀락과 비교했을 때, 다음 두가지 연산이 원자적으로 묶여 임계영역이 뚫리는 일이 발생하지 않는다 |
| 176 | + - 락 사용 여부 확인 |
| 177 | + - 락의 값 변경 |
| 178 | + |
| 179 | +이런 방식의 락은 CPU가 `BLOCKED` 나 `WAITING` 으로 전이되지 않기 때문에 CPU 자원을 계속해서 사용하지만 그만큼 빠르게 락을 획득하고 실행할 수 있다. |
| 180 | + |
| 181 | +- 임계 영역을 필요로 하지만 연산이 매우 짧을 경우 사용 |
| 182 | + |
| 183 | +## 정리 |
| 184 | + |
| 185 | +**CAS** |
| 186 | + |
| 187 | +장점 |
| 188 | + |
| 189 | +- 낙관적 동기화: 락을 걸지 않고 안전하게 업데이트 |
| 190 | + - 충돌이 자주 발생하지 않을 것을 가정, 충돌이 적은 환경에서 높은 성능 |
| 191 | +- 락 프리(Lock-Free): 락 사용 X → 락 획득 대기시간이 적음, 스레드 블로킹 X → 병렬 처리 효율적 |
| 192 | + |
| 193 | +단점 |
| 194 | + |
| 195 | +- 충돌이 빈번한 경우: 계속해서 재시도 해야하며, CPU 자원 지속적 소모 → 오버헤드 발생 |
| 196 | +- 스핀락과 유사한 오버헤드: 많은 충돌 시 많은 재시도 → 성능 저하 |
| 197 | + |
| 198 | +**동기화 락** |
| 199 | + |
| 200 | +장점 |
| 201 | + |
| 202 | +- 충돌 관리: 락 사용을 통해 하나의 스레드만 리소스 접근 → 충돌 X |
| 203 | +- 안정성: 복잡한 상황에서도 락을 통해 일관성 있게 동작 |
| 204 | +- 스레드 대기: 락을 대기하는 스레드는 CPU 사용 X |
| 205 | + |
| 206 | +단점 |
| 207 | + |
| 208 | +- 락 획득 대기시간: 스레드가 락 획득을 위해 대기 → 소모 시간 발생 |
| 209 | +- 컨텍스트 스위칭 오버헤드: 락 획득 대기와 획득 시점에서 스레드의 상태가 변경 → 컨텍스트 스위칭 발생 |
| 210 | + - 컨텍스트 스위칭으로 인한 오버헤드가 발생 |
0 commit comments