싱글톤 구현 프로젝트
멀티 스레드 환경에서 싱글톤 패턴을 사용하며 일어날 수 있는 동기화 문제를 다루기 위한 프로젝트.
2024.07
SynchronizedvolatileDCLenum
package org.example.test;
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private volatile static ChocolateBoiler cb;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if (cb == null) {
synchronized ((ChocolateBoiler.class)) {
if (cb == null) {
cb = new ChocolateBoiler();
}
}
}
return cb;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
package org.example.test;
public enum ChocolateBoilerEnum {
INSTANCE;
private boolean empty;
private boolean boiled;
ChocolateBoilerEnum() {
empty = true;
boiled = false;
}
public void fill() {
if (isEmpty()) {
empty = false;
boiled = false;
}
}
public void drain() {
if (!isEmpty() && isBoiled()) {
empty = true;
}
}
public void boil() {
if (!isEmpty() && !isBoiled()) {
boiled = true;
}
}
public boolean isEmpty() {
return empty;
}
public boolean isBoiled() {
return boiled;
}
}
synchronized
싱글톤 패턴에서는 스레드 간 하나의 객체 공유해야 함.
객체 필요할 때마다 synchronized 키워드로 코드 블록에 락 걸어서 객체 유일성 보장.
- 장점: 원자성 및 가시성 보장으로 여러 스레드가 동시에 객체 생성 못함.(싱글톤 구현)
- 단점: 성능 저하 발생 가능(생성 시점 이외는 필요 없음). 특히 동기화된 코드 블록이 자주 호출되거나 오래 실행될 때 더 두드러짐.
2. volatile
객체 생성 후 스레드 간 공유 데이터 문제 해결하는 방법.
자바의 스레드는 각자 캐시 저장소 보유.
필요할 때마다 캐시에서 값 읽어옴.
싱글톤에서는 각 스레드가 공유 자원 값을 정확히 알아야 해서 volatile로 주 메모리 값을 직접 읽도록 함.
- 장점: 가시성 문제 해결. volatile 키워드는 변수 값을 주 메모리에 즉시 기록하고, 주 메모리에서 직접 읽도록 보장.
- 단점: 원자성 보장 안 됨. volatile은 변수의 읽기/쓰기 연산이 원자적임을 보장하지만, 복합 연산(예: count++)은 원자성 보장 못함.
DCL (Double-Checked Locking)
객체 생성 로직에서 동기화 전후로 객체 생성 체크.
- 장점: 필요할 때만 동기화 수행하므로 synchronized 블록보다 성능 더 좋음. 가시성 문제 해결.
- 단점: 구현이 다소 복잡할 수 있고, 잘못 사용하면 예상치 못한 동기화 문제 발생 가능.
enum
enum을 사용해 싱글톤 구현.
(자바의 enum은 클래스 로딩 시점에 단 한 번만 인스턴스화됨)
-> 별도 동기화 코드 필요 없음.
- 장점:
- 자동 스레드 안전성
- 자바의 enum은 클래스 로딩 시점에 단 한 번만 인스턴스화됨. 별도 동기화 코드 필요 없음.
- 직렬화 안전성
- enum 사용하면 직렬화와 역직렬화 과정에서도 싱글톤 특성 유지됨. 별도 readResolve 메서드 구현할 필요 없음.
- 자동 스레드 안전성
- 단점: enum 타입 아닌 기존 클래스의 경우 enum으로 전환하기 어려울 수 있음.
상황에 적합한 방법 사용.
간단한 상태 플래그나 단일 변수 변경은 volatile로 충분할 수 있지만, 복잡한 동기화 필요한 경우 synchronized 사용이 더 적절함.
DCL은 성능과 동기화 균형 맞출 때 유용.
enum은 가장 간단하고 안전한 싱글톤 구현 방법 중 하나.
자바 프로그램의 멀티스레딩 동작을 정의하는 메모리 모델.
-> 여러 스레드가 어떻게 변수에 접근하고 읽고 쓰는지 규정.
JMM이 없으면 멀티스레드 프로그램의 동작을 예측하기 어려움.
- 가시성: 한 스레드에서 변경한 변수의 값이 다른 스레드에 언제 보이는지 정의.
- 원자성: 연산이 더 이상 쪼개지지 않는 단위. 중간에 끼어들기 불가능함. 예:
count++연산은 원자적이지 않음. - 재정렬: 컴파일러와 CPU가 성능 최적화를 위해 명령어 순서를 재배치하는 것. (JMM은 재정렬로 인한 문제 해결) -> 공부 필요.
volatile 키워드는 변수의 가시성을 보장함. volatile 변수는 다음과 같은 특징이 있음:
- 가시성: 한 스레드가
volatile변수의 값을 변경하면, 그 변경 사항이 즉시 다른 스레드에게 보임. 즉,volatile변수의 값은 주 메모리에서 직접 읽고 씀. - 재정렬 방지:
volatile변수에 접근하는 코드의 순서는 재정렬되지 않음. 즉,volatile변수 앞뒤의 명령어 순서가 바뀌지 않음.
- 원자성 미보장:
volatile은 가시성만 보장하고 원자성을 보장하지 않음. 예를 들어,volatile int count에 대해count++는 여전히 원자적이지 않음. - 복합 연산에 적합하지 않음:
volatile은 단순한 읽기/쓰기 연산에는 적합하지만, 복합 연산에는 적합하지 않음. 복합 연산은synchronized를 사용해야 함.
public class VolatileExample {
private volatile boolean flag = true;
public void writer() {
flag = false; // 다른 스레드에서 이 변경 사항을 즉시 볼 수 있음
}
public void reader() {
if (!flag) {
}
}
}
위 코드에서 flag는 volatile로 선언되어 있어서
한 스레드가 flag를 변경하면 다른 스레드가 즉시 그 변경 감지.
JMM과 volatile을 이해하면 멀티스레드 프로그램의 동작을 더 잘 예측하고 동기화 문제를 다룰 수 있음.