자바 병렬 프로그래밍 - 1부
Intro
자바 병렬 프로그래밍은 총 4부로 이루어져 있으며 1부는 “이론”, 2~4부가 “실제” 위주이다. 특히 4부는 “고급 주제”이다.
해당 책의 내용을 자세히 작성할 생각은 없고 기억할 부분만 작성한다.
따라서 자세한 내용은 잘 정리하신 분의 블로그 + 책을 참고하자.
1부에서는 무엇을 배울까?
- 병렬 프로그래밍 문제점을 피하는 방법
- 스레드 안전한 클래스 작성법
- 스레드 안전성 확인법
- 자바에서 제공하는 스레드 안전한 클래스나 동기화 클래스 등등
1부 - 기본 원리
요약
스레드 안전성
단일 스레드 프로그램은 스레드 간에 데이터 공유가 없어서 동기화 필요가 없다. -> 따라서 데이터 공유가 있으면 반드시 동기화 필요하다.
자바의 “동기화” 수단은 synchronized 키워드, volatile 변수, 명시적 락, 단일 연산 변수(atomic variable)
등…
스레드, 락은 목적을 위한 도구
스레드 안전성은 데이터에 제어 없이 동시에 접근하는 걸 막으려는 목적이고, 여러 스레드가 클래스에 접근할 때 계속 정확하게 동작해야 스레드 안전하다 한다.
스레드 안전한 코드 작성 기본은 공유 및 변경 가능한 상태 변수 접근을 관리하는 것
상태 변수를 동기화 없이 접근하는 것을 고치는 방법 3가지
- 해당 상태 변수를 스레드 간에 공유하지 않는다.
- 해당 상태 변수를 변경할 수 없도록 한다.
- 해당 상태 변수에 접근할 땐 언제나 동기화를 사용한다.
클래스 설계 때부터 스레드 안전성 고려 권장
- 캡슐화나 데이터 은닉 기법이 스레드에 안전한 클래스를 만드는데 도움
상태 없는 클래스는 항상 스레드 안전 -> 지역변수는 왜 아닌가? 라고 생각들면 자바를 다시 공부하자
경쟁 조건(race condition)은 병렬 프로그래밍에서 타이밍이 안 좋아서 잘못된 결과가 나올 수 있는 것
-
++count
는 단일 연산이 아니라 복합 동작 -> 현재 값 가져오고, 1을 추가하고, 다시 write (3번 연산) - 늦은 초기화도 경쟁 조건이 가능
- 가능하면
AtomicLong
처럼 스레드 안전 클래스 자체를 사용 권장
-> 단 여러 상태 변수를 사용하게 되면 또 문제
-> 락을 사용
상태를 일관성 있게 유지하려면 관련 있는 변수(여러 변수)들을 하나의 단일 연산으로 구현해야 한다.
-
암묵적인 락(or 모니터 락)이
synchroinzed
코드를 만나면 동작 -> 모든 자바 객체는 락 내장(뮤텍스로 동작 = Mutual Exclusion = 상호 배제) -
재진입성 -> 암묵적인 락은 재진입 가능하기 때문에 자기가 이미 획득했던 락을 다시 확보 가능
- 즉, 확보 횟수를 따로 기록하므로
synchroinzed
블록을 벗어나지 않는 이상 추가 락 없이 진입 가능
- 즉, 확보 횟수를 따로 기록하므로
- @GuardedBy -> 락으로 보호 표시 -> 유지보수에 중요
- 여러 변수에 관한 불변 조건 존재 시 모두 같은 락으로 보호할 것
락의 일반적인 사용 예 : 모든 변경 가능한 변수를 객체 안에 캡슐화, 암묵적인 락을 사용해 변수 접근 경로를 동기화하여 보호하는 방식
동기화 범위를 줄이는 게 좋다. 단, 너무 줄이는 건 안 된다.
복잡하고 오래 걸리는 내용은 락 사용 안 하는 걸 추천한다. -> 예로 오래 걸리는 다운로드라든지.
단순성(전체 메소드를 동기화)과 병렬 처리 능력(최대한 짧은 부분만 동기화)의 균형이 중요하다.
객체 공유
synchroinzed
는 한 스레드에서 변경한 특정 메모리 값이 다른 스레드에서 제대로 읽어지게 하는 메모리 가시성의 측면도 존재한다.
-
stale data
란 진부한 데이터(이전 데이터) -> 동기화가 필요 (synchroinzed
) - 약한 형태의
synchroinzed
는 ->volatile
이고, 다른 스레드에서 항상 최신 값을 읽게 한다.- volatile은 메인 메모리에 적재하고 캐시 사용X, 가시성만 보장 (단일성은 보장X)
- 락을 사용하면 가시성 + 단일성(연산) 보장
내부 클래스는 항상 부모 클래스 참조를 가진다. 생성 메소드를 실행하는 도중에는 this 변수가 외부로 유출되면 않 된다. -> 팩토리 메소드 사용하자
객체를 동기화하지 않고 같은 효과를 얻고 싶으면??
-
스레드 한정도 좋다
- 스택 한정 (=스레드 내부의 스택에서만 존재해서 다른 스레드에서 볼 수가 없다.) -> 단, 후임 개발자가 해당 변수 외부에 노출시키지 않게끔 이를 표시하는 게 좋다.
- ThreadLocal -> 전역 변수에 사용하면 스레드 안전성 보장
-
불변 객체 -> 언제나 스레드 안전하다.
- 불변 객체에 volatile 키워드를 쓰면 스레드에 안전하다.
외부에서 사용할 일 없는 변수 private 하는 것처럼 변경될 일 없는 변수는 final 하자
객체 공유에 가장 많이 사용하는 원칙들?
- 스레드 한정 : 스레드 내부에 존재하면서 그 스레드에서만 호출
- 읽기 전용 객체를 공유 : 동기화하지 않아도 여러 스레드에서 마음껏 사용
- 스레드에 안전한 객체를 공유 : 안전한 객체는 객체 내부에 동기화 기능이 만들어져 있기 때문에 외부에서 동기화 신경 쓰지 않고 사용
- 동기화 방법 적용 : 특정 객체에 동기화 방법을 적용해두면 지정한 락을 획득하기 전에는 해당 객체를 사용할 수 없다.
객체 구성
스레드 안전한 클래스 설계
- 객체의 상태를 보관하는 변수는 무엇?
- 객체의 상태를 보관하는 변수의 값의 종류와 범위?
- 동기화 정책 명시 -> 동기화 정책 꼭 문서화! @GuardedBy 어노테이션만 써도 아주 좋다
- 고려사항 예시 -> 어떤 변수를 volatile 사용? 어떤 변수에 락을 사용? 어떤 변수를 불변 클래스로? 어떤 변수를 스레드 한정? 어떤 연산은 단일 연산으로 만들지?
상태 범위는 객체와 변수가 가질 수 있는 값의 범위 -> 좁을수록 상태 파악 쉬움
상태 의존 연산은 현재 조건에 따라 동작 여부가 결정되는 연산 -> 병렬 프로그램은 상태가 올바르게 바뀔 경우도 있으므로 이를 기다리다가 연산을 수행 방식도 존재 (세마포어, 블로킹 큐 클래스가 제공)
인스턴스 한정 기법은 데이터를 객체 내부에 캡슐화 후 메소드에서만 사용 방식 -> 자바 모니터 패턴 : 변경 가능 데이터를 모두 객체 내부에 숨긴 후 암묵적인 락으로 동시 접근 방지
이미 스레드 세이프한 클래스에 안전성을 위임 -> 자주 사용( 책임을 맡길 수 있으니까 )
- 클래스에 AtomicLong 변수만 있으면(스레드 세이프함) 클래스는 AtomicLong 상태랑 같아서 AtomicLong에 스레드 안전성 문제를 위임
- 여러 변수가 독립적이라면 마찬가지로 위임 가능
스레드 안전하게 구현된 클래스에 기능을 추가
- 클래스 재구성 방식이 좋다. -> 진짜로 해당 클래스를 재구성하면서 추가 기능을 구현하면 된다.
구성 단위
동기화된 컬렉션 클래스 -> 대표적으로 Vector
- 동기화된 컬렉션 클래스의 문제점?? 사용하는 외부 프로그램 입장에선 여전히 동기화 문제 발생 (vector의 입장에서는 동기화 문제 없는 것 )
- 다만, 대부분 “클라이언트 측 락”을 제공해서 간단히 해결 가능 -> ex:
synchronized (vector)
병렬 컬렉션 클래스 -> 대표적으로 ConcurrentHashMap, CopyOnWriteArrayList
- 동기화 컬렉션을 동기화 컬렉션 클래스를 병렬 컬렉션으로 교체하는 것만으로도 성능을 향상시킬 수 있다.
- ConcurrentHashMap -> 락 스트라이핑(11장)이라 불리는 세밀한 동기화 기법으로 병렬성 업그레이드
- CopyOnWriteArrayList -> List보다 병렬성 업그레이드 -> List 전체에 락이 아닌 “변경할 때마다 복사”로 스레드 안전성을 확보
프로듀서-컨슈머 패턴(디자인 패턴) -> 블로킹 큐
- put : 큐 가득 차면 추가 공간 생길 때까지 대기
- take : 큐 비었으면 값 생길 때까지 대기
작업 가로채기(디자인 패턴) -> Deque
- 규모가 큰 시스템을 구현하기에 적당
- 프로듀서와 컨슈머처럼 경쟁이 일어나지 않음
- 예로 자신의 덱이 비었으면 다른 덱의 작업을 가져가 처리해서 쉬는 스레드가 없게 한다.
블로킹 연산은 일반 연산과 달리 멈춘 상태에서 특정한 신호를 받아야 계속해서 연산
InteruptedException은 블로킹 큐의 put, take에서 발생 가능 -> 블로킹 메소드
- InterruptedExcetion 발생 대처법 : InterruptedExcetion 전달 또는 인터럽트를 무시하고 복구
- 인터럽트 무시하고 복구할 때의
catch
문에서 꼭 인터럽트 발생 사실을 기록
- 인터럽트 무시하고 복구할 때의
- 인터럽트는 스레드가 서로 협력해서 실행하기 위한 방법
동기화 클래스는 상태 정보를 사용해 스레드 간의 작업 흐름을 조절할 수 있도록 만들어진 모든 클래스
- 블로킹 큐, 세마포어, 배리어, 래치 등 + 직접 구현
-
FutureTask는 래치와 비슷하게 동작 -> 미리 작업 실행 용도
- Executor 프레임웍에서 비동기 작업 실행에 사용
- get()으로 결과 가져옴
- 연산 작업이 끝나는 즉시 연산 결과를 리턴
- 연산 도중이면 끝날 때까지 기다렸다가 결과를 알려줌
효율적이고 확장성 있는 결과 캐시 구현 -> 개선 과정을 볼 것
- 무작정
synchronized
사용 시 순차적으로 실행 -> 병렬성이 그닥… - 일반 hashMap에서
ConcurrentHashMap
를 사용 -> 병렬성 개선. 단, 동일한 연산 가능성 -
FutureTask
사용해서 해결 -> 단, 아직도 동일 연산 가능성 - ConcurrentMap의
putIfAbsent
사용해서 복합 연산을 단일 연산 사용 -> 성공- Map의 put이 복합 연산(없으면 추가하라)이라서 동일 연산의 문제가 생겼던 것이다.
댓글남기기