스레드 세이프하게 싱글톤을 만들어봅시다.
싱글톤
은 인스턴스를 오직 하나만 생성할 수 있는 클래스 입니다.
하나의 인스턴스
만 생성글로벌 접근
키보드 리더, 프린터 스풀러, 환경 세팅 등 클래스의 인스턴스를 오직 한개만 만들어야 하는 경우 사용합니다.
또한, 스프링은 기본적으로 container에 bean을 등록할때 싱글톤으로 등록
합니다.
@Controller, @Service, @Repository
등은 여러개의 인스턴스를 가질 필요는 없기 때문에 기본적으로 딱 한개의 인스턴스만 생성되고 컨테이너에 등록됩니다.
(참고로, 싱글톤으로 등록하지 않을 수도 있습니다.)
디자인 패턴은 크게 1. 생성, 2. 구조, 3. 행위로 구분되는데 싱글톤은 객체의 생성에 대한 패턴 입니다.
멀티 스레드 환경에서 동시에 인스턴스 생성을 시도하면, 인스턴스가 여러개 만들어질 수 있습니다.
Settings는 싱글톤 클래스로 null 값을 검사후 인스턴스를 단 한개만 만들도록 설계되었지만, 여러개의 스레드가 동시에 인스턴스 생성을 시도하면 여러개가 생성됩니다.
// Settings.java 싱글톤 클래스
public class Settings {
private static Settings instance;
private Settings() {
System.out.println("새로운 인스턴스가 생성되었습니다.");
}
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
// Main.java
import java.util.concurrent.*;
class Main {
public static void main(String args[]) {
// 1. 스레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 2. 반복문을 통해 - 여러개의 스레드가 거의 동시에 인스턴스를 생성
for (int i = 0; i < 10; i++) {
service.submit(() -> {
Settings.getInstance();
});
}
// 3. 종료
service.shutdown();
}
}
실행해보면... 인스턴스가 8개 생성되었습니다.
어... 난 싱글아니야
예시의 생성방법이며, 멀티 스레드 환경에서 안전하지 않은 방법5입니다.
public class Settings {
// 1. 클래스 변수로 인스턴스 지정
private static Settings instance;
// 2. private 생성자 - new로 인스턴스 생성불가
private Settings() {}
// 3. 클래스 메서드를 통해 인스턴스 생성
public static Settings getInstance() {
// 주의 - 여러개의 스레드가 동시에 if문을 통과하는 경우 여러개의 인스턴스 생성 가능
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
new를 통해 인스턴스 생성 불가
클래스 메서드, 인스턴스를 생성하지 않고 메서드를 사용하기 위함
멀티스레드 환경에서 2개 이상의 스레드가 if 문을 통과하는 경우 여러 인스턴스 생성
// 멀티스레드 환경에서 2개 이상의 스레드가 if 문을 통과하는 경우 여러 인스턴스 생성
if (instance == null) {
instance = new Settings();
}
첫번째 생성 방법의 getInstance 메서드에 synchronized
키워드를 붙인 것입니다.
synchronized
는 동기화 블록으로 지정된 코드 블록은 한번에 하나의 스레드만 접근이 가능합니다.
다만, 멀티 스레드 환경에서 동시에 여러 스레드가 접근하지 못하고 순차적으로 접근해야하기 때문에 느립니다.
public class Settings {
// 1. 클래스 변수로 인스턴스 지정
private static Settings instance;
// 2. private 생성자 - new로 인스턴스 생성불가
private Settings() {}
// 3. 클래스 메서드를 통해 인스턴스 생성
public static synchronized Settings getInstance() {
// 주의 - 여러개의 스레드가 동시에 if문을 통과하는 경우 여러개의 인스턴스 생성 가능
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
synchronized
를 통해 스레드가 순차적으로 접근, 느림
synchronized
클래스가 로드되는 시점에 인스턴스를 생성하는 방법. 원하는 시점에 인스턴스를 생성하지 못합니다.
public class Settings {
// 1. 클래스가 로드되는 시점에 인스턴스를 생성
private static final Settings INSTANCE = new Settings();
// 2. private 생성자
private Settings() {}
// 3. 클래스 변수를 통한 인스턴스 획득
public static Settings getInstance() {
return INSTANCE;
}
}
클래스 로딩 시점에 인스턴스를 미리 만들어 놓는다.
클래스 로딩 및 초기화
클래스 로딩으로 스레드 세이프를 보장하는 방법은 1. eager initialization, 2. LazyHolder, 3. eunm 에 사용됩니다.
멀티 스레드 환경에서 여러개의 스레드가 동시에 클래스를 로딩하려고 오직 한개의 클래스만 로딩됩니다.
다음 코드는 10개의 스레드가 동시에 클래스 로딩을 시도합니다.
다만, 클래스 로딩은 한번만 수행되고, 클래스가 로딩되었을 때 초기화를 수행합니다.
이 의미는 멀티 스레드 환경에서 클래스 로딩은 스레드 세이프함을 의미합니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Main {
public static void main(String args[]) {
// 1. 스레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
service.submit(() -> {
new Single();
});
}
// 3. 종료
service.shutdown();
}
}
class Single {
static {
System.out.println("static 블록 호출");
}
public Single() {
System.out.println("생성자 호출");
}
}
javac Main.java
java -verbose:class Main
synchronized
생성방식에 변화를 준 것으로 synchronized
를 메서드 호출에 두지 않고, 인스턴스 검사에 두었습니다.
volatile
키워드는 변수를 메인 메모리에 저장하겠다는 의미로, 스레드에는 CPU 캐시가 있는데, read, write 할때 CPU 캐시를 사용하여 스레드마다 다른 값을 가지는 것이 아닌 메인 메모리의 값을 사용합니다.
public class Settings {
// 1. 변수를 메인 메모리에 저장
private static volatile Settings instance;
// 2. private 생성자
private Settings() {}
// 3. getInstance 메서드 호출될때는 lock 걸리지 않음
public static Settings getInstance() {
if (instance == null) {
synchronized (Settings.class) {
if (instance == null) {
instance = new Settings();
}
}
}
return instance;
}
}
메서드 호출할때는 락을 걸지 않기 때문에 synchronized 방법보다 조금 빠름
장점
단점
synchronized
클래스를 로딩하고 초기화하는 과정이 스레드 세이프함을 이용하는 방법입니다.
내부의 클래스는 static 임에도 불구하고 LazyLoading 지연로딩이라고 부릅니다. 자세한 내용은 5) 왜 LazyLoading인가에서 알아봅시다.
public class Settings {
// 1. private 생성자
private Settings() {}
// 2. static inner 클래스
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
// 3. static 메서드
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
원하는 시점에 인스턴스 생성
클래스 로딩 및 초기화
왜 LazyHolder인지 몰라서, stackoverflow에 여쭤봤는데 잘 알려주신다.
질문
위키 피디아 - Initialization-on-demand holder
오라클 - 초기화
정리하면 다음과 같습니다. 자세한 과정 및 코드
클래스로더가 .class 파일을 찾고 메모리에 올려놓는 것을 의미합니다.
JVM은 모든 클래스를 메모리에 로딩하지 않습니다. 필요한 클래스가 있을때 메모리에 올려놓습니다.
외부 클래스 로딩 - 내부 클래스 로딩 X
내부 클래스 로딩 - 외부 클래스 로딩 X
-> static 클래스, static 메서드와는 관계 없음
클래스가 로딩될때 초기화도 수행
클래스 로딩 및 초기화 여러 스레드가 동시에 시도해도, 오직 한번만 수행 - 즉, 스레드세이프함
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Main {
public static void main(String args[]) {
// 1. 스레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
service.submit(() -> {
new Single();
});
}
// 3. 종료
service.shutdown();
}
}
class Single {
static {
System.out.println("static 블록 호출");
}
public Single() {
System.out.println("생성자 호출");
}
}
10개의 스레드가 동시에 클래스를 로딩해도 1개의 클래스만 로딩되고, 초기화는 딱 한번만 이루어집니다.
외부 클래스가 로딩되었을때 인스턴스를 생성하지 않고
getInstance 정적 메소드가 호출될때 내부의 클래스를 로딩하고 초기화하는 과정에서 인스턴스를 생성합니다.
클래스를 로딩하고 초기화하는 과정은 여러 스레드가 동시에 시도해도 오직 한번만 수행되기 때문에 스레드세이프합니다.
이를 통해 인스턴스가 오직 한개만 생성됨을 보장합니다.
enum은 클래스가 로드되는 시점에 딱 한번 생성되고 전역에서 접근할 수 있습니다.
enum은 존재 자체로 싱글턴의 2가지 목적을 달성합니다.
하나의 인스턴스
만 생성글로벌 접근
또한 임으로 인스턴스를 생성할 수 없기 때문에 스레드 세이프 또한 만족합니다.
(참고로, enum의 생성자는 기본적으로 private이며, public으로 변경하면 컴파일 에러가 발생합니다.)
public enum Settings {
INSTANCE;
}
enum은 리플렉션에서 접근할 수 없도록 막혀있다.
enum 기본적으로 serializable을 구현하고 있다.
클래스 로딩 및 초기화
10개의 스레드가 동시에 클래스 로딩을 시도해도, 오직 한개의 클래스만 로딩됩니다. 이를 통해 스레드 세이프를 보장받으며
인스턴스는 한개만 생성됩니다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Main {
public static void main(String args[]) {
// 1. 스레드 풀 생성
ExecutorService service = Executors.newCachedThreadPool();
// 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
service.submit(() -> {
System.out.println(SingleTon.INSTANCE);
});
}
// 3. 종료
service.shutdown();
}
}
enum SingleTon {
INSTANCE;
}
리플렉션
은 클래스 타입을 알지 못해도 해당 클래스의 생성자, 메소드, 필드에 접근 및 사용
할 수 있는 Java API 입니다.
리플렉션을 사용해서 클래스의 생성자를 가져오고, 생성자 접근 수준을 public으로 변경 new Settings
를 실행한 것과 유사한 결과를 얻습니다.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Main {
public static void main(String args[]) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
// 1. 싱글톤 클래스의 인스턴스 생성
Settings settings = Settings.getInstance();
// 2. 리플랙션
// 1) settings 클래스의 생성자 가져옴
Constructor<Settings> constructor = Settings.class.getDeclaredConstructor();
// 2) 생성자 접근 수준을 public으로 변경
constructor.setAccessible(true);
// 3) 생성자를 통한 인스턴스 생성 new Settgings()와 유사
Settings settings1 = constructor.newInstance();
// 3. 주소값 비교 - false
System.out.println(settings == settings1);
}
}
// static inner
public class Settings {
// 1. private 생성자
private Settings() {}
// 2. static inner 클래스
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
// 3. static 메서드
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
private 생성자가 실행되려고 할때 예외를 던집니다.
public class Settings {
// 1. private 생성자
private Settings() {
throw new RuntimeException("싱글턴 인스턴스를 리플렉션으로 생성할 수 없습니다.");
}
// 2. static inner 클래스
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
// 3. static 메서드
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
또는 enum을 사용합니다.
리플렉션으로 새로운 인스턴스가 생성되는 이유는 생성자에 접근할 수 있기 때문인데, enum은 리플렉션으로 생성자에 접근 할 수 없습니다.
역직렬화할때 반드시 생성자를 통해 객체를 생성합니다. 이를 통해 새로운 인스턴스가 생성됩니다.
import java.io.*;
class Main {
public static void main(String args[]) throws IOException, ClassNotFoundException {
// 1. 싱글톤 클래스의 인스턴스 생성
Settings settings = Settings.getInstance();
Settings settings1 = null;
// 2. 직렬화 - 객체를 파일로 작성 - 파일명: settings.obj
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(settings);
}
// 3. 역직렬화 - 파일을 객체로 변환
// 역질렬화할때는 반드시 생성자를 사용해서 인스턴스 생성
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
settings1 = (Settings) in.readObject();
}
System.out.println(settings == settings1);
}
}
import java.io.Serializable;
public class Settings implements Serializable {
// 1. private 생성자
private Settings() {}
// 2. static inner 클래스
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
// 3. static 메서드
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
역직렬화할때 readResolve
가 수행됨을 이용하여 인스턴스를 반환하면 직렬화, 역직렬화로 인스턴스가 여러개 생기는 현상은 막을 수 있습니다.
import java.io.Serializable;
public class Settings implements Serializable {
// 1. private 생성자
private Settings() {}
// 2. static inner 클래스
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
// 3. static 메서드
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
// 역직렬화할때 반드시 readResolve 메서드 수행
protected Object readResolve() {
// 인스턴스 반환하도록 작업
return getInstance();
}
}
1 enum
2 Lazy Holder, static inner
백기선 - 코딩으로 학습하는 GoF의 디자인 패턴
이펙티브 자바 - 아이템 3 private 생성자나 열거 타입으로 싱글턴임을 보증하라
http://literatejava.com/jvm/fastest-threadsafe-singleton-jvm/