한 쓰레드가 특정 작업을 끝마치기 전가지 다른 쓰레드에 의해 방해받지 않도록 하는 것이 필요하다. 그래서 도입된 개념이 '임계영역' 과 '잠금(락,lock)'이다.
공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정해놓고, 공유 데이터(객체)가 가지고 있는 lock를 획득한 단 하나의 쓰레드만 이 영역 내의 코드를 수행할 수 있게 한다. 그리고 쓰레드가 작업을 마치고 임계 영역을 벗어 나는 순간 lcok를 반납해 다른 쓰레드가 반납된 lock 을 획득 하여 임계 영역에 들어갈수 있게 한다.
이처럼 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것을 쓰레드의 동기화 라 한다.
public synchronized void calcSum(){
//임계 영역//
}
synchronized(객체의 참조변수){
//임계영역
}
->임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 떄문에 가능하면 메서드 전체에 락을 거는것보다 synchronized블럭으로 임계 영역을 최소화 시키는게 좋다.
코드를 보면 잔고가 출금하려는 금액보다 큰경우에만 출금 하도록 설정 되어있다. 하지만 막상 실행해보면 잔고가 음수인 것을 알수 있다.
그이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 바로 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 떄문이다.
Account 클래스의 인스턴스 변수인 balance의 접근 제어자가 private 라는 점을 주의하자. private가 아니면 외부에서 직접 접근할수 있기 때문에 아무리 동기화를 해도 이 값의 변경을 막을 길이 없다.
-> 한 쓰레드의 작업이 다른 쓰레드에 의해서 영향을 받는 일이 발생할 수 있기 때문에 동기화가 반드시 필요하다.
-> 한 쓰레드에 의해서 먼저 withdraw()가 호출 되면, 이메서드가 종료되어 lock이 반납될떄까지 다른 쓰레드는 withdraw()를 호출 하더라도 대기상태에 머물게 된다.
동기화 까지는 좋은데 특정 쓰레드가 lock을 너무 오래 가지고 있으면 문제가 발생한다.
동기화된 임계영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면, 일단 wait()을 호출 하여 쓰레드가 락을 반납하고 기다리게 한다.
void wait()
void wait(long timeout) //매개변수 시간동안만 기다린다.
void wait(long timeout,int nanos)
void notify()
void notifyAll() //모든 쓰레드 다깨움(lock은 하나)
동기화 할수있는 방법은 synchronized 블럭외에도 lock 클래스들을 이용하는 방법이 있다.
synchronized는 자동적으로 lock이 잠기고 풀리기 떄문에 편리하다. synchronized 블럭내에서 예외가 발생해도 lock은 자동적으로 풀린다.
하지만 같은 메서드 내에서만 lock을 걸수있다. -> lock클래스를 이용하자
ReentrantLock = 재진입이 가능한 lock, 가장 일반적인 베타 lock
ReentrantReadWriteLock = 읽기에는 공유적이고 , 쓰기에는 베타적인 lock
StampedLock = ReentrantReadWriteLock에 낙관적인 lock의 기능 추가
가장 일반적인 lock.
wait(), notify()에서 배운것처럼, 특정 조건에서 lock을 풀고 다시 lock을 얻고 임계 영역에 들어와서 작업을 수행할수 있다.
읽기를 위한 lock와 쓰기를 위한 lock를 제공한다.
읽기 lock가 걸려있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할수 있다. 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드에서 읽어도 문제가 되지 않기 떄문
그러나 읽기 lock가 걸린 상태에서 쓰기lock 는 허용 x
-> 읽기 할때 읽기 lock, 쓰기 할떄 쓰기lock
lock 을 걸거나 해지할떄 스탬프 를 사용하여 읽기와 쓰기를 위한 lock외에 낙관적 읽기 가 추가된 것이다.
읽기 lock가 걸려있으면 쓰기 lock를 얻기위해 읽기 lock 가 풀릴 떄까지 기다려야 하는데 비해 낙관적 읽기 는 쓰기 lock에 의해 바로 풀린다. 그래서 낙관적 읽기에 실패하면, 읽기 lock를 다시 얻어와 읽어야 한다.
->무조건 읽기 lock을 걸지 않고 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.
ReentrantLock()
ReentrantLock(boolean fair)
void lock() //lock을 잠근다
void unlock() //lock 해지
boolean isLocked() //lock 잠겼는지 확인
synchronized 는 자동으로 lock 가 열고 닫히지만, lock 클래스는 수동이다.
임계 영역 내에서 예외가 발생하거나 retrun 문으로 빠져나가게 되면 lock 가 풀리지 않을수 있으므로(synchronized는 자동) unlock()는 try-finally문으로 감싸는것이 일반적이다.
lock.lock()
try{
//임계영역//
} finally{
lock.unlock()
}
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
이메서드는 lock()와 달리 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않는다.
또는 지정된 시간만큼만 기다린다.
lock을 얻으면 true를 반환하고, 얻지 못하면 false를 반환한다.
-> lock()는 lock을 얻을 떄까지 쓰레드를 블락 처리시키므로 쓰레드의 응답성이 나빠질수 있다. 응답성이 중요한 경우, tryLock()를 이용해서 지정된 시간동안 lock를 얻지 못하면 다시 작업을 시도할 것인지를 사용자가 결정할 수 있게 하는 것이 좋다.
-> InterruptedException = 지정된 시간동안 lock을 얻으려고 기다리는 중에 interrupt()에 의해 작업을 취소될 수 있도록 한것.
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를 꺠운다.