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 블록
에서 객체를 생성하고 예외를 처리할 수 있도록 위와 같이 변경할 수 있다.public class LazySingleton {
private static LazySingleton instance;
private LazySingleton(){}
private static synchronized LazySingleton getInstance(){
if(instance == null){ instance = new LazySingleton(); }
return instance;
}
}
synchronized
키워드를 사용했으나 메소드 단위로 lock
이 잡혀 좋지 않은 성능을 보인다.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...
}
volatile
키워드는 변수가 메인 메모리
에 바로 저장될 수 있도록 하고 컴파일러에 의해 reordering
을 방지하도록하는 키워드이다.thread1
과 thread2
가 하나의 공유 변수에 접근할 때 스레드들은 메인 메모리에서 값을 읽어와서 CPU 레지스터에 해당 값을 캐싱
한다. thread1
이 해당 변수의 값을 1증가시켜도 thread2
는 증가된 값을 갖고 있는 것이 아니라 그 전에 CPU 레지스터에 저장된 값을 참조하고 있다. 이처럼 스레드가 변경한 값이 메인 메모리에 저장되지 않아 다른 스레드가 이 값을 볼 수 없는 문제를 visibility (가시성)
문제라고한다.volatile
키워드를 사용할 경우, 변수에 대한 읽기/쓰기 작업은 메인 메모리
로부터 참조하여 수행하게 된다.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 값으로 비교를 했을 것이다.instance 변수를 volatile
키워드로 선언하지 않았을 경우로 가정하고 살펴보자.
thread1
이 동기화 블록에 진입하여 4)에서 인스턴스를 생성하는 과정을 보면 다음과 같다.
DclSingleton 객체
를 초기화한다.그 다음, thread2
가 1) 에서 instance == null
비교를 했을 때 초기화는 되지 않았지만 메모리를 할당한 상태, 즉 생성자 호출이 완전히 끝나지 않은 상태일 수 있다.
이때 thread2
입장에서는 instance 변수가 null이 아니므로 바로 instance 객체를 반환하지만 정상적인 객체라고 볼 수 없는 상황이 있을 수 있다.
JVM 1.5
부터는 어느 한 스레드에서 volatile
필드가 참조하는 객체를 생성할 경우, 그 객체를 가리키는 volatile
필드를 읽는 모든 스레드들은 객체 생성이 끝날 때 까지 기다리게 된다.
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 class
는 Singleton 클래스
가 로딩되어도 로드되지 않다가 getInstance() 메소드가 호출되었을 때 JVM으로 호출되며 이 과정에서 초기화가 한 번 이뤄진다.public enum Singleton {
INSTANCE;
public void method(){...};
}
public static final
변수로 공개한다.new
연산자를 통해 열거 타입 인스턴스를 생성할 수 없으므로 딱 하나씩 존재함을 보장한다.