Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[week4][희동] - 4주차 #25

Merged
merged 2 commits into from
Oct 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion heedong/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
| 1주차 | • [프로세스와 스레드](./process-and-thread.md)<br>• [스레드 생성/실행](./thread-detail.md)(Thread, Runnable)<br>• [스레드 제어/생명주기](./thread-lifecycle.md) |
| 2주차 | • [volatile](./volatile.md)<br>• [synchronized](./synchronized.md)<br>• [concurrent.Lock](./concurrentLock.md) |
| 3주차 | • [생산 소비자 문제](bounded-buffer.md) |
| 4주차 | |
| 4주차 | • [CAS(Compare-And-Swap)](cas.md)<br>• [동시성 컬렉션](concurrent-collection.md) |
| 5주차 | |
74 changes: 74 additions & 0 deletions heedong/cas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CAS - 동기화와 원자적 연산

## 원자적 연산
컴퓨터 과학에서 사용하는 **원자적 연산**(atomic operation)의 의미는 해당 연산이 더 이상 나눌 수 없는 단위로 수행된 다는 것을 의미

> 예시와 함께 알아보자

| 구분 | 구현 | 성능 | 원자성 보장 | 추가 설명 |
|:---:|:-------------------------------------------:|:------------------:|:-------:|:-----------------------------------------------------------------:|
| BasicInteger | int 형 타입 value<br>`synchronized` 미사용 | 가장 빠름 | X | 단일 스레드에서만 사용 가능 |
| VolatileInteger | `volatile` int 형 타입 value<br>`synchronized` 미사용 | 느림 | X | 연산을 위한 안전한 임계 영역이 존재하지 않음 |
| SyncInteger | int 형 타입 value<br>`synchronized` 사용 | 가장 느림 | O | 안전한 임계 영역 존재 |
| MyAtomicInteger | `AtomicInteger` 사용 | synchronized 보다 빠름 | O | `synchronized` , `Lock(ReentrantLock)` 을 사용하는 경우보다 1.5 ~ 2배 정도 빠름 |

락 기반(`synchronized`, `Lock`)의 경우 안전한 임계 영역이 존재하지 하지만, 값을 조회하고, 수정할 때 락 취득을 위한 코스트가 발생

락 프리(`Atomic`) 기법은 CAS(Compare-And-Swap, Compare-And-Set) 연산 기반으로 락을 사용하지 않고 원자적 연산을 지원
- CAS 연산은 락을 완전히 대체하는 것은 아니고, CPU 하드웨어 기반으로 **작은 단위의 일부 영역에 적용**

<br>

## Atomic 클래스
### 동작

```mermaid
---
title: CAS 연산
---
flowchart
ai[AtomicInteger] --> atomic{원자적 연산인가?}
atomic -->|Yes| getAndSet["조회(get), 대입(set)"]
getAndSet --> volatile[volatile value]

atomic -->|No| compare["비교 연산(incr, compareAndSet)"]
compare --> compareAndOperate{"비교 연산 실행<bR>(volatile 조회 후 연산)"}
compareAndOperate -->|"return true<br>(기댓값과 실제값이 같은 경우)"| success[저장]
success --> volatile
compareAndOperate -->|"return false<br>(기댓값과 실제값이 다른 경우)"| fail[처음부터 재시도]
fail --> compare
```

> CAS를 통해 스핀락(Spin Lock)을 구현하는 경우 `compareAndSet` 메서드 활용
>
> Spin Lock은 락을 획득할 때까지 계속 반복문을 돌면서 대기하기 때문에 락 취득을 위해 대기하는 시간이 길어지는 경우(e.g. CPU 연산이 오래 걸리는 경우) 성능 저하가 발생할 수 있음 (CPU 자원 계속해서 소모)

스레드가 락을 획득하기 위해 대기하지 않기 때문에 대기 시간과 오버헤드가 줄어들기 때문에 성능 향상

충돌(연산 시 기댓값과 실제값이 다른 경우)이 빈번하게 발생하는 환경에서는 성능에 문제가 될 수 있음
- 여러 스레드가 자주 동시에 동일한 변수의 값을 변경하려고 시도할 때, CAS는 자주 실패하고 재시도해야 하므로 성능 저하가 발생할 수 있음
- 이런 상황에서는 반복문을 계속 돌기 때문에 CPU 자원을 많이 소모하게 됨

간단한 CPU 연산에는 락(Lock)보다는 CAS를 사용하는 것이 효과적

<br>

### 비교

| 구분 | 동기화 락(Lock) 방식 | CAS(Compare-And-Swap) 방식 |
|:---:|:-----------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------|
| 접근 방법 | 비관적(pessimistic) 접근법<br>(다른 스레드가 방해할 것이다) | 낙관적(optimistic) 접근법<br>(대부분의 경우 충돌이 없을 것이다) |
| 설명 | 데이터에 접근하기 전에 항상 락을 획득<br>다른 스레드의 접근을 막음 | 락을 사용하지 않고 데이터에 바로 접근<br>충돌이 발생하면 그 때 재시도 |
| 장점 | 충돌 관리: 하나의 스레드만 리소스에 접근할 수 있으므로 충돌 발생 X<br><br>안정성: 복잡한 상황에서도 일관성 있는 동작 보장<br><br>CPU 절약: 락을 대기하는 스레드는 CPU를 거의 사용하지 않음 | 낙관적 동기화: 락을 걸지 않고도 값을 안전하게 업데이트<br><br>충돌이 적은 경우 성능 향상: 락 프리(Lock-Free) 기반으로 락을 사용하지 않기 때문에 락을 획득하기 위해 대기하는 시간 X |
| 단점 | 락 획득 대기 시간 증가: 스레드가 락을 획득하기 위해 대기해야 하므로 대기 시간이 길어질 수 있음<br><br>컨텍스트 스위칭 오버헤드: 락을 사용하면 락 획득을 대기하는 시점과 또 락을 획득하는 시점에 스레드의 상태가 변경되어 컨텍스트 스위칭이 발생할 수 있음 | 충돌이 빈번한 경우 성능 저하: 여러 스레드가 동시에 동일한 변수에 접근하여 업데이트를 시도할 때 충돌이 발생할 수 있음<br><br>스핀 락과 유사한 오버헤드: 충돌 시 반복적인 재시도를 하므로, 이 과정이 계속 반복되면 스핀 락과 유사한 성능 저하 발생 |

<br>

### 요약
일반적으로 동기화 락을 사용하고, 아주 특별한 경우에 한정해서 CAS를 사용해서 최적화해야 함
- 빨리 끝나거나 단순한 연산(e.g. 카운트)에서는 CAS를 사용하는 것이 효과적
- 오래 걸리거나 복잡한 연산(e.g. DB I/O 작업, Network I/O 작업)에서는 동기화 락 사용

우리가 일반적으로 사용하는 많은 자바 동시성 라이브러리 및 동기화 컬렉션들은 성능 최적화를 위해 CAS 연산을 적극 활용하고 있음
- 따라서 실무에서 직접 CAS 연산을 사용하는 사용하는 일은 매우 드뭄
- 대신에 CAS 연산을 사용해서 최적화 되어 있는 라이브러리들을 이해하고 편리하게 사용할 줄 알면 충분함
119 changes: 119 additions & 0 deletions heedong/concurrent-collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 동시성 컬렉션

`java.util` 패키지에 소속되어 있는 컬렉션 프레임워크는 스레드 세이프(Thread Safe)하지 않음
- Thread Safe: 여러 스레드가 동시에 접근해도 괜찮은 경우

```java
public class SimpleListMainV0 {

public static void main(String[] args) {
addAll(new BasicList());
}

private static void test(SimpleList list) throws InterruptedException {
log(list.getClass().getSimpleName());

// A를 리스트에 저장하는 코드
Runnable addA = new Runnable() {
@Override
public void run() {
list.add("A");
log("Thread-1: list.add(A)");
}
};

// B를 리스트에 저장하는 코드
Runnable addB = new Runnable() {
@Override
public void run() {
list.add("B");
log("Thread-2: list.add(B)");
}
};

Thread thread1 = new Thread(addA, "Thread-1");
Thread thread2 = new Thread(addB, "Thread-2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
log(list); // actual: [B, null]
}
}
```

따라서 컬렉션에서도 여러 스레드에서 동시에 접근한다면 스레드 세이프한 컬렉션을 사용해야 함

<br>

### 방법 1. 프록시 패턴 활용
```java
public static <T> List<T> synchronizedList(List<T> list) {
return new SynchronizedRandomAccessList<>(list);
}
```

컬렉션 내부의 모든 메서드를 `synchronized` 키워드를 추가하여 관리하는 것은 유지보수에 어려움이 있음

따라서 프록시 패턴을 활용해서 컬렉션을 감싸는 방법을 사용 (e.g. `Collections.synchronizedList()`)
- 내부의 모든 메서드에 `synchronized` 키워드가 추가

하지만 단점도 존재함
- 동기화 오버헤드 발생
- `synchronized` 키워드가 멀티스레드 환경에서 안전한 접근을 보장함
- 다만 각 메서드 호출 시마다 동기화 비용이 추가됨, 이로 인해 성능 저하가 발생할 수 있음
- 전체 컬렉션에 대해 동기화가 이루어지기 때문에 잠금 범위가 넓어질 수 있음
- 이는 잠금 경합(lock contention)을 증가시키고, 병렬 처리의 효율성을 저하시키는 요인 유발
- 모든 메서드에 대해 동기화를 적용하다 보면 특정 스레드가 컬렉션을 사용하고 있을 때 다른 스레드들이 대기해야 하는 상황이 빈번해질 수 있음
- 정교한 동기화가 불가능함
- `synchronized` 프록시를 사용하면 컬렉션 전체에 대한 동기화가 이루어지지만 특정 부분이나 메서드에 대해 선택적으로 동기화를 적용하는 것은 어려움
- 이는 과도한 동기화로 이어질 수 있음

따라서 이 방식은 동기화에 대한 최적화가 이루어지지 않는 구현
- 자바는 이런 단점을 보완하기 위해 `java.util.concurrent` 패키지에 동시성 컬렉션(concurrent collection)을 제공

<br>

### 방법 2. 동시성 컬렉션 사용
자바 1.5부터 `java.util.concurrent` 패키지에는 고성능 멀티스레드 환경을 지원하는 다양한 동시성 컬렉션 클래스들을 제공

#### 컬렉션 인터페이스

| 컬렉션 인터페이스 | 동시성 컬렉션 클래스 | 설명 |
|:------------------:|:--------------:|:-----------------------:|
| `List` | `CopyOnWriteArrayList` | `ArrayList`의 대안 |
| `Set` | `CopyOnWriteArraySet` | `HashSet`의 대안 |
| `Set` | `ConcurrentSkipListSet` | `TreeSet`의 대안 |
| `Map` | `ConcurrentHashMap` | `HashMap`의 대안 |
| `Map` | `ConcurrentSkipListMap` | `TreeMap`의 대안 |

`LinkedHashSet` , `LinkedHashMap` 처럼 입력 순서를 유지하면서 멀티스레드 환경에서 사용할 수 있는 `Set` , `Map` 구현체는 제공하지 않음
- 필요하다면 `Collections.synchronizedXxx()` 를 사용해야 함
- 설계 철학, 성능 이슈(내부 추가 동기화) 및 코드 복잡성 등을 고려하여 제공하지 않음

<br>

#### BlockingQueue 인터페이스

| 동시성 컬렉션 클래스 | 설명 |
|:------------------:|:------------------------------------------------------------------------|
| `ArrayBlockingQueue` | - 크기가 고정된 블로킹 큐<br>- 공정(fair) 모드를 사용할 수 있음, 다만 공정(fair) 모드를 사용 시 성능이 저하 |
| `LinkedBlockingQueue` | - 크기가 무한하거나 고정된 블로킹 큐<br>- `ArrayBlockingQueue` 보다 더 효율적인 메모리 사용 |
| `PriorityBlockingQueue` | - 우선순위 큐<br>- 우선순위가 높은 요소를 먼저 처리하는 블로킹 큐 |
| `SynchronousQueue` | - 데이터를 저장하지 않는 블로킹 큐<br>- 생산자가 데이터를 추가하면 소비자가 그 데이터를 받을 때까지 대기 |
| `DelayQueue` | - 지연된 요소를 처리하는 블로킹 큐<br>- 각 요소는 지정된 지연 시간이 지난 후에야 소비될 수 있음 |

<br>

## 동시성 컬렉션이 모든 상황에서 효율적일까?
> No. 동시성 컬렉션이 모든 상황에서 최적화된 silver bullet은 아님

Thread Safe한 ArrayList 를 기준으로 비교해보자
- SynchronizedList VS CopyOnWriteArrayList

<br>

#### 결론
자료의 크기나 작업의 종류에 따라 가장 효율적인 컬렉션은 다룰 수 있음
- `SynchronizedList`: 쓰기 작업이 읽기 작업보다 많은 경우
- `CopyOnWriteList`: 읽기 작업이 쓰기 작업보다 많은 경우