Skip to content

Commit 459ce82

Browse files
authored
[week4][승용] - 4주차 (#29)
1 parent 6e07c56 commit 459ce82

File tree

3 files changed

+329
-0
lines changed

3 files changed

+329
-0
lines changed

seungyong/11-CAS.md

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
- 컨텍스트 스위칭으로 인한 오버헤드가 발생

seungyong/12-concurrent-collection.md

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# 섹션12. 동시성 컬렉션
2+
3+
## 동시성 컬렉션이 필요한 이유
4+
5+
스레드 세이프(Thread Safe): 여러 스레드가 동시에 접근해도 괜찮은 경우
6+
7+
java.util 패키지에 있는 컬렉션 프레임 워크는 스레드 세이프 할까?
8+
9+
### java.util 패키지는 스레드 세이프 하지 않다.
10+
11+
```java
12+
public void add(Object e) {
13+
elementData[size] = e;
14+
sleep(100); // 멀티스레드 문제를 쉽게 확인하는 코드
15+
size++;
16+
}
17+
```
18+
19+
이 메서드는 원자적이지 않다.
20+
21+
- 내부 배열에 데이터 추가
22+
- size도 함께 하나 증가
23+
- size 값 불러오기
24+
- +1 하기
25+
26+
따라서 원자적이지 않은 연산을 멀티스레드 환경에서 사용하려면 `synchronized` 혹은 `Lock` 을 통해 동기화 해야함
27+
28+
## 프록시 도입
29+
30+
### 프록시(Proxy): 대리자, 대신 처리해주는 자
31+
32+
![image.png](resources/proxy.png)
33+
34+
- 프록시인 SyncProxyList는 원본인 BasicList와 똑같은 SimpleList 구현
35+
- 클라이언트는 원본 구현체든 proxy 구현체는 상관 X
36+
- 클라이언트 입장에서는 프록시든 원본이든 똑같은 SimpleList 구현체
37+
- 프록시는 내부에 원본을 가지고 있음
38+
- 필요한 일을 처리 한 후, 원본을 호출하는 구조로 구현 가능
39+
- 여기서는 Synchronized 블럭을 감싸는 일을 함
40+
- 프록시가 동기화 적용 됐으므로, 원본 코드도 동기화 적용됨
41+
42+
### 프록시 패턴
43+
44+
객체지향 디자인 패턴 중 하나
45+
46+
> 어떤 객체에 대한 접근을 제어하기 위해 그 객체의 대리인 또는 인터페이스 역할을 하는 객체 제공
47+
>
48+
- 접근 제어: 실제 객체에 대한 접근 제어 및 통제
49+
- 성능 향상: 실제 객체의 생성을 지연 or 캐싱하여 성능 최적화
50+
- 부가 기능 제공: 실제 객체에 추가적인 기능(로깅, 인증, 동기화 등)을 투명하게 제공
51+
52+
Ex) Spring AOP
53+
54+
## 자바 동시성 컬렉션 - synchronized
55+
56+
모든 자료구조에 `synchronized` 를 사용해 동기화 해두면 어떨까 싶지만
57+
58+
- 모든 동기화 방식은 성능과 트레이드 오프가 있다
59+
60+
결국 개발자가 정확히 필요성을 판단하고 필요한 경우에만 적용해야 한다.
61+
62+
### Collections.synchronizedList(new ArrayList<>());
63+
64+
```java
65+
public static <T> List<T> synchronizedList(List<T> list) {
66+
return new SynchronizedRandomAccessList<>(list);
67+
}
68+
```
69+
70+
SynchronizedRandomAccessList는 synchronized를 추가하는 프록시 역할 수행
71+
72+
- 클라이언트 → ArrayList
73+
- 클라이언트 → SynchronizedRandomAccessList(프록시) → ArrayList
74+
75+
### 단점
76+
77+
- 동기화 오버헤드 발생
78+
- 락을 사용하기 때문에 성능 저하가 발생할 수 밖에 없음
79+
- 전체 컬렉션에 대해 동기화
80+
- 잠금 경합이 증가하여 병렬 처리의 효율성 저하
81+
- 특정 스레드가 컬렉션을 사용하면 다른 스레드는 대기해야함
82+
- 정교한 동기화 불가능
83+
- 컬렉션 전체에 synchronized를 걸기 때문에 동기화에 대한 최적화 불가능
84+
85+
## 자바 동시성 컬렉션 - 동시성 컬렉션
86+
87+
동시성 컬렉션: thread-safe한 컬렉션, 매우 정교한 매커니즘으로 효율적으로 처리
88+
89+
### 동시성 컬렉션의 종류
90+
91+
- List
92+
- CopyOnWriteArrayList → ArrayList의 대안
93+
- Set
94+
- CopyOnWriteArraySet → HashSet의 대안
95+
- ConcurrentSkipListSet → TreeSet의 대안(정렬된 순서 유지, Comparator 사용 가능)
96+
- Map
97+
- ConcurrentHashMap → HashMap의 대안
98+
- ConcurrentSkipListMap → TreeMap의 대안(정렬된 순서 유지, Comparator 사용 가능)
99+
- Queue
100+
- ConcurrentLinkedQueue → 동시성 큐, 비 차단(non-blocking)큐
101+
- Deque
102+
- ConcurrentLinkedDeque → 동시성 데크, 비 차단(non-blocking)큐
103+
104+
### 스레드를 차단하는 블로킹 큐
105+
106+
- BlockingQueue
107+
- ArrayBlockingQueue
108+
- 크기가 고정된 블로킹 큐
109+
- 공정(fair)모드 사용 가능, 사용 시 성능 저하
110+
- LinkedBlockingQueue
111+
- 크기가 무한하거나 고정된 블로킹 큐
112+
- PriorityBlockingQueue
113+
- 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐
114+
- SynchronousQueue
115+
- 데이터를 저장하지 않는 블로킹 큐, 생산자가 데이터 추가 시 소비할 때 까지 대기
116+
- 생산자 - 소비자 직접거래
117+
- DelayQueue
118+
- 지연된 요소를 처리하는 블로킹 큐
119+
- 각 요소는 지정된 시간이 지난 후 소비 가능

seungyong/resources/proxy.png

56.9 KB
Loading

0 commit comments

Comments
 (0)