본문 바로가기
학습/Java

[java] 멀티스레드 환경과 싱글톤 객체

by KKambi 2020. 7. 15.

해당 포스트는 Jungwoon님의 자바 멀티스레드 환경에서 싱글톤 패턴을 이해하며 작성한 글입니다.

결론은 LazyHolder 패턴으로 싱글톤을 구현하자! 입니다.

 

 

스프링은 빈이라는 싱글톤(유일한 하나의 인스턴스) 객체를 IOC 컨테이너에 생성하여 사용한다.

 

스프링의 빈은 일반적으로

1. 상태를 가지고 있지 않게 설계 (@Controller, @Service, @Repository, ...)

2. 어플리케이션 구동 시 ApplicationContext라는 Static Sharing Pool에 싱글톤 인스턴스 생성

2가지 방법으로 멀티스레드에서의 빈 동기화를 신경쓰지 않아도 된다.

 

하지만 싱글톤 객체는 사실 구현 방법에 따라 Thread-Safe 할 수도, 하지 않을 수도 있다.

 

 

 

구현1 - 정적 팩토리 메소드에서 인스턴스 생성 (Not Thread-Safe)

class Singleton {
    private static Singleton instance = null;
    
    public static Singleton getInstance() {
    	if (instance == null) {
        	instance = new Singleton();
        }
        
        return instance;
    }
    
    private Singleton() {}
}

private으로 생성자를 숨기고, getInstance()라는 정적 팩토리 메소드로만 인스턴스를 반환받을 수 있다.

 

싱글스레드 환경에선 문제가 되지 않는다.

하지만 멀티스레드 환경에선 다수 스레드가 메소드에 진입하여 경합을 벌이는 과정에서 서로 다른 2개의 인스턴스가 생성될 수 있다.

 

 

 

구현2 - 클래스 초기화 시 인스턴스 생성 (Thread-Safe, 스프링의 방법)

class Singleton {
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance() {
        return instance;
    }
    
    private Singleton() {}
}

private으로 생성자를 숨기고, 클래스 초기화 시 인스턴스를 생성하여 static 멤버 변수에 할당한다.

getInstance()는 싱글톤 인스턴스를 반환하기만 하므로 팩토리 메소드라 칭하긴 어렵다.

 

스프링에선 Lazy-Intialization을 사용하지 않는 기본적인 경우, 어플리케이션을 구동하면서

ApplicationContext 내에 해당 방법과 같이 인스턴스를 생성하게 된다.

 

다만 미사용 인스턴스임에도 생성되어 불필요한 시스템 리소스를 낭비할 수도 있다는 단점이 존재한다.

 

 

 

구현3 - 정적 팩토리 메소드에서 인스턴스 생성 + 메소드 동기화 (Thread-Safe, 성능저하)

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

메소드 동기화로 Thread-Safe하지만 성능이 매우 떨어진다.

 

 

 

구현4 - LazyHolder (Thread-Safe, 강력추천)

class Singleton {

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
    
    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    private Singleton() {}
}

제일 좋은 방법이다. 해당 방법으로 구현하자.

 

JVM이 클래스 정보를 로딩할 때 Singleton 클래스의 정보를 가져오지만

LazyHolder는 inner static class로, INSTANCE 멤버 변수는 LazyHolder.INSTANCE를 처음 참조할 때 초기화된다.

 

따라서 특정 스레드가 Singleton 객체의 getInstance() 메소드를 호출하는 순간

LazyHolder 클래스의 static final 멤버 변수가 초기화되고 (싱글톤 인스턴스 생성)

이후 getInstance()를 호출하는 스레드들은 해당 인스턴스를 반환받게 된다.

 

구현1처럼 메소드를 호출하며 경합하게 되지 않을까? 하는 의문이 들 수 있다.

하지만 클래스를 로딩하고 초기화하는 시점은 Thread-Safe를 보장하기 때문에 synchronized와 같은 키워드가 없어도 동기화가 보장된다고 한다.

'학습 > Java' 카테고리의 다른 글

[java] 가비지 컬렉션  (0) 2020.07.05
[java] Stack & Heap과 Reference의 개념  (0) 2020.06.28
[java] 익명 클래스와 람다식  (0) 2020.06.10
[java] Proxy Pattern  (0) 2020.03.23
[java] Annotation Processor란? (추가공부 필요)  (0) 2020.03.21

댓글