JAVA/비동기 프로그래밍

[Java] 동기화 개념

꾸준함. 2024. 2. 24. 23:04

1. 싱글 쓰레드 vs 멀티 쓰레드

프로세스는 오직 한 개의 쓰레드로만 구성하는 싱글 쓰레드 프로세스하나 이상의 쓰레드로 구성하는 멀티 쓰레드 프로세스로 구분할 수 있습니다,.

멀티 쓰레드가 항상 좋은 것은 아니며, 자원을 효율적으로 활용하고 성능을 고려한 후에 적절한 방식을 선택해야 합니다.

  • 현재 시중에 나온 CPU는 대부분 멀티코어를 지원하기 때문에 병렬적 성능 및 동시적 자원을 사용하는 관점에서는 대부분 멀티 쓰레드 기반 프로그래밍이 유리한 것은 맞으나
  • 비동기 논블로킹 프로그래밍 같이 싱글 쓰레드 혹은 적은 쓰레드를 활용하는 것이 좋은 성능 및 응답성을 보여줄 수도 있음

 

1.1 싱글 쓰레드

 

장점

  • context switching으로 인한 딜레이가 없음
  • 동시성 문제없음
  • 자원을 적게 사용
  • 앞서 언급한 문제들을 고려하지 않아도 되기 때문에 구현하기 쉬움

 

단점

  • CPU의 멀티코어 활용 못함
  • 순차적 실행으로 인해 응답성 및 전체 처리량 낮음
  • I/O 처리 시 CPU 낭비
  • 쓰레드에 오류 발생 시 전체 프로그램이 종료됨

 

1.2 멀티 쓰레드

 

장점

  • 동시성으로 인해 사용자의 응답성 향상
  • CPU 멀티코어를 활용해 병렬 성능 향상
  • CPU 낭비 없이 자원을 효율적으로 사용
  • 한 쓰레드에서 오류가 발생하더라도 타 쓰레드에 영향 없음

 

단점

  • 쓰레드가 많을 경우 context switching가 빈번하게 이루어져 성능 저하
  • 동시성 이슈 및 자원을 고려하여 구현해야 하기 때문에 프로그래밍 난이도 비교적 높음
  • 쓰레드 생성 비용이 적지 않음
  • 쓰레드 간 동기화 이슈 발생 가능
    • 같은 프로그램 내 실행되는 여러 쓰레드가 읽기 및 쓰기 작업을 같은 메모리 영역에서 동시에 실행할 경우 발생할 수 있음

 

2. 동기화와 CPU 관계

 

2.1 동기화(Synchronization)

  • 프로세스/쓰레드 간 공유 영역에 대한 동시접근으로 인해 발생하는 데이터 불일치를 방지하고 일관성을 유지하기 위해 순차적으로 공유 영역에서 작업을 수행하도록 보장하는 메커니즘

 

2.2 CPU 연산 처리

  • CPU가 두 개 이상의 명령어를 처리하는 경우에는 원자성이 보장되지 않는데 이는 각 명령을 수행하는 도중 OS의 스케줄링 알고리즘으로 인해 다른 명령을 수행하게 함으로써 현재 수행 중인 명령을 인터럽트 걸 수 있음
  • 두 개 이상의 명령어를 원자성으로 묶기 위해서는 쓰레드 간 동기화가 필요함
    • 한 쓰레드가 모든 명령을 다 수행될 때까지 도중에 인터럽트 받지 않도록 처리 필요

 

3. 임계 영역 (Critical Section)

  • 둘 이상의 쓰레드가 동시에 접근해서는 안 되는 공유 자원에 접근하는 코드 영역
  • 네 가지 영역으로 구성
    • entry section: 임계 영역에 진입하기 위해 진입 허가를 요청하는 영역
    • critical section: 데이터 불일치를 유발할 수 있는 영역이기 때문에 하나의 쓰레드만 접근할 수 있는 영역
    • exit section: critical section에서 빠져나올 때 신호를 알리는 영역
    • remainder section: 위 세 가지 영역을 제외한 나머지 영역


 

3.1 임계 영역 문제 (Critical Section Problem)

 

  • 한 쓰레드가 임계 영역에서 작업을 수행하고 있을 때 다른 쓰레드가 동시에 임계 영역을 사용함으로써 발생하는 문제
  • 해당 문제를 해결하기 위해서는 3가지 조건을 충족해야 함

 

3.1.1 임계 영역 문제를 해결하기 위해 충족해야 하는 조건들

  • Mutual Exclusion(상호 배제): 어떤 쓰레드가 임계 영역에서 작업 중일 경우 다른 쓰레드는 동일한 임계 영역에서 작업 못 함
  • Progress(진행): 임계 영역에서 작업 중인 쓰레드가 없고 임계 영역에 진입하련느 쓰레드가 있을 때 어떤 쓰레드가 들어갈 것인지 적절히 선택해 줘야 하며 이러한 결정은 무한정 미뤄져선 안됨 
  • Bounded Waiting(한정 대기): 다른 쓰레드가 임계 영역에서 작업하도록 요청한 후 해당 요청이 수락되기 전에 기존 쓰레드가 임계 영역에서 실행할 수 있는 횟수에 제한이 있어야 함 즉, Starvation이 발생하지 않도록 처리 필요

 

3.2 동기화 도구

 

  • Mutex, Semaphore, Monitor, CAS과 동기화 도구를 통해 3.1에서 소개한 임계 영역 문제가 발생하는 것을 방지 가능
  • 자바에서는 synchronized 키워드를 포함한 여러 동기화 도구들을 제공
    • ex) ReentrantLock, ReadWriteLock, CountDownLatch, CyclicBarrier, Semaphore, Exchanger

 

3.3 Race Condition

 

  • 여러 쓰레드가 동시에 공유 자원에 접근하고 조작할 때 쓰레드 간 액세스하는 순서나 시점에 따라 실행 결과가 달라질 수 있고 이를 Race Condition이라고 함
  • 즉, 여러 쓰레드가 동시에 임계 영역에 접근해 공유 데이터를 조작함으로써 발생하는 상태
  • 3.1에서 소개한 임계 영역 문제가 해결되지 않은 상태

 

3.3.1 Race Condition이 발생하는 코드


 

 

임계 영역에 대해 동기화 도구를 적용하지 않아 Race Condition이 발생하는 것을 확인할 수 있습니다.

 

3.3.2 동기화 도구를 적용해 Race Condition을 방지하는 코드


 

자바에서 제공하는 동기화 도구인 synchronized 키워드를 적용해 Race Condition을 방지하는 것을 확인할 수 있습니다.

 

4. 안전한 쓰레드 구성

  • 여러 쓰레드에서 클래스나 객체에 동시에 접근해 계속 실행하더라도 지속적인 정확성이 보장되는 코드를 thread-safe 하다고
  • thread-safe한 코드에는 Race Condition이 없으며 Race Condition은 다수의 쓰레드가 공유 자원에 쓰기 작업을 시도할 때 발생하기 때문에 공유 자원을 파악하고 공유 자원을 쓰는 임계영역에 동기화 도구를 적용해야 함

 

4.1 thread-safe한 구조를 위한 처리

 

  • 임계 영역을 동기화
  • 동기화 도구 사용
  • 쓰레드의 스택에 한정해서 상태 관리
  • 불변 객체 사용
  • ThreadLocal 사용

 

4.1.1 임계 영역을 동기화 + 동기화 도구 사용

  • 동시에 여러 개의 쓰레드가 임계 영역을 접근하지 못하도록 공유자원을 쓰는 영역에 동기화 도구 사용
  • 자바에서 제공하는 동기화 도구는 synchronized, ReentrantLock, ReadWriteLock, CountDownLatch, CyclicBarrier, Semaphore, Exchanger 등이 있음

 

4.1.2 쓰레드의 스택에 한정해서 상태를 관리

  • 지역변수
    • 기본형 지역 변수는 쓰레드마다 독립적으로 가지고 있는 스택에 저장되기 때문에 쓰레드 간에 공유될 수 없음 즉, thread-safe 함
    • 메서드로 전달되는 기본형 파라미터 변수도 스택에서만 관리되므로 thread-safe

 

  • 지역 객체 참조
    • 지역 변수라 할지라도 기본형과 달리 객체는 스택에 저장되지 않고 메모리의 힙 영역에 저장
    • 지역적으로 생성된 객체가 해당 메서드에서 벗어나지 않고 사용될 경우 쓰레드는 자신만의 객체를 참조할 수 있게 되어 thread-safe
    • 문자열 같이 불변 객체는 상태가 변경되지 않기 때문에 thread-safe
    • 지역 참조 변수를 다른 클래스의 메서드에 파라미터로 넘겼으 때 해당 클래스가 파라미터 변수를 다른 쓰레드가 접근할 수 있는 멤버 변수로 저장했을 경우에는 thread-safe 보장 못함(객체의 heap 영역은 여러 쓰레드에서 접근 가능)

 

  • 멤버 변수 참조
    • 쓰레드마다 객체를 생성하는 원리와 동일
    • 쓰레드의 스택 별로 객체가 생성되어 참조되도록 구현 필요

 

 

코드 부연 설명

  • Company 객체의 멤버 변수로 Member 객체를 참조
  • 각 쓰레드마다 새로운 객체를 파라미터로 전달할 경우 멤버 변수를 공유하지 않기 때문에 thread-safe
  • 각 쓰레드에 동일한 객체를 파라미터로 전달할 경우 멤버 변수인 member를 공유
    • 힙 영역은 모든 쓰레드가 접근 가능
    • Thread-2에서 member의 name 필드를 수정함에 따라 Thread-3의 member name 필드가 영향을 받은 것을 확인 가능

 

4.1.3 불변 객체 사용

  • effective java에서 강조했다시피 불변 객체를 사용하면 thread-safe함을 보장할 수 있음
  • 최초로 객체가 생성되는 시점 이후 어떠한 상황에서도 상태가 변하지 않기 위해 아래와 같은 작업을 적용할 수 있음 
    • setter 메서드 제거
    • 생성자에서 멤버 변수 초기화
    • 멤버 변수는 final로 선언

 

4.1.4 ThreadLocal 사용

  • ThreadLocal은 각 쓰레드에 대해 독립적인 저장 공간을 제공하기 때문에 여러 쓰레드에서 동시에 ThreadLocal을 접근하더라도 각 쓰레드의 데이터가 서로 영향을 미치지 않음

 

참고

자바 동시성 프로그래밍 [리액티브 프로그래밍 Part.1] - 정수원 강사님

반응형