본문 바로가기

개발

[Java] Thread-Safe, 스레드 안전

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