public class DoubleCheckedLocking {
private static ThreadRaceTarget target;
public ThreadRaceTarget getTarget() {
if (target == null) {
target = new ThreadRaceTarget();
}
return target;
}
}
single thread의 경우에는 괜찮지만 이 경우에는 멀티스레드 환경에서는 정상적으로 동작하지 않는다.
만약에 두개의 thread에서 getTarget()을 호출한다면, 의도한 바와는 다르게 생성자는 두번 호출될것이다.
따라서 이런 경우에는 의도한 바대로 정상동작하게끔 하기 위해서 lock을 잡아야 하는데,
synchronized를 사용해보자.
public class DoubleCheckedLocking {
private ThreadRaceTarget target;
public synchronized ThreadRaceTarget getTarget() {
if (target == null) {
target = new ThreadRaceTarget();
}
return target;
}
}
이 코드는 의도한대로 정상 동작한다.
예를 들어서 두개의 thread에서 하나의 DoubleCheckedLocking instance에 접근해서 getTarget()을 호출한다면,
synchronized가 인스턴스 단위로 걸려있기 때문에 락이 걸려있어 동시성 방어가 된다.
하지만 성능은 어떨까 ?
모든 thread에서 락을 얻고 release하는 과정이 반복되게 된다면 이는 성능을 저하시킬 수도 있다.
왜냐하면 락을 거는 이유는 getTarget()이 호출되었을때, 초기화가 되지 않은 경우가 중복 수행되는 방지하기 위해서이다.
만약 초기화가 완료된 시점에서는 락을 거는건 더이상의 효용이 없다.
그저 멤버 변수를 바로 가져오면 끝날일이다.
따라서 다음과 같은 방법으로 최적화가 가능하다.
public class DoubleCheckedLocking {
private ThreadRaceTarget target;
public ThreadRaceTarget getTarget() {
if (target == null) {
synchronized (this) {
if (target == null) {
target = new ThreadRaceTarget();
}
}
}
return target;
}
}
이는 앞의 방법들과는 성능적으로 최적화 되어 있다.
예를 들어서 100개의 thread가 race하는 상황을 가정해보면,
이전 구현에서는 100번 락을 얻고, 락을 해제한다.
새로운 구현에서는 1번만 락을 얻고, 락을 해제한다.
이 패턴을 사용할때 주의하지 않으면 데이터 경합이 발생한다.
따라서 다음과 같은 방법으로 사용해야 한다.
public class DoubleCheckedLocking {
private volatile ThreadRaceTarget target;
public ThreadRaceTarget getTarget() {
ThreadRaceTarget temp = target;
if (temp == null) {
synchronized (this) {
temp = target;
if (temp == null) {
target = temp = new ThreadRaceTarget();
}
}
}
return temp;
}
}
volatile은 memory barrier를 제공하기 때문에 위의 초기화가 완료되고 나서 target에 접근한다.
여기서 local 변수인 temp는 불필요해 보이지만, 이를 도입함으로써 인해 volatile 변수인 target은 getTarget() 내부에서 한번만 access된다.
이 자체가 성능을 약 40% 정도 향상시킨다.