락
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
BigInteger i = extractFromRequest(servletRequest);
if (i.equals(lastNumber.get())) {
encodeIntoResponse(servletResponse, lastFactors.get());
} else {
BigInteger[] factors = factor(i);
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(servletResponse, factors);
}
}
}
- AtomicReference를 사용한 단일 연산 참조 변수 각각은 스레드에 안전 하지만 UnsafeCachingFactorizer 자체는 틀린 결과를 낼 수 있는 경쟁 조건을 가
지고 있음.
- UnsafeCachingFactorizer에는 인수분해 결과를 곱한 값이 lastNumber에 캐시된 값가 같아야 된다는 불변 조건이 있으며 이 조건은 항상 성립해야 제대 로 동작 함.
- 여러개의 변수가 하나의 불변조건을 구성하고 있다면 이 변수들은 서로 독립적이지 않다.
암묵적인 락
- 자바에서는 단일 연산 특성을 보장하기 위해 synchronized 라는 구문으로 사용할 수 있는 락을 제공 함.
- 자바에 내장된 락을 암묵적인 락(intrinsic lock 혹은 monitor lock 이라 한다)
- 자바에서 암묵적인 락은 뮤텍스 (mutexes , mutual exclusion lock (상호배제 락))로 동작 한 번에 한 스레드만 특정 락을 소유
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
@Override
public synchronized void service(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception{
BigInteger i = extractFromRequest(servletRequest);
if (i.equals(lastNumber)) {
encodeIntoResponse(servletResponse, lastFactors);
} else {
BigInteger[] factors = factor(i);
lastNumber = i;
lastFactors = factors;
encodeIntoResponse(servletResponse, factors);
}
}
- 동기화 수단을 쓰면 아주 쉽게 인수분해 서블릿 스레드를 안전하게 고칠 수 있다.
- 위 예제는 service 메소드에 synchronized 키워드를 추가해 한번에 한 스레드만 실행 할 수 있도록 하였다.
- 하지만 이 방법은 너무 극단적이라 여러 클라이언트가 동시에 사용할 수 없고 이 때문에 응답성이 엄청 떨어질 수 있다.
public class CachedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits() {
return hits;
}
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
BigInteger i = extractFromRequest(servletRequest);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors;
}
}
encodeIntoResponse(servletResponse, lastFactors);
}
}
- 위 예제는 전체 메소드를 동기화 하는 대신 두 개의 짧은 코드 블록을 synchronized키워드로 보호했다
- 하나는 캐시된 결과를 갖고 있는지 검사하는 일종의 확인 후 동작(check-and-act)부분이고 하나는 캐시 된 입력
결과를 새로운 값으로 변경하는 부분 이다
- synchronized 블록 밖에 있는 코드는 다른 스레드와 공유되지 않는 지역 변수만 사용하기 때문에 동기화 할 필요 없다.
- 예전 처럼 AtomicLong 써도 괜찮지만 별다른 장점이 없다.
참고 : 자바 병렬 프로그래밍