쓰레드 part3 (동기화)

Shaun·2021년 9월 11일
0

JAVA

목록 보기
20/30

쓰레드의 동기화

  • 한 쓰레드가 특정 작업을 끝마치기 전가지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 '임계영역' 과 '잠금(락,lock)'이다.

  • 공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock를 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 쓰레드가 작업을 마치고 임계 영역을 벗어 나는 순간 lcok를 반납다른 쓰레드가 반납된 lock 을 획득 하여 임계 영역에 들어갈수 있게 한다.

  • 이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화 라 한다.

synchronized 를 이용한 동기화

  1. 첫번쨰 방법은 메서드 앞에 synchronized 를 붙이는 방법이다. synchronized를 붙이면 메서드 전체가 임계 영역으로 설정된다.

    public synchronized void calcSum(){
    //임계 영역//
    }

  1. 두번째 방법은 메서드 내의 코드 일부를 블럭{} 으로 감싸고 블럭 앞에
    synchronized(객체의 참조변수) 를 붙이는 방법
    이다. 이 블럭의 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 lock를 얻게 되고, 이블럭을 벗어나면 lock를 반납한다.

    synchronized(객체의 참조변수){
    //임계영역
    }

->임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 떄문에 가능하면 메서드 전체에 락을 거는것보다 synchronized블럭으로 임계 영역을 최소화 시키는게 좋다.

ex 1



  • 코드를 보면 잔고가 출금하려는 금액보다 큰경우에만 출금 하도록 설정 되어있다. 하지만 막상 실행해보면 잔고가 음수인 것을 알수 있다.

  • 그이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 떄문이다.

  • Account 클래스의 인스턴스 변수인 balance의 접근 제어자가 private 라는 점을 주의하자. private가 아니면 외부에서 직접 접근할수 있기 때문에 아무리 동기화를 해도 이 값의 변경을 막을 길이 없다.

-> 한 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.

-> 한 쓰레드에 의해서 먼저 withdraw()가 호출 되면, 이메서드가 종료되어 lock이 반납될떄까지 다른 쓰레드는 withdraw()를 호출 하더라도 대기상태에 머물게 된다.

wait() 와 notify()

  • 동기화 까지는 좋은데 특정 쓰레드가 lock을 너무 오래 가지고 있으면 문제가 발생한다.

  • 동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출 하여 쓰레드가 락을 반납하고 기다리게 한다.

  • 나중에 notify() 를 통해 작업을 중단 했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있게 한다.( 임계영역에 다시 들어옴 =재진입)
  • wait() 가 호출되면 해당 쓰레드는 객체의 대기실(waiting pool) 에서 통지를 기다린다. notifyAll()은 기다리고 있는 모든 쓰레드에게 통보하지만, 그래도 lock를 얻을 수 있는 것은 하나의 쓰레드 일뿐이다.(나머지는 다시 기다리는 신세)

void wait()
void wait(long timeout) //매개변수 시간동안만 기다린다.
void wait(long timeout,int nanos)
void notify()
void notifyAll() //모든 쓰레드 다깨움(lock은 하나)

  • wating pool 은 객체마다 존재하는 것이므로 notifyAll 이 호출 되더라도 모든 객체의 wating pool 에있는 쓰레드가 꺠워지는 것이 아니다. notifyAll() 이 호출된 객체의 wating pool에 대기중인 쓰레드만 해당.

Lock 클래스

Lock와 condition을 이용한 동기화

  • 동기화 할수있는 방법은 synchronized 블럭외에도 lock 클래스들을 이용하는 방법이 있다.

  • synchronized는 자동적으로 lock이 잠기고 풀리기 떄문에 편리하다. synchronized 블럭내에서 예외가 발생해도 lock은 자동적으로 풀린다.

  • 하지만 같은 메서드 내에서만 lock을 걸수있다. -> lock클래스를 이용하자

ReentrantLock = 재진입이 가능한 lock, 가장 일반적인 베타 lock
ReentrantReadWriteLock = 읽기에는 공유적이고 , 쓰기에는 베타적인 lock
StampedLock = ReentrantReadWriteLock에 낙관적인 lock의 기능 추가

1.ReentrantLock

  • 가장 일반적인 lock.

  • wait(), notify()에서 배운것처럼, 특정 조건에서 lock을 풀고 다시 lock을 얻고 임계 영역에 들어와서 작업을 수행할수 있다.

2.ReentrantReadWriteLock

  • 읽기를 위한 lock쓰기를 위한 lock를 제공한다.

  • 읽기 lock가 걸려있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할수 있다. 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드에서 읽어도 문제가 되지 않기 떄문

  • 그러나 읽기 lock가 걸린 상태에서 쓰기lock 는 허용 x

-> 읽기 할때 읽기 lock, 쓰기 할떄 쓰기lock

3.StampedLock

  • lock 을 걸거나 해지할떄 스탬프 를 사용하여 읽기와 쓰기를 위한 lock외에 낙관적 읽기 가 추가된 것이다.

  • 읽기 lock가 걸려있으면 쓰기 lock를 얻기위해 읽기 lock 가 풀릴 떄까지 기다려야 하는데 비해 낙관적 읽기 는 쓰기 lock에 의해 바로 풀린다. 그래서 낙관적 읽기에 실패하면, 읽기 lock를 다시 얻어와 읽어야 한다.

->무조건 읽기 lock을 걸지 않고 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.

ReentrantLock 생성자

ReentrantLock()
ReentrantLock(boolean fair)

  • 매개변수를 true 를 주면 가장 오래 기다린 쓰레드가 lcok을 획득한다. 하지만 어떤쓰레드가 오래 기다렸는지 계산하므로 성능은 떨어진다.

void lock() //lock을 잠근다
void unlock() //lock 해지
boolean isLocked() //lock 잠겼는지 확인

  • synchronized 는 자동으로 lock 가 열고 닫히지만, lock 클래스는 수동이다.

  • 임계 영역 내에서 예외가 발생하거나 retrun 문으로 빠져나가게 되면 lock 가 풀리지 않을수 있으므로(synchronized는 자동) unlock()는 try-finally문으로 감싸는것이 일반적이다.

lock.lock()
try{
//임계영역//
} finally{
lock.unlock()
}

tryLock()

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

  • 이메서드는 lock()와 달리 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않는다.

  • 또는 지정된 시간만큼만 기다린다.

  • lock을 얻으면 true를 반환하고, 얻지 못하면 false를 반환한다.

-> lock()는 lock을 얻을 떄까지 쓰레드를 블락 처리시키므로 쓰레드의 응답성이 나빠질수 있다. 응답성이 중요한 경우, tryLock()를 이용해서 지정된 시간동안 lock를 얻지 못하면 다시 작업을 시도할 것인지를 사용자가 결정할 수 있게 하는 것이 좋다.

-> InterruptedException = 지정된 시간동안 lock을 얻으려고 기다리는 중에 interrupt()에 의해 작업을 취소될 수 있도록 한것.

ReentrantLock 와 Condition

  • wait() 와 notify() 를 사용하면 쓰레드의 종류를 구분하지 않고 waiting pool 에 넣는 문제가 발생

  • A쓰레드를 위한 Condition 과 B쓰레드를 위한 Condition 각각 만들어 wating pool 에서 구분해서 기다리게 하자.

private ReentrantLock lock = new ReentrantLock(); // lock 생성
private Condition 쓰레드A = lock.newCondition();
private Condition 쓰레드B = lock.newCondition();

  • Condition 은 이미 생성된 lock로부터 new Condition() 을 호출해서 생성

  • Condition 을 사용하면 wait() 와 notify() 대신에 await() signal() 을 사용한다.

쓰레드A.await() -> 쓰레드A를 기다리게 한다
쓰레드B.signal() -> 쓰레드 B를 꺠운다.

profile
호주쉐프에서 개발자까지..

0개의 댓글