Thread Safety는 멀티스레드 환경에서 여러 스레드가 동시에 같은 자원에 접근하거나 작업할 때, 올바르게 동작하는지 보장하는 개념이다.
스레드 안정성이 보장되지 않으면 데이터가 손상되거나 예상치 못한 동작을 하게 될 수 있다.
멀티스레드 환경에서는 여러 스레드가 동시에 공유 자원에 접근할 수 있다.
이러한 상황에서는 각각의 스레드가 어떠한 공유 자원에 동시 수정
을 시도 하는 경우 발생하는 Race Condition
이나 Deadlock
등과 같은 오류가 문제가 된다.
두 개 이상의 스레드가 같은 데이터를 동시에 수정할 때 발생하는 문제
수정 작업이 서로 덮어쓰거나 동시에 일어나 예상하지 못한 결과를 가져옴
두 개 이상의 스레드가 서로를 기다리는 상황이 발생하여 블로킹되는 상황
스레드들이 계속해서 상태를 바꾸면서도, 실질적으로는 아무런 작업을 수행하지 못하는 상태
public class StaticExample {
private static int sharedVariable = 0;
public static void incrementSharedVariable() {
sharedVariable++;
}
public static int getSharedVariable() {
return sharedVariable;
}
}
두 스레드가 같은 값을 읽고 증가시키면서 서로 덮어쓰는 현상인 Race Condition
이 발생하는 경우
이 경우 여러 스레드가 동시에 sharedVariable
에 접근하면 기대한 만큼의 값 증가가 일어나지 않을 수 있다.
일반적인 HashMap
같은 Java Collection은 스레드에 안전하지 않으므로, 여러 스레드가 동시에 접근할 때 비정상적인 동작이 발생할 수 있다.
public class MapExample {
private Map<String, Integer> map = new HashMap<>();
public void addElement(String key, Integer value) {
map.put(key, value);
}
public Integer getElement(String key) {
return map.get(key);
}
}
공유 변수때와 비슷하게 쓰기 작업에 대해서 Race Condition
이 발생할 수 있다.
클래스의 초기화 시점에서 여러 스레드가 동시에 객체를 생성하는 경우에도 thread-unsafety 문제가 발생할 수 있다.
예를 들면 Singleton
패턴을 사용할 때 아래와 같은 코드는 문제가 될 수 있다.
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
위의 코드에서 getInstance()
를 여러 스레드에서 동시에 호출한다면, instance
가 아직 생성되지 않았을 때 동시에 여러 인스턴스를 생성할 수 있는 가능성이 있다.
스레드가 공유 자원에 접근할 때, synchronized
를 사용하여 한 번에 하나의 스레드만 자원에 접근할 수 있도록 제어
Java에서는 synchronized
키워드를 사용하여 메서드 혹은 블록을 동기화할 수 있다.
public synchronized void increment() {
count++;
}
Java의 java.util.concurrent.locks
패키지에서 제공하는 Lock
인터페이스를 사용하여 synchronized
보다 유연하게 동기화를 제어할 수 있다.
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 락 획득
try {
count++;
} finally {
lock.unlock(); // 락 해제
}
}
Java의 java.util.concurrent.atomic
패키지는 thread-safe한 연산을 수행할 수 있도록 도와주는 Atomic Class를 제공한다.
대표적으로 AtomicInteger
, AtomicLong
등이 있다.
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
CAS
알고리즘 사용으로 synchronized
나 lock
없이도 안정성 보장. 높은 성능불변 객체는 한 번 생성되면 그 상태를 변경할 수 없는 객체를 의미
이러한 객체는 여러 스레드가 동시에 접근하더라도 상태가 변하지 않기에 스레드 안정성을 보장
대표적으로 String
Class가 있다.
public final class ImmutableValue {
private final int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
Map<String, String> map = new ConcurrentHashMap<>();
Static inner class
를 사용하면 JVM이 클래스 로드 시점에 스레드 안전성을 보장해 준다고 한다.
public class Singleton {
private Singleton() {}
private static class SingletonHelper {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
이 방식에서는 SingletonHelper 클래스가 호출될 때까지 로드되지 않으며, 클래스가 처음 로드되는 시점에 JVM에서 인스턴스를 안전하게 생성한다. 그렇기에 synchronized 키워드를 사용할 필요 없이 thread safety를 유지할 수 있다.
static 키워드를 사용하면서도 lazy initialization을 보장할 수 있는 이유는 JVM의 클래스 로딩
방식으로 인해서다.
JVM의 클래스 로딩 과정은 아래와 같다.
위와 같은 과정으로 인해 static 키워드를 사용한 static inner class는 클래스가 처음으로 참조될 때 로드되기 때문에, 처음 호출 전까지는 메모리에 적재되지 않고 초기화도 되지 않는다. 이러한 동작 방식이 lazy initialization을 보장하는 이유다.
JVM에서 static 키워드와 관련된 것을 로드할 때 초기화가 된다고 생각을 하였다. 하지만 실제로 확인을 해보았을 때 클래스 메타데이터가 올라간다고 초기화가 되지 않음을 확인하였다.
public class Singleton {
private static final Logger logger = LoggerFactory.getLogger(Singleton.class);
// 싱글톤 클래스가 로드될 때 호출됨
static {
logger.info("Singleton 클래스가 로드되었습니다.");
}
private Singleton() {
// Singleton 인스턴스가 생성될 때 로그 출력
logger.info("Singleton 생성자가 호출되었습니다.");
}
// static inner class
private static class SingletonHelper {
// 정적 내부 클래스가 로드될 때 로그 출력
static {
logger.info("SingletonHelper 클래스가 로드되었습니다.");
}
// Singleton 인스턴스가 초기화
private static final Singleton INSTANCE = new Singleton();
}
// Singleton 인스턴스를 반환
public static Singleton getInstance() {
// getInstance() 호출 시 로그 출력
logger.info("getInstance() 메서드가 호출되었습니다.");
return SingletonHelper.INSTANCE;
}
public void someMethod() {
// someMethod() 호출 시 로그 출력
logger.info("someMethod()가 호출되었습니다.");
}
}
위와 같은 Singleton
패턴을 구현한 클래스가 있다.
public static void main(String[] args) {
System.out.println(Singleton.class);
}
다른 클래스에서 class 정보를 출력하면 Singleton 클래스의 static 블록
은 호출이 될까?
결과는 단순 메타데이터만 출력이 됨을 알 수 있다.
public static void main(String[] args) {
Singleton.getInstance();
}
그리고 처음으로 인스턴스를 생성할 때 Lazy Initialization
를 보장함을 확인할 수 있다.