[자바 병렬 프로그래밍 - 2장 스레드 안정성] 요약 정리
- 스레드 안정한 코드를 작성하는 것은 근본적으로는 상태, 특히 공유되고 변경할 수 있는 상태에 대한 접근을 관리하는 것
- 스레드 안전성은 코드를 보호하는 것이 아닌, 데이터에 제어 없이 동시에 접근하는 것을 막는 것
- 객체를 스레드에 안전하게 만들려면 동기화를 통해 변경할 수 있는 상태에 접근하는 과정을 조율해야함
- 스레드가 하나 이상 상태 변수에 접근하고 그 중 하나라도 변수에 값을 쓰면, 해당 변수에 접근할 때, 관련된 모든 스레드를 동기화를 통해 조율해야함
- 기본 수단은 synchronized 키워드로서 배타적인 락을 통해 보호 기능을 제공함
- 만약 여러 스레드가 변경할 수 있는 하나의 상태 변수를 적절한 동기화 없이 접근하면 그 프로그램은 잘못된 것임
이를 고치는 방법은 3가지가 있음
1. 해당 상태 변수를 스레드 간에 공유하지 않거나
2. 해당 상태 변수를 변경할 수 없도록 만들거나
3. 해당 상태 변수를 접근할 땐 언제나 동기화를 사용함
- 스레드 안전한 클래스를 설계할 땐, 바람직한 객체 지향 기법이 왕도
- 캡슐화와 불변 객체를 잘 활용하고, 불변 조건을 명확하게 기술해야 함
- 때론 추상화와 캡슐화 기법이 성능과 배치되기도 함
- 항상 코드를 올바르게 작성하는 일이 먼저고, 그 다음 필요한 만큼 성능을 개선해야 함
2.1 스레드 안전성이란
- 여러 스레드가 클래스에 접근할 때, 실행 환경이 해당 스레드들의 실행을 어떻게 스케줄하든 어디에 끼워 넣든, 호출하는 쪽에서 추가적인 동기화나 다른 조율 없이도 정확하게 동작하면 해당 클래스는 스레드 안전하다고 함
(여러 스레드가 클래스에 접근해도 계속 클래스 명세에 부합하게 동작하면 스레드 안전하다고 할 수 있다)
- 스레드 안전한 클래스는 클라이언트 쪽에서 별도로 동기화할 필요가 없도록 동기화 기능도 캡슐화한다
- 예제 2.1 상태없는 서블릿
- 클래스 변수말고 지역변수에만 상태가 저장되면 스레드간 상태 공유가 되지 않음
- 상태 없는 객체에 접근하는 스레드가 어떤 일을 하든 다른 스레드가 수행하는 동작에 영향을 끼칠 수 없음
=> 상태 없는 객체는 항상 스레드 안전하다
2.2 단일 연산
- 예제 2.2 동기화 구문 없이 요청 횟수를 세는 서블릿
@NotThreadSafe
Public class UnsafeCountingFactorizer implements Servlet {
private long count = 0;
public long getCount() {return count;}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++ count; // 포인트
encodeIntoResponse(resp, factors);
}
}
스레드에 안전하지 않다
++count 라는 한줄 코드도 단일 연산(나눌 수 없는 최소 단위의 작업)이 아니다
2.2.1 경쟁 조건
- 경쟁 조건은 상대적인 시점이나 JVM이 여러 스레드를 교차해서 실행하는 상황에 따라 계산의 정확성이 달라질 때 나타남
(타이밍이 딱 맞았을 때만 정답을 얻는 경우)
- 대부분의 경쟁 조건은 관찰 결과의 무효화 때문에 발생
잠재적으로 유효하지 않는 관찰 결과로 결정을 내리거나 계산하는 경우
파일 X 존재 여부 확인 후 -> 미존재 시 파일 X 생성
=> 관찰과 행동 사이에 파일이 생성되었을 수 있다
2.2.2 예제: 늦은 초기화 시 경쟁 조건
- 예제 2.3 늦은 초기화에서 발생한 경쟁 조건
@NotThreadSafe
Public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null) {
instance = new ExpensiveObject();
}
return instance;
}
}
두 스레드가 각각 다른 인스턴스를 참조할 수 있다
2.2.3 복합 동작
- 경쟁조건을 피하려면 변수가 수정되는 동안 다른 스레드가 해당 변수를 사용하지 못하도록 막을 방법이 있어야 함
- 이런 방법으로 보호해 두면 특정 스레드에서 변수를 수정할때 다른 스레드는 수정 도중이 아닌 수정 이전이나 이후에만 상태를 읽거나 변경을 가할 수 있음
- 점검 후 행동과 읽고 수정하고 쓰기 같은 일련의 동작을 복합 동작이라고 함, 즉, 스레드에 안전하기 위해서는 전체가 단일 연산으로 실행되야 하는 일련의 동작을 지칭함
- 예제 2.3 AtomicLong 객체를 이용해 요청 횟수를 세는 서블릿
@ThreadSafe
Public class CountingFactorizer implements Servlet {
private final AtomicLong count = new AtomicLong(0);
public long getCount() {return count.get();}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
count.incrementAndGet();
encodeIntoResponse(resp, factors);
}
}
가능하면 AtomicLong처럼 스레드에 안전하게 이미 만들어져 있는 객체를 사용하는 것이 좋음
가능한 상태의 변화를 파악하는 편이 훨씬 쉽고 스레드 안전성을 더 쉽게 유지, 검증할 수 있다
2.3 락
- 예제2.5 단일 연산을 적절히 사용하지 못한 상태에서 결과 값을 캐시하려는 서블릿
- UnsafeCachingFactorizer에는 인수분해 결과를 곱한 값이 lastNumber에 캐시된 값과 같아야 한다는 불변조건이 있으며, 이와 같은 불변 조건이 항상 성립해야 서블릿이 제대로 동작한다고 볼 수 있음
- 여러 개의 변수가 하나의 불변조건을 구성하고 있다면 이 변수들은 서로 독립적이지 않음
- 상태를 일관성 있게 유지하려면 관련 있는 변수들을 하나의 단일 연산으로 갱신해야 한다
2.3.1 암묵적인 락
- 자바에는 단일 연산 특성을 보장하기 위해 synchronized 라는 구문으로 사용할 수 있는 락을 제공함
- 모든 자바객체는 락으로 사용할 수 있음, 이와 같이 자바에 내장된 락을 암묵적인 락 혹은 모니터락이라고 함
- 락은 스레드가 synchronized 블록에 들어가기 전에 자동 확보되며 정상적이든 비정상적이든 예외가 발생해서든 해당 블록을 벗어날 때, 자동으로 해제된다
- 특정 락으로 보호된 코드 블록은 한 번에 한 스레드만 실행할 수 있음
2.3.2 재진입성
- 스레드가 다른 스레드가 가진 락을 요청하면 해당 스레드는 대기 상태에 들어감
- 암묵적인 락은 재진입 가능하기 때문에 특정 스레드가 자기가 이미 획득한 락을 다시 확보할 수 있음
- 예제2.7 암묵적인 락이 재진입 가능하지 않았다면 데드락에 빠졌을 코드
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
System.out.println(toString() + ": calling doSomething");
super.doSomething();
}
}
재진입 가능한 락이 없으면 하위 클래스에서 synchronized 메소드를 재정의 하고 상위 클래스의 메소드를 호출하는 지극히 자연스러워 보이는 코드도 데드락에 빠질 것임
(참고로 synchronized 메소드를 오버라이드할 경우 synchronized 속성도 가져가는 것은 아니다.)
2.4 락으로 상태 보호하기
- 여러 스레드에서 접근할 수 있고 변경 가능한 모든 변수를 대상으로 해당 변수에 접근할 때는 항상 동일한 락을 먼저 확보한 상태여야 함
- 이 경우 해당 변수는 확보된 락에 의해 보호된다고 말함
- 모든 변경할 수 있는 공유 변수는 정확하게 단 하나의 락으로 보호해야 함
- 유지 보수하는 사람이 알 수 있게 어느 락으로 보호하고 있는지를 명확하게 표시하라
- 여러 변수에 대한 불변조건이 있으면 해당 변수들은 모두 같은 락으로 보호해야 함
- 모든 메소드를 동기화하면 SynchronizedFactorizer처럼 활동성이나 성능에 문제가 생길 수 도 있음
2.5 활동성과 성능
- 인수분해 서블릿의 구조를 CachedFactorizer처럼 고쳐 단순성(전체 메소드를 동기화)과 병렬 처리 능력(최대한 짧은 부분만 동기화) 사이에 균형을 맞춤
- CachedFactorizer은 상태 변수에 접근할 때와 복합 동작을 수행하는 동안 락을 잡지만, 오래 걸릴 가능성이 있는 인수분해 작업을 시작하기 전에 락을 놓음
- 종종 단순성과 성능이 서로 상충할 때가 있음
- 동기화 정책을 구현할 때는 성능을 위해 조급하게 단순성(잠재적 안전성을 훼손하면서)을 희생하고픈 유혹을 버려야 함
- 복잡하고 오래 걸리는 계산 작업, 네트워크 작업, 사용자 입출력 작업과 같이 빨리 끝나지 않을 수 있는 작업을 하는 부분에서는 가능한 한 락을 잡지 말 것
'개발 > 병렬 프로그래밍' 카테고리의 다른 글
자바 병렬 프로그래밍 - 6장 작업실행 (0) | 2016.08.16 |
---|---|
자바 병렬 프로그래밍 - 5장 구성 단위 (0) | 2016.08.12 |
자바 병렬 프로그래밍 - 4장 객체 구성 (0) | 2016.07.31 |
자바 병렬 프로그래밍 - 3장 객체 공유 (0) | 2016.07.31 |
자바 병렬 프로그래밍 - 1장 개요 (0) | 2016.07.14 |