public class LazySingleton {
//싱글톤 객체를 담을 변수
private static LazySingleton myInstance;
//생성자를 private로 선언 => 생성자를 클래스 자체에서만 접근할 수 있어야 함
private LazySingleton (){}
public static LazySingleton getInstance(){
if(myInstance == null){
myInstance = new LazySingleton ();
}
return myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
LazySingleton singleton1 = LazySingleton .getInstance();
LazySingleton singleton2 = LazySingleton .getInstance();
LazySingleton singleton3 = LazySingleton .getInstance();
System.out.println(singleton1);//org.example.Singleton@7a81197d
System.out.println(singleton2);//org.example.Singleton@7a81197d
System.out.println(singleton3);//org.example.Singleton@7a81197d
System.out.println(singleton1==singleton2);//true
}
}
Lazy Initialization 방식은 필요 시 인스턴스를 생성하는 방식으로 자원 관리에 효율적이다.
Singleton 객체를 얻기 위해서는 getInstance() 메서드를 사용하여야 하며 해당 메서드를 사용하면 항상 같은 주소 값의 Singleton 객체를 사용할 수 있다.
Lazy Loading은 기본적으로 getInstance() 메서드 호출 시 클래스 변수로 저장된 인스턴스가 null인지 확인한 후, null일 경우 클래스 변수에 새로운 객체를 할당하는 방식이다. 이와 같은 방식은 Multi-Thread 환경일 때 문제가 발생될 수 있다.
Multi-Thread 환경일 때 두 개 이상의 thread가 getInstance() 메서드를 통해 Singleton 객체 생성을 동시에 시도한다. 이때 객체가 생성되기 전 해당 thread들이 if문에 접근하면서 두 개 이상의 객체가 생성될 가능성이 있다.
그렇기 때문에 Multi-Thread환경에서는 Singleton을 Thread-Safe 하게 구현하는 것이 중요하다.
public class EagerSingleton {
//static 초기화 시 인스턴스에 객체를 바로 할당
private static EagerSingleton myInstance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
EagerSingleton eagerSingleton1 = EagerSingleton.getInstance();
EagerSingleton eagerSingleton2 = EagerSingleton.getInstance();
EagerSingleton eagerSingleton3 = EagerSingleton.getInstance();
System.out.println(eagerSingleton1);//org.example.EagerSingleton@4517d9a3
System.out.println(eagerSingleton2);//org.example.EagerSingleton@4517d9a3
System.out.println(eagerSingleton3);//org.example.EagerSingleton@4517d9a3
}
}
Eager Initialization 방식은 Singleton 객체를 미리 생성해놓는 방식이다. 이 방식은 클래스가 로딩될 때 바로 객체를 생성하기 때문에 Thread-Safe한 방식으로 Singleton을 구성할 수 있다. 하지만 미리 메모리에 할당되기 때문에 객체를 자주 사용하지 않는다면 불필요하게 메모리를 차지할 수 있어 관리 측면에서 비효율적일 수 있다
public class SyncSingleton {
private static SyncSingleton myInstance;
private SyncSingleton() {}
public static synchronized SyncSingleton getInstance() {
if (myInstance == null) {
myInstance = new SyncSingleton();
}
return myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
SyncSingleton singleton1 = SyncSingleton.getInstance();
SyncSingleton singleton2 = SyncSingleton.getInstance();
SyncSingleton singleton3 = SyncSingleton.getInstance();
System.out.println(singleton1);//org.example.SyncSingleton@5ca881b5
System.out.println(singleton2);//org.example.SyncSingleton@5ca881b5
System.out.println(singleton3);//org.example.SyncSingleton@5ca881b5
}
}
Lazy Initialization 방식에서 가장 간단하게 thread-safe를 구현할 수 있는 방법은 Synchronized 키워드를 사용하는 것이다. 해당 키워드를 사용하면 여러 thread가 접근하여도 하나의 thread만 접근 가능하게 하며 해당 thread의 작업이 끝나기 전까지는 다른 thread가 접근할 수 없다. 하지만 이러한 방식을 사용하면 불필요한 lock으로 인해 성능 낭비가 크다.
public class DCLSingleton {
private static DCLSingleton myInstance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (myInstance == null) {
synchronized (DCLSingleton.class) {
if (myInstance == null) {
myInstance = new DCLSingleton();
}//// 1
}
}//// 2
return myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
DCLSingleton singleton1 = DCLSingleton.getInstance();
DCLSingleton singleton2 = DCLSingleton.getInstance();
DCLSingleton singleton3 = DCLSingleton.getInstance();
System.out.println(singleton1);//org.example.DCLSingleton@5ca881b5
System.out.println(singleton2);//org.example.DCLSingleton@5ca881b5
System.out.println(singleton3);//org.example.DCLSingleton@5ca881b5
}
}
DCL 방식은 Synchronized 를 ****getInstance() 메서드 전체에 적용하여 성능 저하를 야기한 부분에 대한 개선 방식이다
인스턴스 할당 시점에만 Synchronized 를 부분적으로 사용하여 인스턴스가 생성되어 null일 경우에만 lock을 시행한다. 하지만 1번과 2번 구문 사이에서 인스턴스가 아직 완전한 초기화가 되지 않았을 때 다른 스레드가 들어오면 문제가 발생할 수 있다
기본적으로 myInstance = new DCLSingleton(); 의 과정은 다음과 같다
컴파일러나 CPU는 과정을 재배치할 수 있으며 2번과 3번의 순서가 바뀔 수 있다.
생성자가 호출되기 전 메모리주소만 설정되었을 때 다른 스레드가 들어오게된다면 부분적으로 초기화된 잘못된 객체를 참조하게 될 수 있다.
public class VolatileSingleton {
private static volatile VolatileSingleton myInstance;
private VolatileSingleton() {}
public static VolatileSingleton getInstance() {
if (myInstance == null) {
synchronized (VolatileSingleton.class) {
if (myInstance == null) {
myInstance = new VolatileSingleton();
}
}
}
return myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
VolatileSingleton singleton1 = VolatileSingleton.getInstance();
VolatileSingleton singleton2 = VolatileSingleton.getInstance();
VolatileSingleton singleton3 = VolatileSingleton.getInstance();
System.out.println(singleton1);//org.example.VolatileSingleton@5ca881b5
System.out.println(singleton2);//org.example.VolatileSingleton@5ca881b5
System.out.println(singleton3);//org.example.VolatileSingleton@5ca881b5
}
}
volatile를 사용한 DCL 방식은 volatile 키워드를 사용함으로서 기본 DCL 방식의 문제점을 해결한다.
우선 재배치 과정을 수행하지 않도록 하여 올바른 순서로 초기화를 할 수 있도록 도와주며 캐시메모리가 아닌 메인메모리에서만 값을 참조하면서 멀티스레드 환경에서 변수가 변경되어도 항상 일관적이도록 유지하면서 부분적으로 초기화된 객체를 참조하는 문제를 방지한다. 하지만 volatile는 메인 메모리를 읽기 때문에 남발할 경우 성능 저하가 발생할 수 있다.
** volatile
일반적으로 멀티 스레드 환경에서는 여러 스레드가 동시에 변수에 접근할 수 있다.
CPU는 성능 향상을 위해 자주 참조되는 변수를 CPU 캐시에 저장한다. 이렇게 하면 한 스레드가 변수 값을 변경했을 때 다른 스레드는 변경된 값을 즉시 알지 못할 수 있어 변수 값이 서로 불일치할 수 있다 .
이는 변수 값의 일관성을 보장해야 하는 경우 문제가 된다.
volatile 키워드는 변수 값이 변경될 때 CPU 캐시가 아닌 메인 메모리로부터 값을 참조하도록 강제할 수 있다.
그렇기 때문에 volatile를 사용하면 모든 스레드는 항상 최신 변수 값을 메인 메모리에서 읽게 되어 변수의 일관성을 보장할 수 있다

public class LazyHolderSingleton {
private LazyHolderSingleton() {}
private static class LazyHolder {
private static final LazyHolderSingleton myInstance = new LazyHolderSingleton();
}
public static LazyHolderSingleton getInstance() {
return LazyHolder.myInstance;
}
}
public class SingletonTest {
public static void main(String[] args) {
LazyHolderSingleton singleton1 = LazyHolderSingleton.getInstance();
LazyHolderSingleton singleton2 = LazyHolderSingleton.getInstance();
LazyHolderSingleton singleton3 = LazyHolderSingleton.getInstance();
System.out.println(singleton1);//org.example.LazyHolderSingleton@24d46ca6
System.out.println(singleton2);//org.example.LazyHolderSingleton@24d46ca6
System.out.println(singleton3);//org.example.LazyHolderSingleton@24d46ca6
}
}
Lazy Holder 방식은 JVM에게 동기화 작업을 위임하는 것이기 때문에 INSTANCE가 이미 JVM 메모리에 올라가 있으면 JVM이 싱글톤 객체를 한번만 생성할 것을 보장해준다
public enum EnumSingleton {
myInstance;
}
public class SingletonTest {
public static void main(String[] args) {
EnumSingleton singleton1 = EnumSingleton.myInstance;
EnumSingleton singleton2 = EnumSingleton.myInstance;
System.out.println(singleton1.hashCode());//1554547125
System.out.println(singleton2.hashCode());//1554547125
}
}
enum 기본적으로 생성자가 private으로 선언되기 때문에 외부에서 객체를 생성할 수 없으므로 싱글톤을 항상 보장해준다. 하지만 final 클래스이기 때문에 상속이 불가능하여 확장에 제한이 있다는 단점이 있다.