싱글톤 패턴 생성 여부를 확인하고 싱글톤이 없으면 새로 만들고, 있다면 만들어진 인스턴스를 반환한다.
public class Singleton {
private static Singleton instance; //인스턴스 선언
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
/*singleton instance를 필요로 하는 모듈이 getInstance()라는 메서드를 호출하게 될때,
instance가 할당되지 않았을땐 instance를 생성하고 반환한다.*/
그러나 이 코드는 메서드의 원자성이 결여되어 있다. 멀티스레드 환경에서는 싱글톤 인스턴스를 2개 이상 만들 수 있다.
※ 원자성 : 여러개의 쓰레드가 있을 때, 특정 시점에 어떤 메소드를 두개 이상의 쓰레드가 동시에 호출 못한다는 것
java는 멀티 스레드 언어이기 때문에, 여러개 스레드에서의 어떠한 로직을 생각해야한다. 그래서 여러개 스레드로 발생할 상황을 생각해야한다.
Thread1에서 if instance==null → instance 생성 → instance==null! → instance 리턴
(빨간글씨가 순서니 빨간글씨만 보기)
if instance==null → thread2에서 if instance==null → instance가 thread2에서 할당 → instance 할당
위의 경우대로 전개될시, 인스턴스가 2개가 만들어진다.
thread.java
public class YunhaSync {
private static String yunha = "오르트구름";
public static void main(String[] agrs) {
YunhaSync a = new YunhaSync();
//Thread1
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);
}
}
}
[로직분석]
메소드 say는 매개변수 song을 받아서 yunha라는 변수에다가 song을 할당하고 랜덤시간동안 있다가 비교하는 로직이다.
① yunha==song
② sleep
③ yuhha==!song
③에서 yunha와 song이 같은지 equal 함수로 확인할때, ①과 다를게 없어서 당연히 true가 나올거라고 생각할지도 모르겠지만
thread 2개에 각각 다른 매개변수(사건의 지평선, 오르트 구름)를 기반으로 say를 호출하고 있을때 Thread1에서 yunha를 사건의 지평선을 만들었다면, sleep일때 thread2가 나타나 yunha를 오르트구름을 바꿀 수 있기 때문에 yunha==song 이 true가 아닐 수도 있다.
그래서 순서대로 실행되게 하기 위해서 synchronized 키워드를 얹으면 된다.
자바 컴파일러 사이트 : https://www.jdoodle.com
thread2.java
public class YunhaSync {
private static String yunha = "오르트구름";
public static void main(String[] agrs) {
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);
}
}
}
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);
}
}
//순서대로 실행이 되었기 때문에 console창에 아무것도 뜨지 않는다.
인스턴스를 반환하기 전까지 격리 공간에 놓기 위해 synchronized 키워드로 잠금을 할 수 있다. 최초로 접근한 스레드가 해당 메서드 호출시에 다른 스레드가 접근하지 못하도록 잠금(lock)을 걸어준다.
이 때 getInstance() 메서드를 호출할 때마다 lock이 걸려 성능저하가 된다.
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
정적(static) 멤버나 블록은 런타임이 아니라 최초에 JVM이 클래스 로딩 때 모든 클래스들을 로드할 때 미리 인스턴스를 생성하는데 이를 이용한 방법이다.
클래스 로딩과 동시에 싱글톤 인스턴스를 만든다. 그렇기 때문에 모듈들이 싱글톤 인스턴스를 요청할 때 그냥 만들어진 인스턴스를 반환하면 되는 원리이다.
하지만 이는 싱글톤 인스턴스가 필요없는 경우도 무조건 싱글톤 클래스를 호출해 인스턴스를 만들어야 하기 때문에 불필요한 자원낭비를 하게된다.
3.java
public class Singleton {
private final static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
getInstance() 메서드를 통해서 할당하는게 아니라, 처음부터 private final static Singleton instance = new Singleton();에 할당을 한다.
따라서 모듈들이 필요할때 호출하는게 아니라, 클래스 로드에 의해 최초에 생성을 해버린다.
4.java
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}