One Cookie a day
Thread Safety에 대해 알아보자
Bubbles
2023. 9. 22. 20:31
Thread
- 프로세스가 할당받은 자원을 이용하는 실행 흐름의 단위
- Thread끼리 메모리의 특정 부분을 공유하며 작동
- Code : 프로그램의 코드 / 명령어들이 기계어로 저장된 영역
- Data : 코드에서 선언된 전역 / 정적 변수가 저장되는 영역 -> 실행 시 할당되고 종료 시 소멸
- Stack : 컴파일 타임에 크기 결정, 지역변수, 매개변수, 리턴 값 등 저장
- Heap : 런타임에 크기가 결정, 사용자에 의해 메모리 공간이 동적으로 할당되고 해제되는 영역, 참조형 등 저장
- 독립적으로 가지는 부분 : program counter, register set, stack
- 다른 스레드와 공유하는 부분 : code, data, OS resources
Thread-Safety?
* 멀티 스레드 간 동기화 문제란?
- 어느 스레드가 어떤 순서로 실행될 지 순서 알 수 없음.
- 스레드가 함께 전역변수를 사용한다면 다른 프로세스가 접근을 못하거나, 바뀐 자원에 접근해 버리는 등 충돌이 발생할 위험
- 운영체제가 자동으로 해 주는 것이 아니므로, 프로그래머가 직접 적합하게 프로그래밍해야 함
= 여러 스레드에서 동일한 객체에 선언된 메소드에 접근하여 데이터를 처리하려 할때 발생하는 문제
여러 스레드에서 동시에 동일한 객체에 선언된 메소드에 접근하여 데이터를 처리해도, 정확하게 동작한다면 Thread-Safe 하다고 할 수 있습니다.
Thread Safety를 보장하는 방법?
1. synchronized
- 여러 스레드가 하나의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근할 수 없도록 막는 키워드
- 메서드에 붙이거나 메서드 내 블록 단위에 붙일 수 있음
- Lock을 걸어 다른 스레드의 접근을 막기 때문에 멀티 스레드 간 가시성 문제와 동시 접근 문제를 해결하여 thread-safe
- 남용하게 되면 스레드들이 lock을 얻기 위해 대기해야 하므로 성능 저하 문제가 발생할 수 있음
- Atomic은 무슨 자료구조?
- package java.util.concurrent.atomic에 존재.
- CAS 알고리즘 이용하여 현재 스레드가 가진 값과 메모리에 저장된 값 비교 후 다르면 스레드를 동기화시킴
- 따라서 synchronzied 처럼 락 걸지 않고, 쓰기 작업 스레드가 하나 이상이어도 된다.
- Volatile로 값이 선언되어 있다.
- 각 스레드가 가진 CPU cache에서 값을 가져오지 않고, 공유하는 Main memory에서 읽고 쓰는 방법
- 따라서 단순 Volatile로 동시성 문제를 해결하려면 쓰기 작업을 수행하는 스레드는 하나여야 함.
- 이 경우 값을 쓸 때는 CAS 알고리즘을 통해 동시성을, 값을 읽어올때는 메모리에서 읽어와서 해결
- 값을 쓸 때 비교 후 작성한다
- public final boolean compareAndSet(int expect, int update)
- return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
// ...
}
2. 메소드 내 지역변수 활용
- 위에서 언급한 대로, 각 스레드는 Stack을 독립적으로 할당받는다.
- Stack에는 지역변수, 리턴 값, 매개변수 등이 저장되는데, 이 Stack은 스레드끼리 공유하지 않는다.
3. Thread-Safe한 자료구조 사용
- Concurrent Package 자료구조들
- ex) Concurrent Hash Map
- put, remove, clear 등의 함수에 내부적으로 synchronized 키워드 구현
- ex) Concurrent Hash Map
4. 불변객체 활용하기
- 생성 후 그 상태를 바꿀 수 없는 객체
- 객체가 가리키고 있는, 힙 영역에 있는 데이터 자체를 변경할 수 없다.
- Immutable하게 만들면 값이 변경되지 않기 때문에, thread-safe하다.
5. ThreadLocal
- 각 스레드별로 할당되는 변수로, 고유한 값들을 가지며 스레드별로 독립적인 접근을 보장한다.
- 내부적으로 Map에 저장되어 있다. (ThreadLocalMap - Thread 정보를 키로 삼아 저장 )
아래는 오라클 공식 문서에서 TreadLocal 페이지에 들어가면 볼 수 있는 예시코드이다.
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
}
참고글