싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 사로 만들고 있다면 만들어진 인스턴스를 반환함
그러나 이 코드는 메서드의 원자성이 결여되어 있어 멀티스레드 환경에서는 싱글톤 인스턴스를 2개 이상 만들 수 있음
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
인스턴스 변수의 은닉 : private static Singleton instance
는 Singleton 클래스의 인스턴스를 저장하는 변수 instance
가 클래스 외부에서 직접 접근되지 않도록 함. 이는 인스턴스가 오직 getInstance()
메서드를 통해서만 접근될 수 있도록 보장하여, 인스턴스의 생성과 관리를 클래스 내부에서만 제어할 수 있게 함
생성자의 은닉 : private Singleton(){}
Singleton 클래스의 생성자를 private로 선언하여, 클래스 외부에서 new
키워드를 이용한 객체 생성을 방지함. 이는 모든 객체 생성 요청이 getInstance()
메서드를 통해 이루어지도록 강제하여, 이 메서드에서 인스턴스 생성의 유일성을 보장함
public class YunhaSync {
private static String yunha = "오르트구름";
public static void main(String[] args) {
YunhaSync a = new YunhaSync();
new Thread(() -> {
for(int i = 0; i < 10; i++) {
a.say("사건의 지평선");
}
}).start();
new Thread(() -> {
for(int i = 0; i < 10; i++) {
a.say("오르트구름");
}
}).start();
}
public void say(String song) {
yunha = song;
try {
long sleep = (long)(Math.random() * 100);
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!yunha.equals(song)) {
System.out.println(song + " | " + yunha);
}
}
}
원자성은 연산이 전부 아니면 전혀 수행되지 않는 것을 의미하는데, 위 로직에서는
yunha
라는 공유 자원을 두 개의 스레드가 동시에 접근하고 수정하고 있음
say
메서드 내에서yunha
의 값을 설정하고 나서, 스레드가 잠시 일시 중지(Thread.sleep
됨. 이 일시 중지 동안 다른 스레드가 실행되어yunha
변수의 값을 변경할 수 있음. 첫 번째 스레드가 일시 중지에서 깨어나yunha
의 값을 확인할 때, 다른 스레드에 의해 변경된 값이 있을 수 있으므로, 첫 번째 스레드가 예상했던 값과 실제 값이 일치하지 않는 경우가 발생할 수 있음
package programmers;
public class YunhaSync {
private static String yunha = "오르트구름";
public static void main(String[] args) {
YunhaSync a = new YunhaSync();
new Thread(() -> {
for(int i = 0; i < 10; i++) {
a.say("사건의 지평선");
}
}).start();
new Thread(() -> {
for(int i = 0; i < 10; i++) {
a.say("오르트구름");
}
}).start();
}
public synchronized void say(String song) {
yunha = song;
try {
long sleep = (long)(Math.random() * 100);
Thread.sleep(sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!yunha.equals(song)) {
System.out.println(song + " | " + yunha);
}
}
}
synchronized
synchronized
키워드는 멀티 스레드 프로그래밍 시 동기화를 달성하기 위해 사용함.
synchronized
키워드가 적용된 메서드나 블록에 대해서는 한 시점에 하나의 스레드만 접근할 수 있도록 보장함. 이는 공유 자원에 대한 동시 접근을 막고, 스레드 간의 안전한 상호작용을 가능하게 하여 데이터 무결성을 유지함
synchronized
키워드 사용synchronized
키워드를 추가함. 이렇게 하면 해당 메서드가 포함된 객체의 lock을 획득한 스레드만 그 메서드를 실행할 수 있음public synchronized void synchronizedMethod(){
//
}
synchronized
블록으로 만듬. 이 떄, 동기화할 객체의 lock을 명시해야 함public void method(){
synchronized(this){
// 동기화 코드
}
}
synchronized와 싱글톤
싱글톤 패턴의
getInstance()
메서드에synchronized
를 사용하는 경우
1. 인스턴스 생성 전 동기화 : 인스턴스가 아직 생성되지 않았을 때,syncronized
키워드는 첫 번째로getInstance()
를 호출하는 스레드가 싱글톤 인스턴스를 안전하게 생성할 수 있도록 보장함
package programmers;
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 성능 고려 : 한번 인스턴스가 생성된 후에도
synchronized
메서드를 계속 사용하면, 모든getInstance()
호출이 동기화되므로 성능에 부정적인 영향을 미칠 수 있음. 이 문제를 완화하기 위해double-checked locking
패턴을 사용함(6번에서 자세히 설명)
정적 멤버
클래스 로딩 시점에 싱글톤 인스턴스를 생성함으로써, 멀티스레딩 환경에서도 안전하게 인스턴스를 관리할 수 있도록함
초기화 시점의 명확성
: 정적 멤버를 이용한 싱글톤 패턴에서는, 클래스가 JVM에 로드되는 시점에 싱글톤 인스턴스가 생성됨. 이는 클래스 로딩이 일어나는 순간 단 한번만 수행되기 때문에, 초기화 시점이 명확하고, 추가적인 동기화가 필요하지 않음
스레드 안전성
: 클래스 로더에 의해 클래스가 초기화될 때, 정적 멤버가 한 번만 생성되므로, 이 방법은 스레드 안전성을 자연스럽게 보장함. 여러 스레드가 동시에 접근하더라도 인스턴스는 단 한번만 생성되기 때문에, 동기화 문제가 발생하지 않음
지연로딩(Lazy Loading) 미지원
: 정적 초기화 블록을 사용하는 싱글톤 패턴은 지연 로딩을 지원하지 않음. 즉, 클래스가 로드되는 시점에 인스턴스가 바로 생성되므로, 실제로 인스턴스가 필요하지 않더라도 메모리를 차지하게 됨. 이는 불필요한 리소스 사용을 초래할 수 있음
구현 예
public class Singleton {
// 클래스 로딩 시점에 싱글톤 인스턴스 생성
private static final Singleton instance = new Singleton();
// private 생성자로 외부에서의 인스턴스 생성 방지
private Singleton() {}
// 외부에서 싱글톤 인스턴스에 접근할 수 있는 메서드
public static Singleton getInstance() {
return instance;
}
}
정적 블록
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return instance;
}
}
정적 멤버와 Lazy Holder(중첩 클래스)
singleInstanceHolder
라는 내부 클래스를 하나 더 만듦으로써 Singleton 클래스가 최초에 로딩되더라도 함께 초기화가 되지 않고 getInstance()가 호출될 때 singleInstanceHolder 클래스가 로딩되어 인스턴스를 생성하게 됨package programmers;
public class Singleton {
private static class singleInstanceHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return singleInstanceHolder.INSTANCE;
}
}
double-checked locking 적용 로직
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
instance
클래스 변수 앞에 붙은 volatile
은 무엇인가?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();
}
}
무한 루프 발생
위 로직이 무한 루프를 도는 이유는 , 첫 번째 스레드가
flag
변수의 값을 갱신하는 것을 두 번째 스레드가 볼 수 없기 때문이다. 이는 첫 번째 스레드가 자신의 로컬 캐시에flag
값을 가지고 있으며, 두 번째 스레드가flag
값을false
로 변경해도 첫 번째 스레드의 로컬 캐시에 반영되지 않기 때문에 발생한다.
boolean flag 앞에 volatile static 선언하기
public class Test {
volatile static 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();
}
}
volatile 키워드는 자바의 변수를 다룰 때 사용되며, 주요한 세 가지 특성이 있음
1. 가시성 보장 :volatile
로 선언된 변수는 값이 변경될 때 다른 스레드에 즉시 반영되어, 모든 스레드가 항상 최신 값을 볼 수 있음
2. 동기화 비용 절감 :syncronized
에 비해 가볍지만, 변수의 읽기와 쓰기를 메인 메모리에서 직접 수행함으로써 동기화의 오버헤드를 줄임
3. 명령어 순서화 :메모리 배리어(Memory Barrier)
기능을 통해volatile
변수의 읽기과 쓰기 명령이 순서대로 실행되도록 하여, 명령어 재배치에 의한 부작용을 방지함
volatile static boolean flag
와 volatile boolean flag
의 차이는?키워드 | 설명 | 공유 수준 |
---|---|---|
volatile static boolean flag | flag 는 클래스 변수로, 클래스의 모든 인스턴스 간에 공유되며 프로그램 전체에서 하나만 존재한다. volatile 은 모든 스레드에 변경사항이 즉시 보이도록 보장한다. 이는 프로그램 전역 상태를 관리할 때 사용된다. | 클래스 수준(전역) |
volatile boolean flag | flag 는 인스턴스 변수로, Test 클래스의 각 인스턴스마다 별도의 flag 를 가진다. volatile 은 해당 인스턴스를 사용하는 스레드에 변경사항이 즉시 보이도록 보장한다. 이는 각 객체 인스턴스의 상태를 스레드에 가시적으로 유지할 때 사용된다. | 인스턴스 수준 |
package programmers;
public class Singleton {
public enum SingletonEnum {
INSTANCE;
public void doSomething() {
System.out.println("Doing something...");
}
}
public static void main(String[] args) {
SingletonEnum.INSTANCE.doSomething();
}
}