78. 공유 중인 가변 데이터는 동기화해 사용하라

📌 1. 발표 전 알아야 할 개념

1) 스레드(Thread)

  • 프로세스 : 실행중인 프로그램. 자바 JVM은 하나의 프로그램으로 실행되며, 동시에 여러 작업을 수행하기 위해 멀티 스레드 지원

  • 스레드 : 프로세스 안에서 동시에 여러 작업을 처리할 수 있게 해주는 실행 단위. 출처 : https://www.codelatte.io/courses/java_programming_basic/3YOZQA8ZUYBPYNHY

  • 스레드를 만들자

    public static void main(String[] args) {
        Runnable runn = new MyThread();
        // [생성] 스레드 생성
        Thread th1 = new Thread(runn);
        // 스레드 실행 (run() 메서드가 비동기적 실행)
        th1.start();
    }

    // [세팅] Runnable 인터페이스를 통해서 구현
    class Test implements Runnable {
        public void run() {
            // 작업
        }
    }

2) 동기 vs 비동기

  • 동기(Synchronous) : 작업이 끝날 때까지 기다렸다가 다음 작업을 실행함

  • 비동기(Asynchronous) : 작업이 끝나는 것을 기다리지 않고 동시에 다른 작업도 수행함

3) 공유자원(Synchronization)과 임계 영역(Critical Section)

  • 여러 스레드가 함께 접근하는 데이터나 자원

  • 임계 영역 : 공유 자원에 접근하는 코드 영역

4) 상호배제(Mutual Exclution)과 가시성(Visibility), 원자성(Atomicity), 배타적 수행(Exclusive Execution)

  • 상호배제 : 한 순간에 오직 하나의 스레드만 공유 자원에 접근할 수 있게 하는 제약

    • 상호 배제를 하지 않으면? 여러 스레드가 동시에 데이터를 읽고 쓰면서 무결성이 깨짐

  • 가시성 : 한 스레드가 변경한 내용을 다른 스레드에게 보이게 함

    • 자바에서는 각 스레드가 CPU 캐시나 레지스터에 값을 들고 있을 수 있음

    • 변경 즉시 메인 메모리에 반영되지 않으면? 다른 스레드는 예전 값을 계속 보게됨

  • 원자성 : 하나의 작업이 중간에 끼어들 수 없이 한 번에 수행되는 성질

  • 배타적 수행 : 한 번에 하나의 스레드만 특정 코드 블록을 실행할 수 있도록 보장하는 것

📕 2. 동기화(Synchronization)

  • 동기화 : 여러 스레드가 공유 자원에 안전하게 접근할 수 있도록 순서를 조정하는 매커니즘 -> (쉽게 말하면..) 여러 스레드가 같은 데이터를 동시에 건드리지 못하도록 순서를 조율한다

  • 락(Lock) : 여러 스레드가 동시에 공유 자원에 접근하지 못하게 막고, 하나의 스레드만 접근할 수 있도록 잠그는 장치

  • 동기화는 내부적으로 을 통해 상호 배제를 구현한다

  • **동기화는 배타적 실행 뿐만 아니라, 스레드의 안정적인 통신에 꼭 필요하다**

  • 만약 동기화가 안된다면...?

public class BrokenCounter {
    private int count = 0;

    public void increment() {
        count++; // count 값 읽고 - 1 더하고 - 결과 다시 저장
    }

    public int getCount() {
        return count;
    }
}

두 스레드가 동시에 실행된다면, 1, 2와 같이 순차적으로 실행되는 것이 아니라, 둘 다 같은 값을 읽고, 똑같이 1을 더하고, 같은 값을 다시 저장할 수 있다. -> 증가가 실행이 안됨!

🔥 3. 동기화를 해보자 - 동기화 방법

3-1. Synchronized

  • Java에서 가장 기본적인 동기화 매커니즘

  • 한 번에 하나의 스레드만 해당 블록에 접근하게 만든다 (배타적 수행 보장)

public class BrokenCounter {
    private int count = 0;

    public synchronized void increment() {
        count++; // 동기화
    }

    public synchronized int getCount() {
        return count;
    }
}
  • synchronized를 사용하면 JVM은 메모리 동기화와 락(lock)을 자동으로 처리해줌

  • 동기화된 메서드는 하나의 스레드만 접근할 수 있음

3-2. volatile

  • volatile 키워드를 이용하여 변수의 가시성 문제를 해결할 수 있음.

  • 변경된 값을 메인 메모리에 즉시 반영, 다른 스레드가 최신 값을 볼 수 있도록 보장

public class VolatileFlag {
    private volatile boolean stop = false;

    public void stopThread() {
        stop = true;
    }

    public void run() {
        while (!stop) {
            // 실행 내용
        }
    }
}
  • 단, 복합 연산(++, += 등)에는 원자성 보장을 하지 않는다. -> syncronized 사용

// 잘못된 사용 예 - count++는 원자적이지 않음
private volatile int count = 0;

public void increment() {
    count++; // 원자성 보장 안 됨
}

🔧 4. 동기화와 관련된 주의사항

4-1. Thread.stop()은 절대 사용하지 마라

  • Thread.stop()은 스레드를 강제 종료 시킴. 이 과정에서 락을 해제하지 않고 종료되어 다른 스레드가 락이 걸린 상태에서 영원히 대기할 수 있음

  • 데이터 무결성이 깨지고, 프로그램이 비정상 종료될 수 있음

  • 락을 잡은 스레드가 락을 반환하지 못한 채로 종료 -> 데드락 발생

  • volatial 플래그나 interrupt()`를 활용해 안전하게 종료 조건을 제어해야 한다.

4-2. 읽기와 쓰기 모두가 동기화 되지 않으면 아무 의미가 없다

  • 어떤 스레드가 synchronized로 값을 썼는데, 다른 스레드가 동기화되지 않은 상태로 그 값을 읽는다면 최신 값이 보장되지 않는다.

4-3. long과 double은 원자적이지 않을 수 있다

  • long과 double은 64비트 크기 단위

  • JVM의 기본 연산 단위는 32비트이기 때문에 두 번 나누어 읽고 쓸 수 있다

  • 멀티 스레드 환경에서는 한 스레드가 long, double 값을 읽는 중 다른 스레드가 쓰게 되면 이상한 값을 읽을 수 있다..!

  • volatile을 이용해 64비트 전체를 한 번에 읽고 쓰도록 강제하자

=> 결론 : 공유 중인 가변 데이터는 항상 동기화하되, 가능하면 공유하지 말자


🤖 최종 결론

  1. 공유 중인 가변 데이터는 항상 동기화하라

  2. 단일 변수의 가시성만 필요할 땐 volatile, 복잡한 동기화와 관련되어 있을 경우 : synchronized

  3. 최선의 방법은 가변 데이터 공유를 피하는 것!


😶‍🌫️ 느낀점

  • 스프링에서 동기화 관련하여 상태 관리나 동시성에 대해 알아보고 싶다

Last updated