singleInstanceHolder라는 내부클래스를 하나 더 만듬으로써, Singleton클래스가 최초에 로딩되더라도 함께 초기화가 되지 않고. getInstance()가 호출될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스를 생성하게 된다.
5.java
class Singleton {
private static class singleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singleInstanceHolder.INSTANCE;
}
}
static 정적 멤버나 정적 블록이던지간에
로직상에서 모듈들이 singleton instance를 호출하지 않아도 무조건 초기에 자원을 할당해야하는 문제점이 있었는데, 모듈들이 필요로 할때만 정적 멤버로 선언한다.
이중 확인 잠금(DCL, Double Checked Locking)도 있다.
인스턴스 생성 여부를 싱글톤 패턴 잠금 전에 한번. 객체를 생성하기 전에 한 번 2번 체크하면 인스턴스가 존재하지 않을 때만 잠금을 걸 수 있기 때문에 앞서 생겼던 문제점을 해결할 수 있습니다.
6.java
public class Singleton {
private volatile Singleton instance;
private Singleton() {}
public Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
여기서 instance라는 변수에 volatile 키워드를 건 것을 볼 수 있다.
메모리 구조는 다음과 같은데, 메인 메모리 위에 CPU 캐시메모리라고 불리는 L3, L2, L1 캐시가 있다. (L4도 드물긴 하지만 L4까지 CPU 캐시 메모리라고 부릅니다.)
JAVA에서는 스레드 2개가 열리면 변수를 메인 메모리(RAM)으로부터 가져오는 것이 아니라 밑의 구조처럼 캐시메모리에서 각각의 캐시메모리를 기반으로 가져오게 된다.
volatile.java
public class Test {
boolean flag = true;
public void test() {
new Thread(() - > {
int cnt = 0;
while (flag) {
cnt++;
}
System.out.println("Thread1 finished\n");
}).start();
new Thread(() - > {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
System.out.println("flag to false");flag = false;
}).start();
}
public static void main(String[] args) {
new Test().test();
}
}
위의 빨간색 사각형을 thread1 이라고 할 때, flag가 true라면 무한루프가 발생한다. thread2에선 flag를 false로 만든다.
그런데 여기에서 위의 그림처럼 flag=true일때 위에서 아래로 실행되므로thread2에서 flag를 false로 만들면 thread1에서 중지가 될 것이라고 생각할 수 있지만
flag라는 변수는 사실 각각의 캐시메모리로 같은 변수를 바라보고 있기 때문에 변수가 공유되지 않는다.
(이렇게 무한 루프가 걸린다)
그런데 여기서 volatile 키워드를 추가하게 되면 Main Memory를 기반으로 저장하고 읽어오기 때문에 변수가 공유되고, 이 문제를 해결할 수 있습니다.
volatile2.java
public class Test {
volatile boolean flag = true;
public void test() {
new Thread(() - > {
int cnt = 0;
while (flag) {
cnt++;
}
System.out.println("Thread1 finished\n");
}).start();
new Thread(() - > {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
System.out.println("flag to false");flag = false;
}).start();
}
public static void main(String[] args) {
new Test().test();
}
}
enum의 인스턴스는 기본적으로 스레드세이프(thread safe)한 점이 보장되기 때문에 이를 통해 생성할 수 있습니다.
7.java
public enum SingletonEnum { INSTANCE;
public void oortCloud() { }
}
그래서 최고의 방법은 무엇일까?
5번은 가장 많이 쓰인다고 알려져있고 7번은 이펙티브 자바를 쓴 조슈아 블로크가 추천한 방법이기에 5번과 7번이 최고의 방법이다.
A single-element enum type is the best way to implement a singleton