객체 공유

여러 개의 스레드에서 객체를 동시에 사용하려 할 때 섞이지 않고 안전하게 동작하도록 객체를 공유하고 공개하는 방법을 살핀다.

크리티컬 섹션?
코드 블록을 동기화할 때 항상 메모리 가시성(Memory Visibility) 문제가 발생한다.

변수를 사용하고 있을 때 다른 스레드가 해당 변수 값을 사용하지 못하도록 막아야한다. 동기화 블록을 빠져나가고 나면 다른 스레드가 변경된 값을 즉시 사용할 수 있어야한다.

가시성

변수에 값을 저장하고 이후에 값을 다시 읽으면, 아까 저장한 값을 가져올 수 있을거라고 예상한다. 하지만 멀티 스레드 환경에서는

특정 변수의 값을 가져갈 때 다른 스레드가 작성한 값을 가져갈 수 없을 수 있다.
심지어 값을 읽지 못할 수 있다.

메모리상 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.

재배치(reordering): 특정 메소드의 소스코드가 100% 코딩된 순서로 동작함을 보장할 수 없다. 동기화를 지정하지 않으면 컴파일러나 프로세서, JVM이 프로그램 코드 실행 순서를 임의로 바꾸는 경우가 있다.

여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다

스레드는 메모리를 공유하는데, 컴파일, 런타임 최적화 시점에 메모리 이외의 저장소를 활용할 수 있는 프로세서 레지스터나 외부 캐시..

스테일 데이터 (stale data)

변수를 사용하는 모든 경우에 동기화를 시켜두지 않으면 해당 변수에 대한 최신 값이 아닌 다른 값을 사용하게 되는 경우가 발생할 수 있다. 다른 스레드에서 작성한 값을 가져갈 수 없기 때문에, 변수를 사용하려고 접근하기 전에 다른 스레드가 값을 갱신했다면, 이미 최신 값이 아닌 값으로 작업을 진행할 수 도 있다.

숫자형에 volatile 키워드를 사용하지 않으면 난데없는 값이 들어갈 수도 있다.
64비트 값에 대해 메모리에 쓰거나 읽을 때 두 번의 32비트 연산을 할 수도 있다

락과 가시성

synchronized로 둘러싸인 코드에서 스레드 A가 사용한 모든 변수 값은, 같은 락을 사용하는 스레드 B가 실행할 때 안전하게 사용할 수 있다. 락은 상호 배제뿐 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.

volatile 변수

volatile 변수는 값을 바꿨을 때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다.
“이 변수는 공유해 사용하고, 실행 순서를 재배치 하지마”라고 컴파일러와 런타임에게 알린다.
프로세서의 레지스터에 캐시되지도 않고, 프로세서 외부의 캐시에도 들어가지 않기 때문에 항상 다른 스레드가 보관해둔 최신의 값을 읽어갈 수 있다.

락이나 동기화 기능과는 다르다. volatile 지정은 락이나 동기화 기능이 동작하시키지 않는다. 메모리 가시성 입장에선 volatile이나 synchronized는 비슷한 효과를 가져온다. 락은 가시성 + 연산의 단일성 보장, volatile은 가시성만 보장

다음과 같은 경우만 사용하자.

변수에 값을 저장하는 작업이 변수의 지금 값과 관련이 없는 경우 ->연산의 단일성도 보장해야함.
해당 변수의 값을 변경하는 스레드가 하나만 존재하는 경우 -> 변수 값 변경이 단일 스레드에서 진행되므로 동시성이 보장될 필요 없음.
변수가 불변조건에 관련되어 있지 않은 경우 -> 동시성 보장이 필요없음
변수를 사용하는 동안 어떤 경우라도 락을 걸어 둘 필요가 없는 경우

공개와 유출

공개(published) -> 어떤 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만듬 이 경우에는 반드시 해당 객체를 동기화시켜야 한다.

유출 상태(escaped) -> 의도적으로 공개하지 않았지만, 외부에서 사용할 수 있게 공개된 경우 특정 객체를 공개 하면서, 그와 관련된 다른 객체까지 덩달아 공개하는 경우

내부 클래스(inner 클래스)의 인스턴스를 외부에 공개하는 경우 -> 내부 클래스 인스턴스는 항상 outer class의 참조를 가지고 있다.

생성 메소드 실행 도중에 this 변수가 외부에 공개되는 경우도 있다.
어떤 객체건 일단 유출되고 나면, 다른 스레드가 유출된 클래스를 잘못 사용할 수 있다고 가정해야함.
생성 메소드 실행 도중에 this가 외부에 유출되면 안된다.

스레드 한정

모든 변경가능한 객체를 다룰 때 선택할 수 있는 전략은 두 가지다.

스레드간 공유하는 경우는 동기화
스레드간 공유하지 않음 -> 객체를 사용하는 스레드를 한정(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를 사용할 때는 공개하는 부분, 가변 객체를 사용하는 모든 부분에 동기화 코드를 작성해야한다.

가변성에 따라 객체를 공개할 때 필요한 점
불변 객체 → 어떤 방법으로 공개해도 문제 없음
결과적 불변 객체 → 안전하게 공개하자
가변 객체 → 안전하게 공개 & 스레드 안전하게 만들거나 & 락으로 동기화 필요.

객체를 안전하게 공유하기

객체를 사용하기 전에 고려할 거\

동기화 코드를 적용해 락을 확보해야 하나?
객체 내부 값을 바꿔도 괜찮나?
값을 읽기만 해야하나?

객체를 공개할 때 -> 해당 객체를 어떤 방법으로 사용할 수 있고, 사용해야 하는지에 대해 정확한 설명 필요

병렬 프로그램에서 객체 공유 원칙

스레드 한정 -> 스레드 한정 객체는 완전하게 해당 스레드 내부에만 존재 및 그 스레드만 호출 사용 가능

읽기 전용 객체 공유

불변 객체와 결과적 불변 객체를 포함한다.
별다른 동기화 작업 없이도 여러 스레드에서 마음껏 사용가능

스레드 안전 객체 공유 -> 객체 내부적으로 필수 동기화 기능이 있으니 외부에서 신경쓸 필요 없다. 여러 스레드에서 마음껏 사용

동기화 방법 적용 -> 객체 접근을 위해 락 획득을 기다린다.

Last updated