12. 동시 성능 기법
#
병렬성이란?- 멀티코어에서는 암달의 법칙이 연산 태스크의 실행 속도를 향상시키는 핵심 요소입니다.
T(n) = S + (1/N) * (T - S)
#
자바 동시성 기초- 가장 기본은 동시성 특유의 반직관적인 모습을 이해하는 것입니다.
- 동기화를 사용할 때는 아주 신중하게 설계하고 미리 잘 따져봐야합니다. 잘못 사용되면 느려집니다.
#
JMM의 이해- JMM : 자바 메모리 모델
- JMM은 다음 질문에 답을 찾는 모델입니다.
- 두 코어가 같은 데이터를 액세스하면 어떻게 되는지
- 언제 두 코어가같은 데이터를 바라본다고 장담할 수 있는지
- 메모리 캐시는 두 질문의 답에 어떤 영향을 미치는지
JMM 같은 메모리 모델은 두 가지 방식으로 접근합니다.
- 강한 메모리 모델
- 전체 코어가 항상 같은 값을 바라봅니다.
- 약한 메모리 모델
- 코어마다 다른 값을 바라볼 수 있고 그 시점을 제어하는 특별한 캐시 규칙이 있습니다.
자바 명세서는 JVM 구현체가 반드시 지켜야할 규칙들이 있습니다.
#
동시성 라이브러리 구축동시 라이브러리를 구성하는 핵심 요소는 다음의 일반 카테고리로 분류됩니다.
- 락, 세마포어
- 아토믹스
- 블로킹 큐
- 래치
- 실행자
#
UnsafeUnsafe로 할 수 있는 일은 다음과 같습니다.
- 객체는 할당하지만 생성자는 실행하지 않습니다.
- 원메모리에 액세스하고 포인터 수준의 연산을 수행합니다.
- 프로세서별 하드웨어 특성(CAS)를 이용합니다.
이를 통해서 고수준의 프레임워크 기능을 구현할 수 있습니다.
- 신속한 역직렬화
- 스레드에 안전한 네이티브 메모리 액세스
- 아토믹 메모리 연산
- 효율적인 객체/메모리 레이아웃
- 커스텀 메모리 펜스
- 네이티브 코드와의 신속한 상호작용
- JNI에 관한 다중 운영체제 대체물
- 배열 원소에 volatile 하게 액세스
#
아토믹스와 CAS- 변수를 업데이트하기 위해 여러 차례 재시도하는 경우, 횟수만큼 성능이 안좋아집니다.
#
락과 스핀락- 블로킹된 스레드를 CPU에 활성 상태로 놔두고 아무 일도 시키지 않은 채 락을 얻을때까지 재시도하는 것을 스핀락이라고 합니다.
스핀락에 대한 핵심 개념은 다음과 같습니다.
- 테스트하고 세팅하는 작업은 반드시 아토믹해야합니다.
- 스핀락에 경합이 발생하면 대기중인 프로세서는 빽빽한 루프를 실행합니다.
#
동시 라이브러리 정리java.util.concurrent
락#
lock()
: 기존 방식대로 락을 획득하고 사용할 수 있을때까지 블로킹합니다.newCondition()
: 락 주위 조건을 설정해 좀 더 유연하게 락을 활용합니다.tryLock()
: 락 획득을 위해 시도합니다.unlock()
: 락을 해제합니다
#
읽기/쓰기 락- 읽기, 쓰기 작업 횟수가 많이 차이나므로
#
세마 포어- 마포어는 풀 스레드나 DB 접속 객체 등 여러 리소스의 액세스를 허용하는 독특한 기술을 제공
#
동시 컬렉션ConcurrentHashMap
등의 동시 컬렉션을 사용하면 좋습니다.
#
래치와 배리어- 쓰레드 세트의 실행을 제어하는 유용한 기법입니다.
#
실행자와 태스크 추상화- 일반적으로 저수준의 스레드 문제를 직접 처리하는 것 보다는
java.util.concurrent
패키지에서 적절한 추상화 동시 프로그래밍 지원 기능을 쓰는 것이 좋습니다.
#
비동기 실행- Executors는 헬퍼 클래스로, 선택한 로직에 따라서 서비스 및 기반 스레드 풀을 생성하는 new* 팩토리 메서드 시리즈를 제공합니다. 보통 이 팩토리 메서드로 실행자 메서드를 생성합니다.
#
ExecutorService 선택하기- 올바른 ExecutorService를 선택하면 비동기 프로세스를 적절히 잘 제어할 수 있고, 풀 스레드 개수를 정확히 잘 정하면 성능이 뚜렷이 향상될 수 있습니다
#
포크/조인- 하위 분할 테스크를 효율적으로 처리합니다.
- 작업 빼앗기 알고리즘을 구현합니다.
#
최신 자바 동시성- 자바 동시성은 실행 시간이 긴 블로킹 캐스크를 다른 스레드와 함께 실행할 수 있는 환경을 염두하고 설계되었습니다.
- CPU 리소스를 효율적으로 사용하는 문제가 더욱 중요하게 부각됍니다.
- 현대 자바는 언어 및 표준 라이브러리에 내장된 추상화를 이용해 성능을 크게 높일 수 있는 환경을 제공합니다.
#
스트림과 병렬 스트림- 자바 8의 가장 큰 변경 사항
#
락-프리 기법- 블로킹이 처리율에 악영향을 미치고성능을 저하시킬 수 있다는 전제하에서 시작합니다.
- CPU 코어를 차지하는 것은 사용률, 전력 소비 측면에서 비용이 듭니다.
#
액터 기반 기법- 태스크를 스레드 하나보다 더 작게 나타내려는 다양한 접근 방식이 고안되었습니다.
- 액터는 그 자체로 고유한 상태와 로직을 갖고 있습니다. 동시에 다른 액터와 소통하는 메일박스 체계를 갖춘, 작고 독립적인 처리 단위입니다.
- 액터는 병렬 시스템 내부에서 하나의 네트워크를 형성하고 그 속에서 각자 나름대로 작업을 수행함으로써 하부 동시 모델을 완전히 추상화한 모습을 바라봅니다
일반적으로 락킹 체제보다 아카를 쓰는 것이 다음의 장점이 있습니다.
- 도메인 모델 내에서 가변 상태를 캡슐화하는 건 의외로 까다로운 일입니다. 특히, 객체 내부 요소를 가리키는 레퍼런스가 언제라도 제어권 밖으로 벗어날 수 있기 때문에 더욱더 그렇습니다.
- 상태를 락으로 보호하면 처리율이 크게 떨어질 수 있습니다.
- 락을 쓰면 데드락을 비롯한 별별 문제가 유발될 수 있습니다.
#
마치며.- 싱글 스레드 애플리케이션을 동시성 기반의 설계 방식으로 전환할 때는 다음을 고려합니다.
- 순서대로 죽 처리하는 성능을 정확히 측정할 수 있어야 합니다.
- 변경을 적용한 다음 진짜 성능이 향상됐는지 테스트해야합 니다.
- 성능 테스트는 재실행하기 쉬워야 합니다. 특히, 시스템이 처리하는 데이터 크기가 달라질 가능성이 큰 경우 그렇습니다.
- 다음과 같은 유혹은 피해야합니다.
- 병렬 스트림을 곳곳에 갖다쓴다.
- 수동으로 락킹하는 복잡한 자료 구조를 생성한다.
- java.util.concurrent에 이미 있는 구조를 다시 만든다.
- 다음과 같이 목표를 정해야합니다.
- 동시 컬렉션을 이용해 스레드 핫 성능을 높입니다.
- 하부 자료 구조를 최대한 활용할 수 있는 형태로 액세스를 설계합니다.
- 애플리케이션 전반에 걸쳐 락킹을 줄입니다.
- 가급적 스레드를 직접 처리하지 않도록 태스크/비동기를 적절히 추상화합니다.