반응형
Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- Coroutine
- Rxjava
- 안드로이드
- ReactiveProgramming
- 알고리즘
- g 단위테스트
- 알게되는
- 스레드
- 디자인패턴
- viewmodel
- 테스트
- 글또
- 안드로이드강좌
- android
- 병럴프로그래밍
- 안드로이드스튜디오
- Compose
- 병렬프로그래밍
- 책
- kotlin강좌
- theming
- Gradle
- 코틀린
- Kotlin
- 자바
- 커스텀상태
- mockito
- 코루틴
- k8s
- 회고
Archives
- Today
- Total
선생님, 개발을 잘하고 싶어요.
자바 병렬 프로그래밍 - 1부 3장 - 객체 공유 본문
- 여러 개의 스레드에서 객체를 동시에 사용하려 할 때 섞이지 않고 안전하게 동작하도록
- 객체를 공유하고 공개하는 방법을 살핀다.
- 크리티컬 섹션?
- 코드 블록을 동기화할 때 항상 메모리 가시성(Memory Visibility) 문제가 발생한다.
- 반드시 달성돼야 하는 거
- 변수를 사용하고 있을 때 다른 스레드가 해당 변수 값을 사용하지 못하도록 막아야한다.
- 동기화 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있어야한다.
가시성
- 변수에 값을 저장하고 이후에 값을 다시 읽으면, 아까 저장한 값을 가져올 수 있을거라고 예상한다.
- 하지만 멀티 스레드 환경에서는
- 특정 변수의 값을 가져갈 때 다른 스레드가 작성한 값을 가져갈 수 없을 수 있다.
- 심지어 값을 읽지 못할 수 있다.
- 🌟 메모리상 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.
- 재배치(reordering): 특정 메소드의 소스코드가 100% 코딩된 순서로 동작함을 보장할 수 없다.
- 동기화를 지정하지 않으면 컴파일러나 프로세서, JVM이 프로그램 코드 실행 순서를 임의로 바꾸는 경우가 있다.
- 🌟 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다
- 스레드는 메모리를 공유하는데, 컴파일, 런타임 최적화 시점에 메모리 이외의 저장소를 활용할 수 있는게 원인인가? 프로세서 레지스터나 외부 캐시 등
스테일 데이터 (stale data)
- 변수를 사용하는 모든 경우에 동기화를 시켜두지 않으면 해당 변수에 대한 최신 값이 아닌 다른 값을 사용하게 되는 경우가 발생할 수 있다.
- 다른 스레드에서 작성한 값을 가져갈 수 없기 때문에, 변수를 사용하려고 접근하기 전에 다른 스레드가 값을 갱신했다면, 이미 최신 값이 아닌 값으로 작업을 진행할 수 도 있다.
단일하지 않은 64비트 연산
- 숫자형에 volatile 키워드를 사용하지 않으면 난데없는 값이 들어갈 수도 있다.
- 64비트 값에 대해 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 할 수도 있기 때문…
락과 가시성
- synchronized로 둘러싸인 코드에서 스레드 A가 사용한 모든 변수 값은, 같은 락을 사용하는 스레드 B가 실행할 때 안전하게 사용할 수 있다.
- 🌟 락은 상호 배제뿐 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.
volatile 변수
- volatile 변수는 값을 바꿨을 때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다.
- “이 변수는 공유해 사용하고, 실행 순서를 재배치 하지마”라고 컴파일러와 런타임에게 알린다.
- 프로세서의 레지스터에 캐시되지도 않고, 프로세서 외부의 캐시에도 들어가지 않기 때문에 항상 다른 스레드가 보관해둔 최신의 값을 읽어갈 수 있다.
- 락이나 동기화 기능과는 다르다. volatile 지정은 락이나 동기화 기능이 동작하시키지 않는다.
- 메모리 가시성 입장에선 volatile이나 synchronized는 비슷한 효과를 가져온다.
- 🌟 락은 가시성 + 연산의 단일성 보장, volatile은 가시성만 보장
- 🌟 다음과 같은 경우만 사용하자.
- 변수에 값을 저장하는 작업이 변수의 지금 값과 관련이 없는 경우
- 연산의 단일성도 보장해야함.
- 해당 변수의 값을 변경하는 스레드가 하나만 존재하는 경우
- 변수 값 변경이 단일 스레드에서 진행되므로 동시성이 보장될 필요 없음.
- 변수가 불변조건에 관련되어 있지 않은 경우
- 동시성 보장이 필요없음
- 변수를 사용하는 동안 어떤 경우라도 락을 걸어 둘 필요가 없는 경우 (?)
- 변수에 값을 저장하는 작업이 변수의 지금 값과 관련이 없는 경우
공개와 유출
- 공개(published) 되었다 👉 어떤 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만듬
- 이 경우에는 반드시 해당 객체를 동기화시켜야 한다.
- 유출 상태(escaped) 👉 의도적으로 공개하지 않았지만, 외부에서 사용할 수 있게 공개된 경우
- 특정 객체를 공개 하면서, 그와 관련된 다른 객체까지 덩달아 공개하는 경우
- 내부 클래스(inner 클래스)의 인스턴스를 외부에 공개하는 경우
- 내부 클래스 인스턴스는 항상 outer class의 참조를 가지고 있다.
- 생성 메소드 실행 도중에 this 변수가 외부에 공개되는 경우도 있다.
- 어떤 객체건 일단 유출되고 나면, 다른 스레드가 유출된 클래스를 잘못 사용할 수 있다고 가정해야함.
- 🌟 생성 메소드 실행 도중에 this가 외부에 유출되면 안된다.
- this를 넘겨서 이벤트 등록하는 이벤트를 생각할 수 있는데 이벤트 등록을 생성자에서 하는게 아니라 팩토리 메서드를 만들어 사용할 수 있다.
// 생성자로 source를 받고 registerListener 코드를 등록하면 // 무명 객체에 의해 부주의하게 this 참조가 유출되게 된다. class SafeListener private constructor() { val listener: EventListener = object : EventListener { .. } companion object { fun newInstance(source: EventSource): SafeListener { safe = SafeListener() source.registerListener(safe.listener) return safe } } }
스레드 한정
- 모든 변경가능한 객체를 다룰 때 선택할 수 있는 전략은 두 가지다.
- 스레드간 공유하는 경우는 동기화
- 스레드간 공유하지 않음
- 2번 처럼, 객체를 사용하는 스레드를 한정(confine)하는 방법으로 스레드 안정성을 확보할 수 있다.
- GUI 프로그래밍에선 이벤트 스레드를 제외한 다른 스레드에서 UI 객체를 사용할 수 없다.
- 스레드 한정 기법은 프로그램 처음 설계부터 다뤄야한다.
- 프로그램 구현 내내 한정 기법을 계속 적용해야한다.
- 이러한 기법을 사용해도, 개발자는 스레드에 한정된 객체가 외부로 유출되지 않도록 신경 써야 한다.
스레드 한정 - 주먹구구식
- GUI 모듈과 같은 특정 시스템을 단일 스레드로 동작하도록 만들 것이냐?
- 특정 스레드에 한정하려는 객체가 volatile로 선언되어 있다면?
- volatile 변수는 단일 스레드에서만 쓰기 작업을 한다면, 읽기 작업은 멀티 스레드에서 접근해도 안전하다. (가장 최근에 업데이트된 값을 정확하게 읽어갈 수 있다.)
스택 한정
- 스택 한정 기법 👉 객체를 로컬 변수를 통해서만 사용할 수 있는 특별한 경우의 스레드 한정 기법
- 로컬 변수는 모두 암묵적으로 현재 실행 중인 스레드에 한정되어 있다고 볼 수 있다. (스레드마다 스택을 가지므로 스택에 저장되는 변수는 스레드에 한정될 것)
- reference 변수를 스택에 저장한다면, 그 객체에 대한 참조가 외부로 유출되지 않도록 개발자가 주의해야 한다. (객체의 참조는 힙, 즉 스레드 간 공유 영역에 존재하므로)
- 스레드에 안전하지 않은 객체라도 특정 스레드 내부에서만 사용한다면 동기화 문제는 없다. 안전하다.
ThreadLocal
- 호출하는 스레드마다 다른 값을 사용할 수 있도록 관리
- ThreadLocal의 get메서드를 호출하면, 현재 실행 중인 스레드에서 최근에 set된 값을 가져올 수 있다.
- e.g. 버퍼 처럼 임시로 사용할 객체를 매번 새로 생성하는 대신, 이미 만들어진 객체를 재활용하고자 할 때 많이 쓰인다.
- 개념적으로 Map<Thread, T> 타입으로 생각할 수 있다.
- 전역 변수가 아니면서도 전역 변수처럼 동작하므로 오용에 주의하자.
불변성 (immutablity)
- 객체의 상태가 변하지 않는다면? 지금까지 봤던 복잡하고도 다양한 문제가 일순에 사라진다!
- 🌟 불변 객체는 언제라도 스레드에 안전하다.
- 객체 불변과 참조 불변은 구분해서 생각하자. 데이터가 불변 객체에 들어있다고 해도, final이 아니면 다른 불변 객체로 참조를 바꿀 수 있다. 프로그램의 데이터가 언제든지 바뀌는 셈
final 변수
- 변수의 값을 변경할 수 없다.
- 초기화 안정성을 보장한다. 별다른 동기화 작업 없이도 불변 객체를 자유롭게 사용하고 공유할 수 있다.
불변 객체를 공개할 때 volatile 키워드 사용
- 경쟁 조건을 만드는 여러 변수를 모두 모아 하나의 불변 객체로 관리하면? 경쟁 조건을 방지할 수 있다.
안전 공개
- 특정 데이터를 여러 개의 스레드에서 사용하도록 공유할 때 적절한 동기화 방법을 적용하지 않는다면 굉장히 이상한 일이 발생할 가능성이 높다.
불변 객체와 초기화 안정성
- 🌟 불변 객체는 별다른 동기화 적용하지 않아도, 어느 스레드건 마음껏 안전하게 사용할 수 있다.
안전한 공개 방법의 특성
- 불변 객체가 아닌 객체는 올바른 방법으로 안전하게 공개해야 한다.
- 공개하는 스레드, 사용하는 스레드 양쪽 모두에 동기화 방법을 적용
- 안전하게 공개? 👉 객체 참조 및 내부의 상태를 외부 스레드도 동시에 볼 수 있어야한다.
- 객체 참조를 static 메소드에서 초기화
- 객체 참조를 volatile or AtomicReference 클래스에 보관
- 객체 참조를 올바르게 생성된 클래스 내부 final 변수에 보관
- 락을 사용해 올바르게 막혀 있는 변수에 객체 참조 보관 (스레드 세이프한 API)
- 스레드 동기화 기능 갖고 있는 API
- Hashtable, ConcurrentMap ... 스레드 안전한 맵, 키 값
- Vector, CopyOnWriteArrayList, CopyOnWriteArraySet … 스레드 안전한 컬렉션
- BlokcingQueue, ConcurrentLinkedQueue …
결과적으로 불변인 객체
- 불변 객체를 다시 찬양하자면….
- 특정 객체를 안전한 방법으로 공개했을 경우
- 객체에 대한 참조를 갖고 객체를 불러와 사용하는 시점에는 공개하는 시점의 객체 상태를 정확하게 사용할 수 있고,
- 값이 바뀌지 않는 한 여러 스레드에서 동시에 값을 가져다 사용해도 동기화 문제가 없다.
- 불변 객체인 것처럼 사용하면 동기화 작업을 하지 않아도 된다.
가변 객체
- mutable object를 사용할 때는 공개하는 부분, 가변 객체를 사용하는 모든 부분에 동기화 코드를 작성해야한다.
- 🌟 가변성에 따라 객체를 공개할 때 필요한 점
- 불변 객체 → 어떤 방법으로 공개해도 문제 없음
- 결과적 불변 객체 → 안전하게 공개하자
- 가변 객체 → 안전하게 공개 & 스레드 안전하게 만들거나 & 락으로 동기화 필요.
객체를 안전하게 공유하기
- 객체를 사용하기 전에 고려할 거
- 동기화 코드를 적용해 락을 확보해야 하나?
- 객체 내부 값을 바꿔도 괜찮나?
- 값을 읽기만 해야하나?
- 객체를 공개할 때
- 해당 객체를 어떤 방법으로 사용할 수 있고, 사용해야 하는지에 대해 정확한 설명 필요
🌟 병렬 프로그램에서 객체 공유 원칙
스레드 한정
- 스레드 한정 객체는 완전하게 해당 스레드 내부에만 존재 및 그 스레드만 호출 사용 가능
읽기 전용 객체 공유
- 불변 객체와 결과적 불변 객체를 포함한다.
- 별다른 동기화 작업 없이도 여러 스레드에서 마음껏 사용가능
스레드 안전 객체 공유
- 객체 내부적으로 필수 동기화 기능이 있으니 외부에서 신경쓸 필요 없다. 여러 스레드에서 마음껏 사용
동기화 방법 적용
- 객체 접근을 위해 락 획득을 기다린다.
'일상 > 책 리뷰' 카테고리의 다른 글
자바 병렬 프로그래밍 - 1부 5장 - 구성 단위 (0) | 2022.06.14 |
---|---|
자바 병렬 프로그래밍 - 1부 4장 - 객체 구성 (0) | 2022.06.11 |
자바 병렬 프로그래밍 - 1부 2장 - 스레드 안정성 (0) | 2022.06.09 |
자바 병렬 프로그래밍 - 0부 1장 - 개요 (0) | 2022.06.09 |
Kotlin in Action - 2부 11장 - DSL 만들기 (0) | 2022.06.07 |
Comments