- 메모리상의 공유된 변수를 여러 스레드에서 서로 사용할 수 있게 하려면 반드시 동기화 기능을 구현해야 한다.
- 동기화되지 않은 상황에서 메모리상의 변수를 대상으로 작성해둔 코드가 '반드시 의도한대로 동작할 것이다'라고 단정지을 수 없다.
- 여러 스레드에서 공동으로 사용하는 변수에는 항상 적절한 동기화 기법을 적용한다.
- 특정 변수에 값을 저장하거나 읽는 코드가 여러 스레드에서 앞서거니 뒤서거니 실행되면 이전에 저장해뒀던 값을 가져가지 못할 수 있음
- 예제 3.1 변수를 공유하지만 동기화되지 않은 예제
ready 변수의 값을 읽기 스레드에서 영영 읽지 못할 수 있어서 무한 반복에 빠질 수 있다.
자바 메모리 모델에서는 별다른 동기화 구조가 잡혀있지 않으면 컴파일러가 직접 코드 실행 순서를 조절하여 레지스터 캐시, 명령 실행 순서 재배치 등의 작업을 할 수 있다.
3.1.1 스테일 데이터
- stale : 신선하지 않은, (만든 지) 오래된, 퀴퀴한
- 한 스레드가 변수의 값을 변경하고, 다른 스레드가 해당 변수를 읽을 때 변경되기 이전의 값을 가지고 왔다면 스테일 데이터라고 한다.
- 어떤 변수에서건 스테일 현상이 발생하면 예기치 못한 예외 상황이 발생하기도 하고, 데이터를 관리하는 자료구조가 망가질 수도 있고, 계산된 결과 값이 올바르지 않을 수도 있고, 무한반복에 빠져들 수도 있다.
3.1.1 스테일 데이터
예제 3.3 동기화된 상태로 정수 값을 보관하는 클래스
@ThreadSafe
public class SynchronizedInteger {
@GuardBy("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 사용하지 않은 경우 메모리에 읽기 쓰기 연산 시에 두번 의 32비트 연산 사용을 허용해서 전혀 엉뚱한 값을 가져올 수 있다.
- 메모리
원자성
한 스레드에서 특정 메모리의 값을 변경하는 동안 다른 쓰레드가 같은 메모리의 값을 읽거나 변경할 경우, 그 결과가 어느 한 스레드의 동작으로 보장되어야 한다. 일부 비트만 변경되거나 두 스레드의 변경 명령결과가 섞여서 다른 값이 되지 않는다는 것이 보장되어야 한다.
3.1.3 락과 가시성
- 락은 상호 배제뿐만 아니라 정상적인 메모리 가시성을 확보하기 위해서도 사용한다.
- 변경 가능하면서 여러 스레드가 공유해 사용하는 변수를 각 스레드에서 각자 최신의 정상적인 값으로 활용하려면 동일한 락을 사용해 모두 동기화시켜야 한다.
3.1.4 volatile 변수
- volatile로 선언된 변수의 값을 바꿨을 때 다른 스레드에서 항상 최신 값을 읽어갈 수 있도록 해준다.
- 락을 사용하면 가시성과 연산의 단일성을 모두 보장받을 수 있다. 하지만 volatile 변수는 연산의 단일성은 보장하지 못하고 가시성만 보장한다.
- volatile
변수는 다음과 같은 상황에서만 사용하는 것이 좋다.
- 변수에 값을 저장하는 작업이 해당 변수의 현재 값과 관련이 없거나 해당 변수의 값을 변경하는 스레드가 하나만 존재
- 해당 변수가 객체의 불변조건을 이루는 다른 변수와 달리 불변조건에 관련되어 있지 않다.
- 해당 변수를 사용하는 동안 어떤 경우라도 락을 걸어 둘 필요가 없는 경우
3.2 공개와 유출
- 유출상태 - 의도적으로 공개시키지 않았지만 외부에서 사용할 수 있게 공개된 경우
- 현재 코드 범위 밖에서 코드 내부의 값에 접근가능할 경우 반드시 해당 객체를 동기화시켜야 한다.
- 객체를 공개했을 때 그 객체 내부의 private이 아닌 변수나 메소드를 통해 불러올 수 있는 모든 객체는 함께 공개된다.
- 객체 내부에서 사용하는 값이 적절하게 캡슐화되어 있다면 프로그램이 정상적으로 동작할 것이라고 쉽게 예측할 수 있고, 예상치 못한 상황에서 원래 설계했던 동작을 벗어나지 않도록 제한할 수 있다.
3.2.1 생성 메소드 안전성
- 생성 메소드를 실행하는 도중에는 this 변수가 외부에 유출되지 않게 해야한다.
- 생성 메소드에서 스레드를 생성하는 건 별 문제가 없는 일이지만, 시작시키는 건 문제의 소지가 많은 일이다. 스레드를 시작시키는 기능은 start나 initialize 등의 메소드로 만들어 사용하는 편이 좋다.
- 생성 메소드에서 이벤트 리스너를 등록하거나 새로운 스레드를 시작시키려면 생성 메소드를 private으로 지정하고 public으로 지정된 팩토리 메소드를 만들어 사용한다.
- 생성 메소드에서 오버라이드 가능한 메소드(private도 아니고 final도 아닌)를 호출하는 경우 this 참조가 외부에 유출될 가능성이 있다.
3.3 스레드 한정
- 특정 객체를 단일 스레드에서만 활용하도록 하면 해당 객체는 동기화할 필요가 없다.
- 스레드 한정기법은 프로그램 설계 과정부터 구현 과정까지 계속해서 적용해야 하며 스레드에 한정된 객체가 외부로 유출되지 않도록 항상 신경써야 한다.
3.3.1 스레드 한정 - 주먹구구식
- 스레드 한정 기법을
구현 단계에서 완전히 알아서 잘 처리해야 할 경우,
언어적인 방법으로 객체를 특정 스레드에 한정할 수 없기 때문에 임시방편을 사용할 수밖에 없다보니 오류가 발생할 가능성이 높다. - 특정 모듈의 기능을
단일 스레드로 동작하도록 구현한다면,
언어적인 지원없이 직접 구현한 스레드 한정 기법에서 나타날 수 있는 오류의 가능성을 최소화할 수 있다.
- volatile
변수를 공유해 사용할 때에는 특정 단일 스레드에서만
쓰기 작업을 하도록 제한하면,
읽기 작업이 가능한 다른 모든 스레드는 volatile 변수의 특성상 가장 최근에 업데이트된 값을 정확하게 읽어갈 수 있다. - 안전성을 완벽하게 보장할 수 있는 방법이 아니므로 꼭 필요한 곳에만 제한적으로 사용하고 가능하다면 스택 한정이나 ThreadLocal 클래스 등의 좀 더 안전한 스레드 한정기법을 사용한다.
3.3.1 스레드 한정 - 주먹구구식
- ThreadLocal
3.3.2 스택 한정
- 특정 객체를 로컬 변수를 통해서만 사용할 수 있는 경우의 한정 기법.
- 스레드 내부의
스택은 외부 스레드에서 볼 수 없고, 로컬변수는 현재 실행 중인 스레드 내부의 스택에만 존재하기 때문에 로컬변수는 모두 암묵적으로 현재 실행 중인 스레드에 한정되어 있다고 볼
수 있다.
- 기본 변수형을 사용하는 로컬 변수는 언어적으로 스택한정 상태가 보장된다.
- 객체형 변수가 스택 한정 상태를 유지할 수 있게 하려면 해당 객체에 대한 참조가 외부로 유출되지 않도록 해야 한다.
- 스레드에 안전하지 않은 객체라 해도 특정 스레드 내부에서만 사용한다면 동기화 문제가 없기 때문에 안전하다. 단, 유지보수 시 주의가 필요하다.
3.4 불변성
- 불변객체 - 맨 처음 생성되는 시점을 제외하고는 그 값이 전혀 바뀌지 않는 객체
- 연산의 단일성이나 가시성에 대한 거의 모든 문제는 여러 개의 스레드가 예측할 수 없는 방향으로 변경 가능한 값을 동시에 사용하려 하기 때문에 발생한다.
- 불변 객체는 언제라도 스레드에 안전하다.
- 불변 객체
조건
- 생성되고 난 이후에는 객체의 상태를 변경할 수 없다.
- 내부의 모든 변수는 fianl로 설정돼야 한다.
- 적절한 방법으로 생성되야 한다(this 변수에 대한 참조가 외부로 유출되지 않아야 한다)
3.4.1 final 변수
- final을 지정한 변수의 값은 변경할 수 없다. (단, 변수가 가리키는 객체가 불변객체가 아니라라면 해당 객체에 들어있는 값은 변경할 수 있다.)
- 초기화 안정성을 보장하므로 별다른 동기화 없이 불변객체를 자유롭게 사용하고 공유할 수 있다.
3.4.2 예제: 불변 객체를 공개할 때 volatile 키워드 사용
- 불변 객체애 volatile 키워드를 적용해 시간적으로 가시성을 확보하면 따로 락을 사용하지 않았다 해도 스레드에 안전하다.
3.5 안전 공개
- 객체를 여러 스레드에서 공유하도록 해야 할 경우, 반드시 안전한 방법을 사용해야 한다.
예제 3.14 동기화 하지 않고 객체를 외부에 공개.
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
3.5.1 적절하지 않은 공개 방법: 정상적인 객체도 문제를 일으킨다
- 객체를 올바르지
않게 공개할 경우 발생하는 문제점
- 스테일 상태
- 예제 3.15 올바르게 공개하지 않으면 문제가 생길 수 있는 객체
3.5.2 불변 객체와 초기화 안전성
- 자바 메모리 모델에는 불변 객체를 공유하고자 할 때 초기화 작업을 안전하게 처리할 수 있는 방법이 만들어져 있다.
- 안전하게 초기화
과정을 진행하려면 다음의 불변 객체 요구조건을 만족시켜야 함.
1) 상태를 변경할 수 없어야 함
2) 모든 필드의 값이 final로 선언돼야 함
3) 적절한 방법으로 생성해야 함
- 불변 객체는 별다른 동기화 방법을 적용하지 않았다 해도 어느 스레드에서건 마음껏 안전하게 사용할 수 있다. 불변 객체를 공개하는 부분에 동기화 처리를 하지 않았다 해도 아무런 문제가 없다.
3.5.3 안전한 공개 방법의 특성
- 불변 객체가 아닌 객체는 모두 올바른 방법으로 안전하게 공개해야 하며, 대부분은 공개하는 스레드와 불러다 사용하는 스레드 양쪽 모두에 동기화 방법을 적용해야 한다.
- 객체에 대한 참조와 객체 내부의 상태를 외부 스레드에 안전하게 공개하려면
- 객체에 대한 참조를 static 메소드에 초기화한다.
static 초기화 방법은 JVM에서 클래스를 초기화하는 시점에 작업이 모두 진행된다.
JVM 내부에서 동기화가 맞춰져 있기 때문에 객체를 안전하게 공개할 수 있다.
- 객체에 대한 참조를 volatile 변수 또는 AtomicReference 클래스에 보관한다.
- 객체에 대한 참조를 올바르게 생성된 클래스 내부의 final 변수에 보관한다.
- 락을 사용해 올바르게 막혀 있는 변수에 대한 참조를 보관한다.
ex. Vector, synchronizedList 같은 동기화된 스레드 안전한 컬렉션에 객체를 보관
3.5.4 결과적으로 불변인 객체
- 기술적으로 불변 객체가 아니어도, 한번 공개된 이후 내용이 변경되지 않으면 결과적으로 불변인 객체라고 볼 수 있다.
- 프로그램 내부에서 해당 객체를 공개한 이후에는 불변 객체인 것처럼 사용한다.
- 결과적인 불변 객체는 개발 과정도 간편하고 동기화 작업을 할 필요가 없기 때문에 프로그램의 성능을 개선하는데 도움이 된다.
3.5.5 가변 객체
- 가변객체 - 객체를 생성한 이후 그 내용이 변경될 수 있는 객체
- 가변객체를 사용할 때에는 공개하는 부분과 객체를 사용하는 모든 부분에서 동기화 코드를 작성해야 한다.
- 가변성에 따른
객체공개
- 불변객체는 어떤 방법으로 공개해도 문제가 없다.
- 결과적으로 불변인 객체는 안전하게 공개해야 한다.
- 가변객체는 안전하게 공개해야 하고, 동기화와 락을 사용해 스레드 안전성을 확보한다.
3.5.6 객체를 안전하게 공유하기
- 객체를 사용할
때
- 객체를 사용하기 전 동기화 코드를 적용해 락을 확보해야 하는가?
- 객체 내부의 값을 변경해도 되는가? 아니면 읽기만 해야 하는가? - 객체를 공개할
때
- 객체를 어떤 방법으로 사용할 수 있는가? - 멀티 스레드를
사용하는 병렬프로그램에서의 객체공유 원칙
- 스레드 한정
- 읽기 전용객체 공유 : 불변객체, 결과적으로 불변인 객체
- 스레드에 안전한 객체를 공유
- 동기화 방법 적용
'개발 > 병렬 프로그래밍' 카테고리의 다른 글
자바 병렬 프로그래밍 - 6장 작업실행 (0) | 2016.08.16 |
---|---|
자바 병렬 프로그래밍 - 5장 구성 단위 (0) | 2016.08.12 |
자바 병렬 프로그래밍 - 4장 객체 구성 (0) | 2016.07.31 |
자바 병렬 프로그래밍 - 2장 스레드 안정성 (0) | 2016.07.14 |
자바 병렬 프로그래밍 - 1장 개요 (0) | 2016.07.14 |