Chapter 13 스레드 9~

Seunghee Ryu·2023년 10월 28일
0

자바의 정석

목록 보기
6/11

스레드의 동기화

  • 멀티스레드 프로세스의 경우 여러 스레드가 같은 프로세스 내의 자원을 공유해서 작업하기 때문에 서로의 작업에 영향을 주게 된다
  • 한 스레드가 특정 작업을 끝마치기 전까지 다른 스레드에 의해 방해받지 않도록 하는 것이 필요
  • 임계 영역과 잠금이라는 개념이 도입됨
  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해 놓고 공유 데이터(객체)가 가지고 있는 lock을 획들한 단 하나의 스레드만 이 영역 내의 코드를 수행할 수 있게 한다
  • 해당 스레드가 임계 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 스레드가 반납된 lock을 획득하여 임계 영역의 코드를 수행할 수 있게 된다
  • 한 스레드가 진행 중인 작업을 다른 스레드가 간섭하지 못하도록 막는 것을 스레드의 동기화라고 한다

synchronized를 이용한 동기화

// 1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum() {
}

// 2. 특정한 영역을 임계 영역으로 지정
// 참조변수는 락을 걸고자하는 객체를 참조하는 것이어야 한다
// synchronized 블럭이라고 부름
synchronized(객체의 참조변수) {
}

wait()과 notify()

  • 특정 스레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것도 중요
  • 동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면 wait()를 호출하여 스레드가 락을 반납하고 기다리게 한다
  • 작업을 진행할 수 있는 상황이 되면 notify()를 호출하여 중단했던 스레드가 다시 락을 얻어 작업할 수 있게 한다
  • 매개변수가 있는 wait()은 지정된 시간동안만 기다린다

기아 현상과 경쟁 상태

  • 통지를 받지 못하고 오랫동안 기다리게 되는 것을 기아 현상이라 한다
  • 이 현상을 막으려면 notify() 대신 notifyAll()을 사용해야 한다
    - 모든 스레드에게 통지를 하도록 함
  • notifyAll()로 기아 현상은 막았지만 lock을 얻기 위해 waiting pool의 모든 스레드가 경쟁하게 된다
    - 이것을 경쟁 상태라고 함
    - 경쟁 상태를 개선하기 위해 스레드를 구별해서 통지하는 것이 필요

Lock과 Condition을 이용한 동기화

  • 동기화 할 수 있는 방법은 lock 클래스들을 이용하는 방법도 있다
  • synchronized 블럭은 동기화를 하면 자동적으로 lock이 잠기고 풀리기 때문에 편리
  • 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 불편함
    - 이럴 때 lock 클래스 사용
  • ReentrantLock : 재진입이 가능한 lock
    - 가장 일반적
  • ReentrantReadWriteLock : 읽기에는 공유적이고 쓰기에는 배타적인 lock
    - 읽기를 위한 lock과 쓰기를 위한 lock을 제공
    - 읽기 lock은 중복해서 걸고 읽을 수 있음
    - 읽기 lock이 걸린 상태에서 쓰기 lock은 수행할 수 없다
    - 쓰기 lock이 걸린 상태에서 읽기 lock은 수행할 수 없다
  • StampedLock :ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가
    - lock을 걸거나 해지할 때 스탬프(long 타입의 정수값)를 사용
    - 읽기와 쓰기를 위한 lock 외에 낙관적 읽기 lock이 추가된 것
    - 낙관적 읽기 lock은 쓰기 lock에 의해 바로 풀린다
    - 낙관적 읽기에 실패하면 읽기 lock을 얻어서 다시 읽어와야 한다
    - 즉 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 건다

ReentrantLock의 생성자

  • 두개의 생성자를 가진다
ReentrantLock()
ReentrantLock(boolean fair)
  • 생성자의 매개변수를 true로 주면 lock이 풀렸을 때 가장 오래 기다린 스레드가 lock을 획득할 수 있게, 즉 공정하게 처리한다
    - 하지만 공정하게 처리하려면 어떤 스레드가 가장 오래 기다렸는지 확인해야 하기 때문에 성능이 떨어진다

ReentrantLock과 Condition

  • 스레드를 구분해서 통지하지 못한다는 단점을 해결하기 위해 Condition이 존재한다
  • 스레드의 종류를 구분하지 않고 공유 객체의 waiting pool에 같이 몰아넣는 대신, 스레드의 conditon을 만들어서 각각의 waiting pool에서 따로 기다리도록 한다
// lock으로 condition을 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();

// wait()와 notify() 대신 await()와 signal()을 사용한다
  • 기아 현상이나 경쟁 상태가 개선되었으나 스레드의 종류에 따라 구분하여 통지할 수 있게 된 것일 뿐 특정 스레드를 선택할 수는 없기 때문에 같은 종류의 스레드 간의 기아 현상이나 경쟁 상태는 발생할 가능성이 남아있다

volatile

  • 멀티 코어 프로세스는 코어마다 별도의 캐시를 가지고 있다
  • 코어는 메모리에서 읽어온 값을 캐시에 저장하고 캐시에서 값을 읽어서 작업한다
  • 같은 값을 읽어올 때는 먼저 캐시에 있는지 확인하고 엇을 때만 메모리에서 읽어온다
  • 도중에 메모리에 저장된 변수의 값이 바뀌었는데 캐시에 저장된 값이 갱신되지 않아서 메모리에 저장된 값이 다른 경우가 발생한다
  • 변수 앞에 volatile을 붙이면 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어오기 때문에 캐시와 메모리 간의 값의 불일치가 해결된다
  • synchronized 블럭을 사용해도 같은 효과를 얻을 수 있다

원자화

  • 작업을 더 이상 나눌 수 없게 한다는 의미
    - volitile은 해당 변수에 대한 읽거나 쓰기를 원자화 한다
  • 하나의 명령어는 더 이상 나눌 수 없는 최소의 작업 단위
  • JVM은 데이터를 4byte 단위로 처리하기 때문에 intdhk int보다 작은 타입들은 한 번에 읽거나 쓰는 것이 가능하다(하나의 명령어로 작업 완료 가능)
  • long과 double은 8byte이기 때문에 하나의 명령어로 값을 읽거나 쓸 수 없기 때문에 변수의 값을 읽는 과정에 다른 스레드가 끼어들 여지가 있다
  • 다른 스레드가 끼어들지 못하게 하려고 변수를 읽고 쓰는 모든 문장을 synchronized블럭으로 감쌀수도 있지만 변수를 선언할 때 volatile을 붙일수도 있다
  • 상수는 변하지 않는 값이므로 멀티 스레드에 안전하기 때문에 volatile을 붙일 필요가 없다

fork & join 프레임웍

  • 하나의 작업을 작은 다위로 나눠서 여러 스레드가 동시에 처리하는 것을 쉽게 해준다
  • RecursiveAction : 반환값이 없는 작업 구현
  • RecursiveTask : 반환값이 있는 작업 구현
  • 두 클래스 모두 compute()라는 추상 메서드를 가지고 있는데 상속을 통해 추상 메서드를 구현한다
  • fork()는 작업을 스레드의 작업 큐에 넣는 것이다
    - 작업 큐에 들어간 작업은 더 이상 나눌 수 없을 때까지 compute()로 나뉜다
  • 작업의 결과는 join()을 호출해서 얻을 수 있다
  • 작업 큐에 추가된 작업도 나눌 수 있다
    - 이것을 통해 한 스레드에 작업이 몰리지 않고 여러 스레드가 골고루 작업을 나누어 처리할 수 있게 한다
  • fork()는 비동기 메서드이고 join은 동기 메서드이다
    - 비동기 메서드는 메서드를 호출만 할 뿐 결과를 기다리지 않는다

0개의 댓글