Chapter 03. 객체 공유
- 여러개의 스레드에서 특정 객체를 동시에 사용하려 할 때 섞이지 않고 안전하게 동작하도록 객첼ㄹ 공유하고 공개하는 방법을 살펴본다.
- synchronized 키워드를 사용해 동기화시킨 블록이 단일 연산인 것처럼 동작하게 할 수 있다.
3.1 가시성
재배치 (reordering)
현상은 읽기 작업을 수행하는 스레드가 변수의 값을 먼저 읽어가는 상황이다.
재배치 현상은 특정 메소드의 소스코드가 100% 코딩된 순서로 동작한다는 점을 보장할 수 없다는 점에서 기인하는 문제이다.
단일 스레드로 동작할 때는 차이점을 전혀 알아챌 수 없지만 여러 스레드가 동시에 동작하는 경우에는 확연하게 나타날 수 있다.
동기화 기능을 지정하지 않으면 컴파일러나 프로세서, JVM 등이 프로그램 코드가 실행되느 순서를 임의로 바꿔 실행하는 이상한 경우가 발생하기도 한다. 다시 말하자면, 동기화 되지 않은 상황에서 메모리상의 변수를 대상으로 작성해둔 코드가 '반드시 이런 순서로 동작할 것이다'라고 단정지을 수 없다.
3.1.1 스테일 데이터
변수의 최신 값을 이용하지 못하고 다른 값을 사용하게 되는 경우가 발생할 수 있다. 항상 스테일 데이털ㄹ 사용하게 될 때도 있고, 정상적으로 동작하는 경우도 있다는 점이다. 다시 말하자면 특정 스레드가 어떤 변수를 사용할때 정상적인 최신 값을 사용할 수
도 있고, 올바르지 않은 값을 사용할 수
도 있다는 말이다.
@NotThreadSafe
public class MutableInteger {
private int value;
public int get() {
return value;
}
public void set(int value) {
this.value = value;
}
}
- 동기하되지 않은 상태로 정수 값을 보관하는 클래스
@ThreadSafe
public class SynchronizedInteger {
@GuardedBy("this") private int value;
public synchronized int get() {
return value;
}
public synchronized void set(int value) {
this.value = value;
}
}
- 동기화된 상태로 정수 값을 보관하는 클래스
3.1.2 단일하지 않은 64비트 연산
동기화되지 않은 상태에서 특정 스레드가 변수의 값을 읽으려 한다면 스테일 상태의 값을 읽어갈 가능성이 있긴 하지만, 그래도 전혀 엉뚱한 값을 가져가는 것이 아니라 바로 이전에 다른 스레드에서 설정한 값을 가져가게 된다. 말하자면 전혀 난데 없는 값이 생기지 않는다
는 정도로 생각할 수 있겠다.
하지만, 64비트를 사용하는 숫자형에 volatile 키워드를 사용하지 않은 경우에는 난데없는 값마저 생길 가능성이 있다. 자바 메모리 모델은 메모리에서 값을 가져오고fetch
저장store
하는 연산이 단일해야 ㅎㄴ다고 정의하고 있지만, volatile로 지정하지 않은 long이나 double형의 64비트 값에 대해서는 메모리에 쓰거나 읽을때 두번의 32비트 연산을 사용할 수 있도록 허용하고 있다.
3.1.3 락과 가시성
락은 상호 배제(mutual exclusion)뿐만 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다. 변경 가능하면서 여러 스레드가 공유해 사용하는 변수를 각 스레드에서 각자 최신의 정상적인 값으로 활용하려면 동일한 락을 사용해 모두 동기화시켜야 한다.
3.1.4 volatile 변수
자바에서는 volatile 변수로 약간 다른 형태의 좀더 약한 동기화 기능을 제공한다. volatile로 선언된 변수의 값을 바꿨을때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다.
- 특정 변수를 선언할 때
volatile
키워드를 지정하면, 컴파일러와 런타임 모두이 변수는 공유해 사용하고, 따라서 ㅅㄹ행 순서를 재배치해서는 안 된다
고 이해한다. volatile
로 지정된 변수는 프로세서의 레지스터에 캐시되지도 않고, 프로세서 외부의 캐시에도 들어가지 않기 때문에 volatile 변수의 값을 읽으면 항상 다른 스레드가 보관해둔 최신의 값을 읽어갈 수 있다.volatile
변수를 사용할 때에는 아무런 락이나 동기화 기능이 동작하지 않기 때문에synchronized
를 사용한 동기화보다는 아무래도 강도가 약할 수 밖에 없다.volatile
변수의 가시성 효과는 변수 자체의 값에 대한 범위보다 약간 확장되어 있다.
스레드 A가 volatile 변수에 값을 써넣고 스레드 B가 해당 변수의 값을 읽어 사용한다고 할 때, 스레드 B가 volatile 변수의 값을 읽고나면 스레드 A 가 변수에 값을 쓰기전에 볼 수 있었던 모든 변수의 값을 스레드 B도 모두 볼수 있기 때문에 메모리 가시성 입장에서 본다면 volatile 변수를 사용하는 것과 synchronized 키워드로 특정한 코드를 묶는 게 비슷한 효과를 가져오고, volatile 변수의 값을 읽고 나면 synchronized 블록에 진입하는 것과 비슷한 상태에 해당한다.
어쨋든 메모리 가시성에 효과가 있긴 하지만 그렇다고 volatile 변수에 너무 의존하지 않는게 좋다.
- volatile 변수만 사용해 메모리 가시성을 확보하도록 장성한 코드는 synchronized로 직접 동기화한 코드보다 훨씬 읽기가 어렵고, 따라서 오류가 발생할 확률도 높다.
동기화하고자 하는 부분을 명확하게 볼 수 있고, 구현하ㅏ기가 훨씬 간단한 경우에만 volatile 변수를 활용하자. 반대로 작은 부분이라도 가시성을 추론해봐야 하는 경우에는 volatile변수를 사용하지 않는 것이 좋다. volatile 변수를 사용하는 적절한 경우는 일반적으로 변수에 보관된 클래스의 상태에 대하ㅏㄴ 가시성을 확보하거나 중요한 이벤트가 발생했다는 등의 정보를 정확하게 전달하고자 하는 경우 등이 해당된다.
락을 사용하면 가시성과 연산의 단일성을 모두 보장받을 수 있다. 하지만 volatile 변수는 연산의 단일성은 보장하지 못하고 가시성만 보장한다.
정리하자면, volatile 변수는 다음과 같은 상황에서만 사용하는 것이 좋다.
- 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나 해당 변수의 값을 변경하는 스레드가 하나만 존재
- 해당 변수가 객체의 불변조건을 이루는 다른 변수와 달리 불변조건에 관련되어 있지 않다.
- 해당 변수를 사용하는 동안에는 어떤 경우라도 락을 걸어 둘 필요가 없는 경우
3.2 공개와 유출
- 특정 객체를 현재 코드의 스코프 범위 밖에서 사용할 수 있도록 만들면
공개(published)
되었다고 한다. - 의도적으로 공개시키지 않았지만 외부에서 사용할 수 있게 된 경우를
유출 상태(escaped)
라고 한다.
public static Set<Secret> knownSecrets;
public void initialized() {
knownSecrets = new HashSet<Secret>();
}
- 스코프에 관계없이 완전히 공개되는 경우,
객체 공개
특정 객체 하나를 공개한다고 해도, 그와 관련된 다른 객체까지 덩달아 공개하게 되는 경우도 있다.
class UnsafeStates {
private String[] states = new String[] {
"AK", "AL", .....
};
public String[] getStates() { return states; }
}
- private으로 선언된 변수가 getStates 메소드를 통해 외부에 공개될 수 있기 때문에,
유출 상태
에 놓여 있다.
@AntiPattern
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener (
new EventListener () {
public void onEvent (Event e) {
doSomething(e);
}
});
}
}
- this 클래스에 대한 참조를 외부로 공개하는 상황, AntiPattern!!
생성 메소드를 실행하는 도중에는 this 변수가 외부에 유출되지 않게 해야 한다.
- 생성 메소드에서 this 변수를 ㅇ출시키는 가장 흔한 오류는 생성 메소드에서 스레드를 새로 만들어 시작시키는 일이다.
public class SafeListener {
private final EventListener listener;
private SafeListener() {
listener = new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
};
}
public static SafeListener newInstance(EventSource source) {
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
- 생성 메소드에서 this 변수가 외부로 유출되지 않도록 팩토리 메소드를 사용하는 모습
출처 :브라이언 괴츠, 더그 리, 팀 피얼스, 조셉 보우비어, 데이빗 홈즈, 조슈아 블로쉬 공저, 『 멀티코어를 100% 활용하는 자바 병렬 프로그래밍』, 에이콘(2008.7.30), 3장 인용.