JAVA 4편 - 동기화와 데드락

Jino·2022년 12월 15일
0

JAVA

목록 보기
5/14
➡️ 작성일 : 2022.11.02

동기화 문제

  • 멀티쓰레드 프로세스의 경우 여러 쓰레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다.
    • 여러 쓰레드가 동일한 자원 접근 시 동기화 이슈 발생
    • 쓰레드A 가 작업을 하던 도중 다른 쓰레드B 에게 제어권이 넘어갔을 떄 쓰레드A가 작업하던 공유 데이터를 쓰레드B 가 임의로 변경하였다면
      다시 쓰레드A 가 제어권을 받아서 나머지 작업을 마쳤을 떄
      원래 의도했던것과 다른 결과를 얻을 수 있다.

synchronization 쓰레드의 동기화

  • 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는것
  • 동기화 문제를 해결하기 위해 도입된 개념
    • critical section 임계영역
    • lock 잠금 (락)
  • 공유 데이터를 사용하는 코드 영역을 임계영역으로 설정하고
    공유데이터가 가지고 있는 lock 을 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행 가능
    그동안 다른 쓰레드들은 기다리게 되고
    lock 을 가지고 작업중이던 쓰레드가 임계영역 내의 코드를 모두 수행하고 난 후 lock 을 반납하면
    다른 쓰레드가 반납된 lock 을 획득하여 임계영역을 수행

동기화를 하는 방법


기본방법

  • synchronized 사용하는 방법
    // 방법 1 : 메서드 전체를 임계영역으로 설정
    public   **synchronized**   void useBuy(int price){}
    
    // 방법 2 : 특정 영역을 임계영역으로 설정
    public void useBuy(int price){
    	**synchronized ( 락을 걸고자 하는 객체의 참조변수 ) {** 
    	
    	**}**
    }

    임계영역설정은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에
    가능하면 메서드 전체에 락을 거는 방식보다 synchronized 블록을 통해 임계영역을 최소화 해
    효율적인 프로그램이 되도록 노력해야한다.

    • 특정 쓰레드가 객체의 락을 가진 상태로 오랜시간을 보내지 않도록 해야한다.
      • 이 경우 lock 이 반납 될 때까지 다른 쓰레드들은 작업이 원활이 진행되지 않는다.
    • wait(), notify(), notifyAll() 을 통해 조절
      • Object 에 정의되어있다.
      • 동기화 블럭 내에서만 사용할 수 있다.
      • 보다 효율적인 동기화를 가능케한다.
      • 단 오래 기다린 쓰레드가 락을 얻는다는 보장은 없다.
      • wait()
        • 쓰레드가 lock 을 반납하고 기다리게 한다.
        • 실행중이던 쓰레드는 해당 객체의 대기실(wating pool) 에서 통지를 기다린다.
      • notify()
        • 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있도록 한다.
        • 해당 객체의 대기실에 있던 모든 쓰레드 중에서 임의의 쓰레드만 통지를 받는다.
      • notifyAll()
        • 기다리는 모든 쓰레드에게 통지한다.
        • 그러나 lock 을 얻을 수 있는것은 하나의 쓰레드이므로
          나머지 쓰레드는 통보를 받았지만 lock 을 얻지 못하여 다시 lock 을 기다리게 된다.
  • 문제점
    • 오래 기다린 쓰레드가 락을 얻는다는 보장은 없다.
    • notify()와 notifyAll() 를 사용할 수 있지만 기아현상과 경쟁상태를 완벽히 해결할 수 없다.
    • notify()와 notifyAll() 는 단순히 일부 혹은 모든 쓰레드에게 lock 을 얻을 수 있다는 통지를 할 뿐 대상을 특정할 수 없다. → 결국 모든 쓰레드가 경쟁하게 된다.
    • 기아현상
      • 한 쓰레드가 키를 받지못해 오랫동안 기다리는 현상
    • 경쟁상태
      • lock 을 얻기위해 여러 쓰레드가 서로 경쟁하는 것

Lock 의 사용

  • java.util.concurrent.locks 패키지가 제공하는 lock 클래스들
  • 기존 synchronized 의 불편함을 개선
    • synchronized 블럭을 통해 동기화 할 시 자동으로 lock 이 잠기고 풀린다는 점
      (편리할수도 단점일수도)
    • synchronized블럭 내에서 예외가 발생하면 lock 은 자동으로 풀린다는 점
    • 같은 메서드 내에서만 lock 을 걸 수 있다는 제약의 불편함
  • 종류
    • ReentrantLock
      • 특정 조건에서 lock 을 얻고 임계영역에서 작업 수행
      • 기존의 lock 과 같은 방식
    • ReentranReedWriteLock
      • 읽기를 위한 lock 과 쓰기를 위한 lock 을 제공
      • 읽기 lock 이 걸려있을 시 다른 쓰레드가 읽기 lock 을 중복으로 걸고 읽기 수행 가능
      • 읽기 lock 이 걸린 상태에서 쓰기 lock 을 거는것은 허용되지 않는다.
      • 쓰기 lock 을 건 상태에서 읽기 lock 을 거는것 역시 허용되지 않는다.
    • StampedLock
      • lock 을 걸거나 해지할 시 스템프(long 타입의 정수값)을 사용
      • 낙관적 읽기 lock 개념이 추가
      • 기존의 읽기 lock 이 걸려있을 시 쓰기 lock 을 걸기 위해선 걸려있는 읽기 lock 이 풀릴때 까지 기다려야 함에 반해
        낙관적 읽기 lock 은 쓰기 lock 에 의해 읽기 lock 이 풀려버린다.
  • 특징
    • 기본이 되는 ReentrantLock 의 사용법을 이해 후 Java API 문서 참고
    • 생성자
    • void lock() : lock 을 잠근다
    • void unlock() : lock 을 해지한다
    • boolean isLocked() : lock 이 잠겨있는지 확인
    • boolean tryLock() : 다른 쓰레드에 의해 lock이 걸려있으면 지정된 시간만큼만 기다리고 결과에 따라 ture / false 를 반환

Condition 사용

  • 대기중인 쓰레드의 종류를 구분하지 못하는 이유로 발생하는 경쟁상태를 어느정도 해소할 수 있는 방법
    private ReentrantLock lock = new ReentrantLock();
    
    // lock 으로 condition 을 생성
    private Condition useConditionA = lock.newCondition();
    private Condition useConditionB = lock.newCondition();
    private Condition useConditionC = lock.newCondition();
    • 여전히 특정 쓰레드 선택 불가로 인한 기아현상, 경쟁상태가 발생 가능성은 있음
    • Condition 을 세분화 할수록 발생가능성은 줄어든다.
  • waiting pool 을 각각 쓰레드의 종류별로 생성하여 그곳에서 대기할 수 있도록 한다.
    • await() : 기존의 wait() 처럼 쓰레드 대기
    • signal() : 기존의 notify() 처럼 대기쓰레드에게 통지

volatile 키워드

  • volatile 키워드는 변수의 읽기나 쓰기를 원자화하는것
    • 단 동기화하는것이 아니라는 것에 주의
  • 멀티코어프로세서의 코어마다 별도의 캐시를 지니고 있으므로 데이터를 조회시 메모리의 데이터가 아닌 캐시의 데이터를 읽어 오작동 하는 경우가 있다.
  • volatile 키워드는 값을 캐시가 아닌 데이터에서 읽어오도록 하여 메모리간 불일치 문제를 해결한다. (synchronized 블럭 역시 들어갈때와 나올때 캐시와 메모리간의 동기화가 이루어지므로 값의 불일치가 해소 되는 효과가 있다.)
  • JVM 이 데이터를 4바이트 단위로 처리하기에 long , double 과 같은 큰 변수는 하나의 명령어로 처리할 수 없어 다른 쓰레드가 끼어들 가능성이 존재
    • 이를 volatile 키워드를 사용하면 변수에 대한 원자화가 진행되어 다른 쓰레드가 끼어드는것을 방지할 수 있다.
    • 원자화 : 작업의 단위를 더이상 쪼갤 수 없게 하는것

fork & join 프레임워크

  • 멀티 코어를 더 잘 활용할 수 있도록 멀티쓰레드 프로그래밍의 중요성에 따라 추가
  • 하나의 작업을 작은 단위로 나눠서 여러 쓰레드가 동시에 처리하는것을 쉽게 수행할수 있도록 한다.
  • ForkJoinPool 쓰레드풀을 사용
    • fork & join 프레임워크에서 제공하는 쓰레드풀
    • 쓰레드를 반복해서 생성하지 않아도 된다는 장점
      • 지정된 수의 쓰레드를 생성해서 미리 만들어놓고 반복해서 재사용이 가능
    • 너무 많은 쓰레드가 생성되어 성능이 저하되는것을 막아준다는 장점
    • 쓰레드가 수행해야하는 작업이 담긴 큐 제공
      • 각 쓰레드는 자신의 작업 큐에 담긴 작업을 순서대로 처리
  • 종류
    • 수행할 작업에 따라 두 클래스 중하나를 상속받아 추상메서드를 구현하여 사용
      • RecursiveAction : 반환값이 없는 작업을 구현할 때 사용
      • RecursiveTask : 반환값이 있는 작업을 구현할 때 사용
  • 구현과정
    1. 클래스 상속 및 추상메서드(compute()) 구현
      • compute() 의 구현 시 수행할 작업과 함께 작업을 어떻게 나눌것인가에 대해서도 지정해주어야한다.
      • 한쪽은 fork()로 작업범위를 분기 및 쓰레드풀에 추가하고 다른쪽은 compute() 를 재귀호출하며 수행하게 된다.
    2. 쓰레드풀 생성
    3. 수행할 작업 생성
    4. invoke() 로 작업 수행

Deadlock 교착상태

  • 무한 대기 상태
  • 두개 이상의 작업이 서로 상대방의 작업이 끝나기만을 기다리는 상태로 인해
    다음 단계로 진행하지 못하는 현상
  • 데드락의 발생 조건
    • 데드락은 4가지의 발생 조건이 있고 해당 조건을 모두 충족해야 발생한다.
    • 발생조건 4가지 중 하나라도 성립하지 않으면 발생하지 않는다. (발생조건을 회피하는 방법으로 해결이 가능하다)
    1. 상호 배제 Mutual Exclusion
      • 한 자원에 대해 여러 쓰레드 동시 접근 불가
    2. 점유 대기 Hold and Wait
      • 자원을 가지고 있는 상태에서 다른 쓰레드가 사용하고 있는 자원 반납을 기다리는 것
    3. 비선점 Non Preemptive
      • 다른 쓰레드의 자원을 실행 중간에 강제로 가져올 수 없음
    4. 순환 대기 Circle Wait
      • 각 쓰레드가 순환적으로 다음 쓰레드가 요구하는 자원을 가지고 있는것

  • 참고
    • 남궁성 - 자바의 정석
profile
방대한 백엔드의 바다에서 착실히 습득하는 유망주 J

0개의 댓글