[4주차] 동시성 기초

goodstar·2024년 11월 18일
0

Java 스터디

목록 보기
10/14
post-thumbnail

1. 동시성과 병렬성의 차이점

  • 동시성 (Concurrency):
    • 여러 작업을 동시에 처리하는 것처럼 보이도록 만드는 개념.
    • 멀티스레드 환경에서 각 스레드가 시간 단위로 번갈아 가며 실행됨.
    • 예: 싱글코어 프로세서에서 멀티태스킹.
      하나의 스레드가 I/O 작업을 처리하는 동안 다른 스레드가 CPU 작업을 처리하는 경우. 비디오 스트리밍 서비스에서, 화면 렌더링과 네트워크 데이터 전송을 동시에 진행.
  • 병렬성 (Parallelism):
    • 여러 작업을 실제로 동시에 처리하는 개념.
    • 멀티코어 프로세서에서 각 작업이 별도 코어에서 실제로 동시에 실행됨.
    • 예: 듀얼코어 CPU에서 두 개의 작업이 동시에 실행. 이미지 처리 프로그램에서, 각 코어가 이미지의 서로 다른 부분을 병렬로 처리.

2. Thread-Safe하다는 것의 의미

  • Thread-Safe란:
    • 여러 스레드가 동시에 접근하더라도 데이터의 일관성정확성(무결성)이 보장되는 상태.
  • 예시:
    • Java의 StringBuffer는 동기화 메커니즘을 사용해 여러 스레드가 동시에 접근해도 안전함.
    • 반면, StringBuilder는 동기화가 없어서 Thread-Safe하지 않음.
  • 구현방법:
    • 동기화 (synchronized 키워드)
    • Lock 인터페이스 사용
    • Thread-Safe 클래스 (StringBuffer, Vector)
    • 동시성 컬렉션 (ConcurrentHashMap, CopyOnWriteArrayList)
    • 원자적 클래스 (AtomicInteger, AtomicLong 등)
    • 불변 객체 사용

3. 가시성 문제와 원자성 문제

  • 가시성 문제:
    • 한 스레드에서 변경한 값을 다른 스레드가 즉시 확인하지 못하는 문제를 의미합니다.
    • 원인: CPU 캐싱 또는 JVM 최적화로 인해, 메모리에 저장된 값이 모든 스레드에서 일관되게 동기화되지 않기 때문입니다.
    • 여러 개의 스레드가 사용됨에 따라, CPU 캐시 메모리와 RAM 간의 데이터 불일치로 발생하며, 이를 해결하려면 특정 변수가 항상 RAM에서 직접 읽히도록 해야 합니다.
  • 원자성 문제:
    • 하나의 작업이 완전히 실행되거나, 전혀 실행되지 않는 상태를 보장하지 못할 때 발생합니다.
    • 예를 들어, count++는 단순한 증가 연산처럼 보이지만, 내부적으로는 read-modify-write 과정이 포함되어 있습니다. 이 과정 중 다른 스레드가 접근하면 충돌이 발생하여 정확한 결과를 보장하지 못하는 문제가 발생합니다.

      원자성이란 특정 작업이 더 이상 쪼갤 수 없는 하나의 단위로 실행되는 성질을 의미합니다.
      즉, 작업이 모두 실행되거나 전혀 실행되지 않는 상태(실패 시 아무 변화도 없는 상태로 복구되는 것)를 보장하는 것을 말합니다.


4. 자바의 동시성 이슈

동시성 프로그래밍(여러 작업을 동시에 처리할 수 있도록 설계된 프로그램을 개발하는 방식)에서는 CPU와 RAM의 중간에 위치하는 CPU Cache Memory와 병렬성이라는 특징때문에 다수의 스레드가 공유 자원에 접근할 때 두 가지 문제가 발생할 수 있다.

  • 가시성 문제
  • 원자성(동시 접근) 문제

해결 방법

자바에서는 이런 문제를 해결하기 위해 여러 가지 도구를 제공합니다:

  1. volatile:

    • 가시성 문제를 해결합니다. 변수 값을 직접 메모리에 쓰고 읽도록 보장합니다.
  2. synchronized:

    • 특정 코드 블록이나 메서드에 락을 걸어, 한 번에 한 스레드만 접근하도록 합니다. ( 가시성, 원자성 문제 해결 )
  3. Atomic 클래스:

    • 원자성을 보장하는 클래스 (AtomicInteger, AtomicLong 등)를 사용합니다.
  4. Lock 인터페이스:

    • 고급 락(ReentrantLock, ReadWriteLock 등)을 활용해 정교한 락 관리가 가능합니다.

Lock 인터페이스란?
Lock 인터페이스는 자바에서 스레드 간의 자원 접근을 제어하기 위해 사용됩니다. 기존의 synchronized 키워드와 비교해, 다음과 같은 특징을 가집니다:

  • 락 획득과 해제를 명시적으로 처리 (lock()unlock() 메서드 사용).
  • 락 대기 시간 설정 가능 (tryLock() 메서드).
  • 조건 변수를 사용하여 보다 정교한 스레드 간 통신 가능 (Condition 객체).
  • 성능 최적화 가능.

5. volatile 키워드

  • 역할:
    • 변수의 가시성 문제를 해결.
    • 한 스레드에서 값을 변경하면, 모든 스레드가 즉시 해당 변경을 볼 수 있음.
  • 특징:
    • 변수는 항상 메인 메모리에서 읽고 쓰이도록 보장.
    • 동기화는 제공하지 않음 (원자성 문제 해결 불가).

6. synchronized 키워드

  • 역할:

    • 임계 영역을 설정해 스레드 간 충돌을 방지.
    • 한 스레드가 해당 블록에 접근 중이면 다른 스레드는 대기.
    • Lock을 획득한 스레드만 임계 영역 실행.

      "임계영역(Critical Section)" : 멀티스레드 프로그래밍에서 공유 자원(Shared Resource)에 대해 동시에 여러 스레드가 접근하면 안 되는 코드 블록

  • 사용 방법:

    1. 메서드 동기화: 메서드 전체에 대해 락을 적용합니다.
      public synchronized void increment() {
          count++;
      }
    2. 블록 동기화: 특정 코드 블록에 대해 락을 적용합니다.
      public void increment() {
          synchronized (this) {
              count++;
          }
      }

7. synchronized의 문제점

  1. 성능 저하:
    • 락 획득과 해제 과정이 비용이 많이 들며, 다수의 스레드가 경쟁할 경우 대기 시간이 길어질 수 있습니다.
  2. 교착 상태(Deadlock):
    • 여러 스레드가 서로 다른 락을 대기하면서 순환 대기가 발생할 수 있습니다. (스레드 A와 스레드 B가 있고, 두 스레드가 두 개의 락(Lock1, Lock2)을 사용해야 작업을 완료할 수 있습니다.)
  3. 비공정성:
    • 특정 스레드가 Lock을 독점할 가능성이 있다.

synchronized 키워드는 동시성 제어를 위해 락을 사용하는데, 락은 다음과 같은 이유로 성능 저하를 유발합니다:

  1. Thread Contention(스레드 경합):
    • 여러 스레드가 동일한 락을 얻기 위해 경쟁합니다.
    • 대기 시간이 발생하며, context switching 비용도 추가됩니다.
  2. 락 대기 정책:
    • 락을 획득하지 못한 스레드는 대기 상태로 전환됩니다.
    • 이는 운영 체제의 스케줄링 정책에 따라 다르며, 추가적인 오버헤드가 있습니다.
  3. 모니터와 대기 큐:
    • synchronized는 내부적으로 모니터(Monitor)라는 객체를 사용하며, 락 대기 스레드가 모니터의 대기 큐에서 대기합니다.
    • 락을 해제하고 스레드가 깨어나는 과정에서 시간이 소요됩니다.

해결방법

1. 성능 저하

  • ReentrantLock 사용: ReentrantLock은 락을 세밀하게 제어할 수 있도록 도와주며, 필요에 따라 tryLock()을 활용해 타임아웃을 설정하거나, 공정 모드를 활성화해 성능을 최적화할 수 있습니다.
  • CAS(non-blocking): AtomicInteger, AtomicReference와 같은 원자 클래스(Atomic Classes)를 활용해 락 없이도 스레드 안전하게 데이터를 수정할 수 있습니다.
  • 락 범위 최소화: 동기화 블록을 필요한 최소 범위로 제한해 불필요한 대기를 줄이고 성능을 개선합니다.

2. 교착 상태 (Deadlock)

  • 락 순서 고정: 스레드가 여러 락을 요청할 경우 항상 동일한 순서로 요청하도록 설계해 교착 상태를 예방합니다.
  • 타임아웃 설정: 락을 일정 시간 동안만 요청하고, 요청에 실패하면 다른 작업을 수행하거나 실패 처리를 합니다. 이를 통해 교착 상태로 무한히 대기하는 상황을 방지합니다.
  • 자원 선점 방지: 락 요청 시 모든 락을 획득할 수 없다면 이미 소유한 락을 해제하고 다시 요청합니다. 이렇게 하면 순환 대기가 발생하지 않습니다.

3. 비공정성

  • ReentrantLock의 공정 모드: ReentrantLock의 공정 모드를 활성화하면 락 요청 순서에 따라 락을 배정해 특정 스레드가 계속 대기하지 않도록 합니다.
ReentrantLock fairLock = new ReentrantLock(true); // 공정성 활성화
  • 락 없는 동기화 데이터 구조 사용: ConcurrentHashMap이나 ConcurrentLinkedQueue와 같은 락 없는 스레드 안전 데이터 구조를 활용해 공정성과 성능을 동시에 확보할 수 있습니다.

8. synchronized는 어떻게 구현되어 있나요?

synchronized의 작동 방식

Java에서 synchronized는 다음 과정을 통해 동작하며, 내부적으로는 JVM에서 이를 관리합니다.
(1) 모니터 락(Monitor Lock)

  • 모니터 락은 객체 단위로 관리됩니다. 모든 객체에는 고유한모니터(monitor)가 존재하며, synchronized는 해당 객체의 모니터 락(모니터 내부의 락) 을 점유함으로써 동기화된 코드 블록에 대한 접근을 제어합니다.
  • 한 스레드가 모니터 락을 획득하면, 다른 스레드는 해당 락이 해제될 때까지 대기 상태에 들어갑니다.

(2) 바이트코드와 모니터 엔트리/출구

  • Java 컴파일러는 synchronized 키워드를 만나면 해당 부분을 바이트코드의 monitorentermonitorexit 명령으로 변환합니다.
    • monitorenter: 락을 획득합니다.
    • monitorexit: 락을 해제합니다.
  • JVM은 이 명령어를 통해 스레드 간 락을 관리합니다.

(3) 구현의 주요 원리

  • 락 관리: JVM 내부적으로 객체 헤더(Object Header)에 저장된 모니터 정보를 기반으로 락 상태를 추적합니다.
  • 대기 큐: 락을 획득하지 못한 스레드는 모니터에 있는 대기 큐(Wait Queue)에 추가됩니다.
  • 락 해제: 락을 소유한 스레드가 monitorexit를 호출하면, 대기 큐에 있는 다른 스레드가 락을 획득할 기회를 얻습니다.

객체 헤더(Object Header)는 JVM이 객체를 관리하는 데 필요한 정보를 저장하는 메타데이터입니다. 일반적으로 다음과 같이 구성됩니다.
Mark Word: 객체의 상태(락, 해시 코드, GC 정보 등)를 저장. synchronized의 락 상태 관리에 사용됩니다.
클래스 포인터: 객체가 어떤 클래스의 인스턴스인지 가리키는 참조.
Array Length (배열 객체에만 해당): 배열의 길이를 저장.


9. atomic하다는 것의 의미

  • atomic은 작업이 더 이상 분리할 수 없는 단위로 처리된다는 의미.
  • 특징:
    • 작업이 중간 상태 없이 완전히 실행되거나, 전혀 실행되지 않는 것을 보장
  • 예시:
    • count++는 atomic하지 않음 (read, modify, write로 분리).

10. Atomic 타입이란?

Atomic 타입스레드 안전성(thread-safety)을 보장하면서 연산이 "atomic"하게 이루어지도록 설계된 데이터 타입입니다. Java에서는 java.util.concurrent.atomic 패키지에 포함된 클래스들이 대표적인 atomic 타입에 해당합니다.

주요 Atomic 클래스

  1. AtomicInteger: 정수형 데이터를 atomic하게 다루는 클래스.
  2. AtomicLong: Long 타입 데이터를 atomic하게 다루는 클래스.
  3. AtomicBoolean: Boolean 타입 데이터를 atomic하게 다루는 클래스.
  4. AtomicReference<V>: 참조형 데이터를 atomic하게 다루는 클래스.

Atomic 타입의 특징

  1. Lock-Free: Atomic 타입은 내부적으로 CAS(Compare-And-Swap)를 사용하여 락 없이 스레드 간 데이터 동기화를 제공합니다.
  2. Thread-Safe: 여러 쓰레드가 동시에 접근해도 데이터의 일관성을 보장합니다.
  3. 중단 불가(Indivisible): 어떤 연산도 중간에 다른 쓰레드가 끼어들 수 없으며, 연산이 완료된 상태만 보장합니다.

0개의 댓글

관련 채용 정보