Thread-Safe란?
다수의 스레드가 공유 자원에 접근해도 프로그램이 문제없이 동작하는 것, 즉 안정성이 보장되는 상태를 의미한다.
Thread Safety는 단순히 한 번에 하나의 스레드가 공유 자원에 접근하도록 보장하는 것만을 의미하지 않는다.
RaceCondition, Deadlock, Livelock, Starvation 이 발생하지 않는 동시에, 공유 자원에 대한 순차적인 접근이 이루어지도록 보장해야한다.
- RaceCondition : 멀티 스레드 환경에서 두 개 이상의 스레드가 공유 자원에 동시 접근할 때 발생할 수 있는 문제.
- Deadlock : 교착상태. 두 개 이상의 스레드가 서로 자원을 점유한 채, 상대방이 점유한 자원을 기다리며 무한히 대기하는 상태.
- Livelock : 활성 교착 상태. 두 개 이상의 프로세스가 서로의 진행을 방해하지 않으면서도, 특정 조건을 만족시키기 위해 반복적으로 상태를 변경하며 무한히 대기하는 상태.
- Starvation : 기아상태. 특정 프로세스나 스레드가 자원에 접근할 기회를 계속해서 얻지 못하는 상황.
Thread-Safety를 고려해야 하는 이유
- 스레드의 접근 시점과 순서에 따라 실행결과가 달라지게 된다.
- 예상하지 못한 결과를 초래한다.
- 그러므로 원하는 결과를 얻기 위해서 멀티스레드 환경인 경우 Thread-Safety를 고려해야한다.
Thread-Safety
Stateless 무상태, Immutable 불변
객체를 무상태(Stateless)로 구현하면 Safe하다. 내부 상태를 가지고 있지 않은 것이다.
메소드가 호출될 때, 상태가 변하지 않으면 Safe하다.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
public class Main {
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println(calculator.add(1, 2)); // 출력: 3
System.out.println(calculator.subtract(5, 3)); // 출력: 2
}
}
Immutable 객체는 한번 생성되면 그 상태를 변경할 수 없는 객체입니다. 모든 필드는 final 로 선언되고, 생성 후에는 변경될 수 없습니다.
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Synchronized
하나의 스레드가 synchronized로 지정한 임계 영역에 들어가 있을때 lock이 걸리기 때문에 다른 스레드가 임계 영역에 접근할 수 없다. 이후 해당 스레드가 벗어나면 unlock 상태가 되어 대기하고 있던 다른 스레드가 접근해 다시 lock을 걸어 사용할 수 있다.
public class BusSynchronizedMethod {
private final int minOccupancy = 10;
private int reservation = 0;
public ***synchronized*** void getBusTicket() {
try {
Thread.sleep(100);
reservation++;
if (reservation < minOccupancy) {
Thread.sleep(10);
System.out.println("인원 부족으로 버스 운행이 취소될 수 있습니다. 현재 예약 인원: " + reservation);
}
} catch (InterruptedException e) {
System.out.println("ERROR!");
}
}
}
Atomic Objects
atomic 클래스는 멀티 스레드 환경에서 원자성을 보장한다.
AtomicInteger, AtomicLong, AtomicBoolean 등의 atomic 클래스.
public class BusAtomic {
private final int minOccupancy = 10;
private final ***AtomicInteger*** reservation = new AtomicInteger();
public void getBusTicket() {
try {
Thread.sleep(100);
int newReservation = reservation.incrementAndGet();
if (newReservation < minOccupancy) {
Thread.sleep(1);
System.out.println("인원 부족으로 버스 운행이 취소될 수 있습니다. 현재 예약 인원: " + newReservation);
}
} catch (InterruptedException e) {
System.out.println("ERROR!");
}
}
}
Volatile
변수에 volatile 키워드를 붙이면 CPU cache를 사용하지 않고 Main Memory에 변수를 저장해 읽기와 쓰기를 한다.
public class VolatileExample {
private static ***volatile*** boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (running) {
// Do some work
}
System.out.println("Thread terminated.");
});
thread.start();
Thread.sleep(1000); // 메인 스레드가 1초간 대기
running = false; // 메인 스레드가 플래그를 false로 변경
System.out.println("Flag changed to false.");
}
}
Synchronized Collections
Java Collection Framework(JCF)의 대부분 Collection 구현체들은 Thread-Safe하지 않다.
java.util.Collections 클래스가 제공하는 static 팩토리 메소드인 synchronizedCollection() 메서드를 이용하면 Thread-Safe한 Collection 객체를 생성할 수 있다.
void syncrhonized_collection에_원소를_추가한다() throws InterruptedException {
int N = 30;
Collection<Integer> syncCollection = Collections.***synchronizedCollection***(new ArrayList<>());
List<Integer> addElements = Arrays.asList(1, 2, 3);
CountDownLatch latch = new CountDownLatch(N);
for (int i = 0; i < N; i++) {
THREAD_POOL.execute(() -> {
syncCollection.addAll(addElements);
latch.countDown();
});
}
latch.await();
System.out.println(syncCollection.size());
assertThat(syncCollection.size()).isEqualTo(N * 3);
}
Concurrent Collections
Synchronized Collection 대신 Concurrent Collection을 사용해도 Thread-Safe한 Collection 객체를 생성할 수 있다. java.util.concurrent 패키지에서 CopyWriteArrayList, ConcurrentMap, ConcurrentHashMap 등의 클래스를 찾아볼 수 있다.
void concurrent_collection에_원소를_추가한다() throws InterruptedException {
int N = 30;
Collection<Integer> concurrentCollection = new ***CopyOnWriteArrayList***<>();
List<Integer> addElements = Arrays.asList(1, 2, 3);
CountDownLatch latch = new CountDownLatch(N);
for (int i = 0; i < N; i++) {
THREAD_POOL.execute(() -> {
concurrentCollection.addAll(addElements);
latch.countDown();
});
}
latch.await();
System.out.println(concurrentCollection.size());
assertThat(concurrentCollection.size()).isEqualTo(N * 3);
}
'개발' 카테고리의 다른 글
[SQL] SubQuery, 서브쿼리 (0) | 2024.06.01 |
---|---|
[Java] Java의 메모리 영역, 컴파일 방식 (0) | 2024.06.01 |
[React] DOM, Virtual DOM (0) | 2024.06.01 |
인증/인가 (feat. Cookie, Session) (0) | 2024.05.26 |
ESM과 CommonJS의 차이 (0) | 2024.05.26 |