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