3장: 객체 공유
가시성
reordering(재배치) 현상
특정 메서드의 소스 코드가 코딩된 순서로 반드시 수행됨을 보장하지 않아 발생한다.
여러 스레드 간에서 사용하는 변수를 적절히 동기화해주지 않는 경우, 컴파일러가 직접 코드 실행 순서를 조절하면서 레지스터에 데이터를 캐시하거나, CPU가 명령 실행 순서를 재배치하고 프로세서 내부 캐시에 데이터를 보관할 수 있다.
stale data
공유 변수를 사용하는 모든 경우에 대해 동기화시켜두지 않는다면 최신 값이 아닌 유효하지 않은 옛날 값을 읽을 가능성이 존재한다.
이러한 경우 시스템 자원을 불필요하게 계속 점유하는 등 예기치 못한 예외가 언제든지 발생할 수 있다.
예를 들어 특정 스레드에서 set 메서드로 값을 새로 지정했지만, 다른 스레드에서 get 메서드를 호출해도 새로운 값이 조회되지 않을 수 있다는 것이다.
여러 스레드가 공유하는 64비트 숫자형 타입 변수에 volatile 키워드를 붙이지 않는 경우에도 잘못된 값을 읽을 수 있다.
volatile로 지정되지 않은 long, double 타입에 대해서는 메모리로부터 읽거나 쓸 때 두 번의 32비트 연산으로 나누어 수행할 수 있기 때문에, 이전 값과 새로운 값에서 각각 32비트를 읽어올 가능성이 존재하게 된다. 이는 과거 32비트 프로세서를 지원하기 위함이었다.
락은 상호 배제(mutual exclusion) 뿐만 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.
항상 synchronized 문 내부에서 사용하도록 동기화된 변수의 경우 스테일 상태에 빠지지 않는다.
volatile
volatile로 선언된 변수의 값을 바꾸면 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 한다.
컴파일러와 런타임 모두 해당 변수가 여러 스레드에 공유되어 사용되므로 실행 순서가 재배치되면 안된다고 간주한다.
프로세서의 레지스터에 캐싱되지 않고 프로세서 외부의 캐시에도 저장되지 않게 된다.
락과 동기화 기능은 제공되지 않는다.
volatile 변수만 사용해 메모리 가시성을 확보한 코드는 synchronized로 직접 동기화한 것보다 읽기 어려우므로, 너무 의존하면 안된다.
연산의 단일성은 보장하지 않는다. 따라서 증가 연산자(
++
) 등을 사용한 부분에 대해서는 동기화되지 않는다.다음 상황에 사용하는 것을 권장한다.
변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련 없거나, 값을 변경하는 스레드가 하나만 존재할 때
변수가 객체의 불변 조건에 관련되어 있지 않을 때
변수를 사용하는 동안 락을 걸 필요가 없을 때
공개와 유출
유출(escaped)이란 의도적으로 공개시키지 않았지만 외부에서 사용 가능하게 공개된 경우를 의미한다.
객체가 유출되는 상황은 다양하게 존재한다.
public static
변수로 객체를 설정할 때다음 코드에서 Set<Secret> 자체와 각 Secret이 모두 여러 스레드와 클래스에서 사용할 수 있도록 공개된다.
public static Set<Secret> knownSecrets;
public static
변수 내부에 참조를 갖도록 객체를 설정할 때private이 아닌 메서드를 호출해 결과를 받아올 때
다음과 같이 메서드의 결과를 통해 객체가 공개될 수 있다.
public String[] getStates() { return states; }
내부 클래스의 인스턴스를 외부에 공개할 때
내부 클래스는 항상 부모 클래스에 대한 참조를 갖고 있으므로, 공개된 내부 클래스 객체를 포함하는 외부 클래스 객체도 함께 공개된다.
생성 시점의 안전성
생성자를 통해 객체를 생성하는 도중에 해당 객체를 외부에 유출하면 정상적이지 않은 상태의 객체를 외부에서 사용할 수 있으므로 절대 유출되지 않도록 해야 한다.
생성 메서드에서 스레드를 새로 만들어 시작하는 경우, 생성 메서드의 클래스와 새로운 스레드가 this 변수를 공유할 수 있다. 단순히 스레드를 생성하는 측면에서는 문제가 없지만, 스레드를 시작하게 되면 this에 정의되어 있는 자원들을 사용 가능한 상태이므로 스레드 시작시키는 기능은 별도 메서드에 두는 것이 좋다.
생성 메서드에서 오버라이드 가능한 메서드를 호출하는 경우 this 참조가 외부에 유출될 수 있다.
스레드 한정
객체 인스턴스를 특정 스레드에 한정시켜두면 스레드 안전성을 확보할 수 있다.
한 번에 여러 스레드가 특정 객체를 사용하지 못하도록 아예 막아버리는 것이다.
volatile 변수의 경우 특정 단일 스레드만 쓰기 작업을 할 수 있도록 하고, 나머지는 읽기 작업만 할 수 있도록 제한해야 한다.
스택 한정 기법
특정 객체를 로컬 변수를 통해서만 사용할 수 있도록 한다.
로컬 변수는 현재 실행중인 스레드의 스택에만 존재하므로 외부 스레드에서 볼 수 없다.
ThreadLocal
변경 가능한 싱글톤 혹은 전역 변수를 기반으로 설계되어 있는 구조에서 변수가 임의로 공유되는 상황을 막을 수 있다.
전역 변수처럼 동작하기 때문에 프로그램 구조 상 전역 변수를 남발할 수 있고, 메서드 인자로 값을 넘기지 않고 ThreadLocal을 통해 전달받기 때문에 프로그램 유지보수가 어려울 수 있다.
불변성
처음 생성된 후에 값이 전혀 변하지 않는 불변 객체는 스레드 안전성을 확보할 수 있으면서 단순하게 관리할 수 있다.
객체 상태가 변경되는 경우를 대비하지 않고도 외부에 공개, 공유해도 된다.
객체 내부의 변수가 final이더라도 참조로 연결되어 있는 객체인 경우, 해당 참조 객체까지 불변이어야 완벽한 불변 객체이다.
참조 객체를 불변으로 만들기 위해 변수를
private final
로 설정하고 외부에서 변경할 수 없도록 메서드를 적절하게 구성해야 한다.나중에 변경할 일이 없다고 판단되는 변수는
final
로 선언하면 초기화 안전성을 통해 별도 동기화 작업 없이 안전하게 사용할 수 있다.불변 객체에
volatile
키워드를 적용한 필드를 사용하면 특정 스레드가 불변 객체를 필드에 대입할 때 다른 스레드가 가시성을 확보할 수 있다. 따라서 별도의 락이 필요 없다.
안전 공개
가변 객체 또는 결과적으로 불변인 객체의 경우 안전하게 공개하는 방식을 사용해야 한다.
변수를 동기화하지 않은 채
public
으로 공개하면 스레드 안전성이 깨지게 된다.
초기화 시 안전성 확보
불변 객체를 여러 스레드 간에 공유하고자 할 때 초기화 작업을 안전하게 처리하는 방법이 있다.
안전하게 초기화 작업을 진행하려면 객체의 상태를 변경할 수 없어야 하고, 모든 필드가 final으로 선언되어야 하며, 적절한 방법으로 생성되어야 한다.
final로 선언한 변수에 변경 가능한 객체가 할당되어 있다면 해당 변수를 사용하는 모든 부분을 동기화시켜주어야 한다.
안전한 공개 방식
객체를 공개하는 스레드와 객체를 사용하는 스레드 모두에 동기화 방법을 적용해야 한다.
생성 메서드가 실행된 후 객체를 안전하게 공개하는 방법
객체에 대한 참조를 static 메서드에서 초기화한다.
객체에 대한 참조를 volatile 변수 혹은 AtomicReference 클래스에 보관한다.
객체에 대한 참조를 클래스의 final 변수에 보관한다.
변수에 대한 락을 건 후 해당 변수에 참조를 보관한다.
synchronizedList 메서드 혹은 Vector 객체 등이 이 방식으로 동작한다.
객체 자체는 변경 가능하지만 외부에 공개된 후에는 변경 불가능하게 되는 경우, 불변 객체로 간주하고 처리하면 된다.
객체를 가져다 쓸 때 해당 객체를 어느 정도까지 사용해도 되는지 알고 있어야 한다. 객체 사용 전 동기화 작업이 필요한지, 내부의 값을 바꿔도 되는지 등을 알고 있어야 한다. 객체를 외부에 공개할 때에도 마찬가지로 이러한 정보를 알려야 한다.
Last updated