싱글톤 인스턴스

YaaaPyoung·2022년 4월 12일
0

1. 싱글톤을 구현하는 방법

1. Eager Initialization

  • 클래스가 JVM으로 로딩될 때 최초 한번 생성하는 방식이다.
public class EagerSingleton {
    private static EagerSingleton instance = new EagerSingleton();
    private EagerSingleton(){}
    private static getInstance(return instance);
}
  • 이 방법은 싱글톤 객체를 생성하는 리소스 비용이 적을 때 사용하는 것이 바람직하다. 만약, 어플리케이션에서 해당 객체를 사용하지 않는데도 불구하고 무조건 싱글톤 객체를 생성하기 때문이다.
  • 그리고 이러한 방식은 객체를 생성할 때 발생하는 예외를 핸들링할 수 없다는 단점이 있다.
  • 만약, 더 이상 싱글톤 방식이 아닌 방식으로 동작하게 하려면 getInstance() 메소드만을 수정하면 된다.
public class EagerSingleton {
	public static final EagerSingleton instance = new EagerSingleton();
}
  • public static final로 선언하게되면 선언부만 보고서도 싱글톤 객체임을 확실히 알 수 있게된다.
public class EagerSingleton {
    private static EagerSingleton instance;
    static {
        try{
            instance = new EagerSingleton();
        }catch{Exception e}{
            //예외 처리
        }    	 
    }
    private EagerSingleton(){}
    private static getInstance(return instance);
}
  • static 블록에서 객체를 생성하고 예외를 처리할 수 있도록 위와 같이 변경할 수 있다.

2. Lazy Initialization

  • 클래스가 로딩될 때 객체를 생성하는 것이 아니라 해당 객체를 필요로할 때 생성하는 방식이다.
public class LazySingleton {
    private static LazySingleton instance;
    private LazySingleton(){}
    private static synchronized LazySingleton getInstance(){
        if(instance == null){ instance = new LazySingleton(); }
        return instance;
    }
}
  • 추가적으로 스레드간 동기화를 위해 메소드 레벨에서 synchronized 키워드를 사용했으나 메소드 단위로 lock이 잡혀 좋지 않은 성능을 보인다.

2. Double-Checked Locking

  • 메소드 레벨에서 synchronized 키워드를 사용하는 것이 아니라 실제 객체가 null인 경우에만 동기화를 할 수 있도록 임계 영역의 범위를 좁힌 것이다.
  • 이 방식을 사용할 땐 volatile 키워드를 사용하는 것이 필수적이다.
public class DclSingleton {
    
    private static volatile DclSingleton instance;
    
    public static DclSingleton getInstance() {
        
        if (instance == null) {
            synchronized (DclSingleton .class) {
                if (instance == null) { // <----- volatile과 연관있음
                    instance = new DclSingleton();
                }
            }
        }
        return instance;
    }
    // private constructor and other methods...
}

1) volatile 키워드 참고

  • volatile 키워드는 변수가 메인 메모리에 바로 저장될 수 있도록 하고 컴파일러에 의해 reordering을 방지하도록하는 키워드이다.
  • 서로 다른 CPU 코어에서 실행되고 있는 thread1thread2가 하나의 공유 변수에 접근할 때 스레드들은 메인 메모리에서 값을 읽어와서 CPU 레지스터에 해당 값을 캐싱한다. thread1이 해당 변수의 값을 1증가시켜도 thread2는 증가된 값을 갖고 있는 것이 아니라 그 전에 CPU 레지스터에 저장된 값을 참조하고 있다. 이처럼 스레드가 변경한 값이 메인 메모리에 저장되지 않아 다른 스레드가 이 값을 볼 수 없는 문제를 visibility (가시성) 문제라고한다.
  • volatile 키워드를 사용할 경우, 변수에 대한 읽기/쓰기 작업은 메인 메모리로부터 참조하여 수행하게 된다.

2) volatile 키워드와 Double-Checked Locking

  • 아래 코드에서 어느 부분에 때문에 volatile 선언이 필수적인지 확인해보자.
public static Singleton getInstance(){
1)	if (instance == null) { 
2)		synchronized (DclSingleton .class) {
3)			if (instance == null) {
4)				instance = new DclSingleton();
5)			}
6)		}
7) }
8) return instance;
}
  • 최초에 1번 라인 에서 thread1과 thread2가 동시에 메인 메모리에서 instance 값을 읽어 객체가 생성되지 않은 것을 확인하고 if문 안으로 진입한다.

  • 2번 라인에서 thread1이 synchronized 블록 으로 진입하여 3번 라인에서 비교문을 수행하고 4번 라인에서 객체를 생성한 다음 lock을 해제하고 메소드를 빠져나온다.

  • 2번 라인에서 thread2가 synchronized 블록 으로 진입하고 3번 라인에서 비교문을 수행할 때, 메인 메모리에서 thread1이 할당한 instance 변수의 값을 읽어오고 비교문을 수행한다. 이때 instance 변수는 더 이상 null이 아니므로 객체를 생성하지 않고 메소드를 빠져나온다.

    • volatile 키워드를 사용하지 않았다면 thread2가 3번 라인에서 비교문을 수행할 때, 1번 라인에서 읽어온 null 값으로 비교를 했을 것이다.
    • 이처럼 각 스레드마다 캐시된 값을 바라보기때문에 thread1이 객체를 생성했음에도 불구하고 thread2도 객체를 생성하는 일이 발생한다.
  • instance 변수를 volatile 키워드로 선언하지 않았을 경우로 가정하고 살펴보자.

  • thread1이 동기화 블록에 진입하여 4)에서 인스턴스를 생성하는 과정을 보면 다음과 같다.

    1. Heap 영역에 메모리 할당 i.e 0xffffff
    2. 참조변수(instance)에 메모리 주소 값(0xffffff) 저장
    3. 메모리 초기화 DclSingleton 객체를 초기화한다.
  • 그 다음, thread2가 1) 에서 instance == null 비교를 했을 때 초기화는 되지 않았지만 메모리를 할당한 상태, 즉 생성자 호출이 완전히 끝나지 않은 상태일 수 있다.

  • 이때 thread2 입장에서는 instance 변수가 null이 아니므로 바로 instance 객체를 반환하지만 정상적인 객체라고 볼 수 없는 상황이 있을 수 있다.

  • JVM 1.5 부터는 어느 한 스레드에서 volatile 필드가 참조하는 객체를 생성할 경우, 그 객체를 가리키는 volatile 필드를 읽는 모든 스레드들은 객체 생성이 끝날 때 까지 기다리게 된다.

3. LazyHolder

  • inner static class를 사용하는 방법으로 volatile 이나 synchronized 키워드를 사용하지 않고서도 동시성 문제를 해결할 수 있다.
public class Singleton{
    
    private Singleton(){}
    
    static class LazyHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private static Singleton getInstance() { return LazyHolder.INSTANCE; }
}
  • inner static classSingleton 클래스가 로딩되어도 로드되지 않다가 getInstance() 메소드가 호출되었을 때 JVM으로 호출되며 이 과정에서 초기화가 한 번 이뤄진다.

4. Enum

  • 원소가 하나뿐인 열거타입으로 싱글톤을 만드는 방법이 가장 좋다.
public enum Singleton {
    INSTANCE;
    public void method(){...};
}
  • 열거 타입 자체는 하나의 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 변수로 공개한다.
  • 열거 타입밖에서는 new 연산자를 통해 열거 타입 인스턴스를 생성할 수 없으므로 딱 하나씩 존재함을 보장한다.

[참고]

DCL

volatile

싱글톤

0개의 댓글