본문 바로가기

IT/java

Multi-Threaded 환경에서 Singleton의 동시성 문제

1. 클래스의 초기화 시점

클래스는 어느 시점에서든 먼저 초기화가 되어야 사용할 수 있다. 클래서그 최초로 초기화되는 시점은 다음과 같다.
 * T가 클래스이며  T의 인스턴스가 생성될 때
 * T가 클래스이며 T에서 선언한 static 메소드가 호출되었을 때
 * T에서 선언한 static 필드에 값이 할당 되었을 때
 * T에서 선언한 static 필드가 사용되었을 때 ( 단, static 필드가 상수가 아닐 경우)
 * T가 최상위클래스이고, T안에 위치한 assert 구문이 실행되었을 때

2. Singleton pattern

singleton pattern은 인스턴스를 하나만 생성해서 그 객체를 공유해서 사용하는 패턴이다.

* private 멤버 변수로 자신의 class의 인스턴스를 가진다.
* private 생성자를 지정하여 외부에서 instance를 생성하지 못하게한다.
* getInstance() 메소드를 통해 객체를 static하게 가져올 수 있다.

2.1

public class Singleton1 {
    private static Singleton1 single = new Singleton1();
    public static Singleton1 getInstance(){
        return single;
    }
    private Singleton1(){
    }
}


* 클래스 로드시 new가 실행된다. (class를 사용하지 않을 경우에도 instance를 생성함)
* 항상 1개의 인스턴스를 가진다.

2.2

public class Singleton2 {
    private static Singleton2 single;
    public static synchronized Singleton2 getInstance(){
        if (single == null) {
            single = new Singleton2();
        }
        return single;
    }
    private Singleton2(){
    }
}


* getInstance()가 호출될 때 인스턴스가 생성 ( 클래스 로드시에는 생성되지 않음)
* synchronized사용으로 인해 성능 저하

2.3 Initialization on demand holder idiom

 클래스의 초기화 시점에서 살펴본 초기화 되는 경우의 예에서 "T에서 선언한 static 필드가 사용되었을 때(단, static 필드가 상수가 아닐 경우)"가  Initialization on demand holder idiom의 내용의 핵심이 된다.

public class Singleton4 {
    private Singleton4(){
    }
    private static class SingletonHolder{
        static final Singleton4 single = new Singleton4();
    }
    public static Singleton4 getInstatnce(){
        return SingletonHolder.single;
    }
}

 
* 내부 클래스를 사용하는 방법
* 내부 클래스가 호출되는 시점에 최초 생성 : 클래스 로드시에 생성되지 않음, 속도 빠름

 2.4 Double-Checked Locking

public class Singleton3 {
    private volatile static Singleton3 single;
    public static Singleton3 getInstance(){
        if (single == null) {
            synchronized(Singleton3.class) {
                if (single == null) {
                    single = new Singleton3();
                }
            }
        }
        return single;
    }
    private Singleton3(){
    }
}


 * if(single == null) : 매번 synchronized 블럭 안으로 들어가는 것을 방지
* volatile : 변수의 원자성 보장
 
3. Lazy Initialization

 
싱글턴 패턴은 태생적으로 지연 초기화(Lazy Initialization)의 개념을 내포한다. 앞의 내용에서 초점을 맞추고 있진 않지만, 이제까지의 내용 역시 지연 초기화의 구현이라는 시각에서 싱글턴을 논의되었다.

지연 초기화란 클래스의 초기화를 해당 클래스를 실제로 사용하는 시점까지 늦추는 것이다. 이를 통해 초기화 비용이 큰 클래스들이 클래스 로드 시점에 한꺼번에 초기화 되며 발생하는 복잡도를 줄일 수 있으며, 다른 클래스들과 분리하여 뒤늦게 독립적으로 초기화함으로써 동시성이나 의존성 문제를 해결하는 데 사용할 수도 있다.


싱글턴 패턴(앞서 살펴본 내용을 포함한)의 경우도 getInstance()를 호출하는 시점에 해당 클래스가 기존에 생성되었는지 유무를 판단하여 객체를 뒤늦게 생성하는 지연 초기화의 개념이 도입되어 있다.(2.2~2.4)