Skip to content

Commit

Permalink
docs: add ch12 (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
lsj8367 authored Mar 26, 2024
1 parent b9a46d0 commit 1639f0f
Showing 1 changed file with 228 additions and 0 deletions.
228 changes: 228 additions & 0 deletions ch12/lsj8367.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# 12장 동시 성능 기법

현재 우리는 멀티코어 프로세서가 일반적인 시대에 살고있다.

잘 만든 애플리케이션은 멀티코어에 애플리케이션 부하를 고루 분산시켜 처리할 수 있음.

여기서 JVM 같은 실행 플랫폼의 장점이 뚜렷하다.

## 병렬성

> 암달의 법칙
>
어떤 연산 태스크가 병렬 실행이 가능한 파트와 반드시 순차 실행해야하는 파트로 구성

순차 실행파트 S

총 태스크 소요시간 T

자유로운 프로세스 개수 N

T는 프로세서 개수의 함수 T(N) 으로 표시할 수 있다.

전체 태스크 소요 시간 : `T(N) = S + (1/N) * (T - S)`

병렬 태스크나 다른 순차 태스크 간의 소통이 필요 없다면 무한하게 속도를 높여볼 수 있다.

이런 워크로드를 **낯간지러운 병렬** 이라고 한다.

처리율 향상은 동시성을 부여하는 전체 목표와 상충된다.

코드베이스를 병렬화하는 작업을 진행할 때 복잡도가 늘어난 대가로 얻은 혜택을 충분히 입증가능하게 성능 테스트를 수반해야 한다.

### JMM(Java Memory Model)의 이해

자바 1.0부터 이런 모델이 존재했고 문제점 개선후 자바5부터 배포됐다.

자바 플랫폼은 공유 상태를 어디서 액세스하던지 JMM이 약속한 내용을 반드시 이행한다.

- 강한 메모리 모델
- 전체코어가 항상 같은 값을 바라본다.
- 약한 메모리 모델
- 코어마다 다른 값을 바라볼 수 있고 그 시점을 제어하는 특별한 캐시규칙 존재

### JMM의 기본 개념

- 한 이벤트는 무조건 다른 이벤트보다 먼저 발생한다 (Happens-Before)
- 이벤트가 객체 뷰를 메인 메모리와 동기화시킨다(Synchronized-With)
- 실행 스레드 밖에서는 명령어가 순차 실행되는 것처럼 보인다(As-If-Serial)
- 한 스레드에 걸린 락을 다른 스레드가 그 락을 획득하기 전에 해제한다(Release-Before-Acquire)

동기화를 통한 락킹은 가변 상태를 공유하는 가장 중요한 기법이고, 동시성을 다루는 자바의 근본적인 관점을 대변한다.

자바에서 스레드는 객체 상태 정보를 스스로 들고 다니며, 스레드가 변경한 내용은

메인 메모리로 곧장 반영되어 같은 데이터를 액세스하는 다른 스레드가 다시 읽는 구조다.

> synchronized 키워드
>
모니터를 장악한 스레드의 로컬뷰가 메인 메모리와 동기화 되었다 라는 뜻과 같다.

멀티스레드 순서와 가시성을 보장하는 유일한 장치였다.

> synchronized의 한계점
>
- 락이 걸린 객체에서 일어나는 동기화 작업은 모두 균등하게 취급
- 락 획득/해제는 반드시 메소드 수준이나 메소드 내부의 동기화 블록 안에서 이루어져야 한다.
- 락을 얻지 못한 스레드는 블로킹된다. 락을 얻지 못할 경우, 락을 얻어 처리를 계속하려고 시도하는 것조차 불가능

쓰기 작업에만 키워드를 사용하게 되면 소실된 업데이트 현상이 나타난다.

## 동시성 라이브러리 구축

`java.util.concurrent` 패키지는 멀티 스레드르 애플리케이션을 자바로 더 쉽게 개발할 수 있게 설계된 라이브러리

- 락, 세마포어
- 아토믹스
- 블로킹 큐
- 래치
- 실행자

비교해서 바꾸기(CAS, Compare And Swap)는 예상되는 현재값과 원하는 새값, 그리고 메모리 위치를 전달받아 2가지 일을 수행한다.

1. 예상되는 현재 값을 메모리 위치에 있는 콘텐츠와 비교
2. 두 값이 일치하면 현재 값을 원하는 새 값으로 교체

CAS는 여러가지 중요한 고수준의 동시성 기능을 구성하는 기본 요소

CAS는 `sun.misc.Unsafe` 클래스를 통해 액세스한다.

### Unsafe

개발자가 직접 사용할일은 없다.

> 가능한 일
>
- 객체는 할당하지만 생성자 실행X
- 원(raw) 메모리에 액세스하고 포인터 수준의 연산을 수행
- 프로세서별 하드웨어 특성을 이용

이 덕분에 고수준 프레임워크들을 구현할 수 있다.

- 신속한 (역)직렬화
- Thread-Safe한 네이티브 메모리 액세스
- Atomic 메모리 연산
- 효율적인 객체/메모리 레이아웃
- 커스텀 메모리 펜스
- 네이티브 코드와의 신속한 상호작용
- JNI(Java Native Interface) 에 관한 다중 운영체제 대체물
- 배열 원소에 volatile하게 액세스

### Atomics와 CAS

Atomic 연산은 값을 더하고 증감하는 복합 연산을 수행하는데 `get()` 을 사용하여 계산 결과값을 돌려받는다.

Atomic은 lock-free 이기에 데드락은 있을 수 없다.

업데이트 작업이 실패하는 경우 내부에서 재시도 루프를 실행하게 된다.

### java.util.concurrent 락

- `lock()`
- 기존 방식대로 락 획득 후 락 사용때까지 블로킹
- `newCondition()`
- 락 주위에 조건을 설정하여 좀 더 유연하게 락 활용
- 락 내부에서 관심사 분리
- `tryLock()`
- 락을 획득하려고 시도한다. (Timeout 옵션 설정가능)
- 스레드가 락을 사용할 수 없는 경우에도 계속 처리 진행 가능
- `unlock()`
- 락을 해제함
- `lock()`에 대응되는 후속 호출

> LockSupport
>
LockSupport 클래스는 스레드에게 허가증(퍼밋)을 발급

발급할 퍼밋이 없다면 스레드는 기다린다.

개념 자체는 세마포어와 비슷하지만, LockSupport는 오직 한가지만 발급한다.

스레드는 퍼밋을 받지 못한경우 잠시 파킹되었다가 유효한 퍼밋을 받을 수 있을 때 다시 언파킹 된다.

> ReentrantReadWriteLock
>
기존 synchronized나 `ReentrantLock` 을 이용하면 한가지 락정책만 이용 가능했는데,

ReentrantReadWriteLock 클래스의 ReadLock, WriteLock 을 활용하면 여러 스레드가 읽기 작업 수행중에도 블로킹하지 않게 할 수 있다.

> 세마포어
>
풀 스레드나 DB 접속 객체 등 여러 리소스의 액세스를 허용하는 독특한 기술을 제공한다.

퍼밋이 하나뿐인 세마포어는 뮤텍스와 동등하다.

→ 뮤텍스는 뮤텍스 걸린 스레드만 해제 가능, 세마포어는 비소유 스레드도 해제할 수 있음.

> 래치 배리어
>
스레드 세트의 실행을 제어하는 유용한 기법

`CountDownLatch` 클래스를 사용하여 구현함.

스레드 실행때마다 countDown을 수행해주어 제어한다.

## 실행자와 태스크 추상화

스레드 문제를 직접 처리하는것보다 `java.util.concurrent` 패키지에서 적절한 수준으로 조치를 취해주자.

태스크를 추상화 하는 방법은 값을 반환하는 태스크를 `Callable` 인터페이스로 나타내면 된다.

Runnable 은 결과를 반환하거나 예외를 던지지 않는다. (void 메소드를 갖고있음)

태스크들은 ExecutorService 에서 관리되는 스레드풀에 의해 실행될 수 있다.

이를 만들기 위해서는 `Executors` 헬퍼 클래스로 정적팩토리 메소드를 통해 만들어줄 수 있다.

### ExecutorService 선택하기

올바른 ExecutorService를 선택하면 비동기 프로세스를 적절히 잘 제어할 수 있고, 풀 스레드 개수를 정확하게 정하면 성능이 뚜렷하게 향상될 수 있다.

### 포크/조인

ForkJoinPool이라는 구현체에 기반한다.

- 하위 분할 태스크를 효율적으로 처리할 수 있다.
- 작업 빼앗기 알고리즘을 구현한다.

### 스트림과 병렬 스트림

parallelStream을 이용하면 병렬로 작업하고 그 결과를 재조합할 수 있다.

병렬 실행하더라도 상태 변경으로 생기는 문제를 예방할 수 있다.

항상 사용하지 않고, 컬렉션이 작을수록 직렬 연산이 빠르기에 사용전에 꼭 성능테스트를 진행하고 도입해야 한다

### 락-프리

블로킹이 처리율에 악영향을 미치고 성능을 저하시킬 수 있다는 전제하에 시작한다.

블로킹의 문제점은 스레드를 컨텍스트 스위칭할 기회가 있다는 사실을 OS에 의지하기 때문이다.

## 정리

### 싱글 스레드 애플리케이션의 동시성 기반 설계방식으로 전환시 고려점

- 순서대로 처리하는 성능을 정확히 측정 가능해야 한다.
- 변경을 적용한 다음 진짜 성능이 향상됐는지 테스트
- 성능 테스트는 재실행하기 쉬워야한다.
- 시스템이 처리하는 데이터 크기가 달라질 가능성이 큰 경우 그렇다.

### 피해야하는 것

- 병렬 스트림을 곳곳에 갖다쓴다.
- 수동으로 락킹하는 복잡한 자료구조 생성
- java.util.concurrent 에 이미 있는 구조를 다시 만드는 것

### 목표

- 동시 컬렉션을 이용한 스레드 핫성능 높이기
- 하부 자료 구조를 최대한 활용할 수 있는 형태로 액세스 설계
- 애플리케이션 전반에 걸쳐 락킹 줄이기
- 가급적 스레드를 직접 처리하지 않도록 태스크/비동기를 적절히 추상화

0 comments on commit 1639f0f

Please sign in to comment.