쓰레드 동기화 : 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는것
쓰레드는 자원을 공유하기 때문에 공유데이터를 사용할 수 있다.
공유데이터에 여러 쓰레드가 동시에 접근하게 되면 프로그래머의 의도와 다르게 동작할 수 있기 때문에 동기화를 통해 공유 데이터에 대한 동시접근 문제를 해결 할 수 있다.
공유데이터를 사용하는 코드 영역을 임계 영역 이라한다. 공유데이터(객체)가 가지고 있는 lock을 획득한 쓰레드만 영역내의 코드를 수행하고 해당 영역 코드실행을 완료했을경우 lock을 반납할 수 있다. lock은 하나만 존재하며 임계영역에는 하나의 쓰레드만 접근이 가능하다.
synchronized를 사용한 동기화
synchronized 키워드를 사용하여 동기화가 가능하다.
1.메서드를 임계영역으로 지정
ex)
public synchronized void calcSum(){
...
}
메서드에 synchronized 키워드를 붙일 경우 메서드 전체가 임계영역으로 설정되고, 쓰레드는 synchronized메서드가 호출된 시점부터 메서드의 객체 lock을 얻어 작업을 수행한다. 이후 해당 메서드의 작업이끝나면 메서드의 객체 lock을 반환한다.
2.특정한 영역을 임계영역으로 지정
ex)
synchronized(객체의 참조변수){
...
}
메서드 내의 임의의 코드를 블럭{ }으로 감싸고 블럭 앞에 synchronized(참조변수)를 붙이는 것이다. 매개변수형태로 되어있는 참조변수에는 락을 걸고자 하는 객체를 참조하는 것이여야한다. 일반적으로 this를 사용한다.
이러한 블럭을 synchronized블럭이라 하며 이 블럭 영역 내의 코드를 실행하면서 지정한 객체의 lock을 얻고 블럭내의 코드를 모두 실행한 후 블럭을 빠져나갈때 lock을 반환한다.
synchronized키워드를 이용한 쓰레드 동기화의 경우 영역만 지정해주면 자동으로 lock을 얻고 반환한다.
모든 객체는 lock을 하나씩 가지고 있으며, 해당 객체의 lock을 가진 쓰레드만 객체내의 임계영역 코드를 실행할 수 있다. 그리고 다른 쓰레드들은 lock을 얻기위해 기다리게된다. 임계영역은 멀티쓰레드 프로그램에서 프로그램의 성능을 좌우할 수 있기 때문에 임계영역을 최소화하거나 동기화를 잘 조절하여 효율적인 프로그램이 되도록 해야한다.
임계영역에서 사용되는 공유데이터는 private여야 한다. synchronized는 지정된 코드를 한번에 하나의 쓰레드만 접근 가능하도록 했을 뿐이기 때문에 다른방면에서의 데이터접근을 막기 위해서는 private로 설정해야한다.
wait()과 notify()
특정쓰레드가 객체 lock을 가진상태에서 오랜시간이 지나지않도록, 임계영역코드를 수행하다가 작업을 더이상 수행하지 못하는 상황을 해결하기위해 wait()과 notify()가 고안되었다.
lock을 가지고있는 쓰레드가 wait()을 호출하면, 작업중이던 쓰레드는 lock을 반납하고 해당 객체의 대기실(waiting pool : 객체의 lock을 얻기위해 대기하는 쓰레드 대기실)로 이동하여 notify()를 기다린다. notify()가 호출되면, 해당 객체 대기실에 있던 쓰레드중 임의의 쓰레드만 통지(lock)을 받아 임계영역에 접근한다.
notifyAll()은 객체의 대기실에 존재하는 모든 쓰레드들에게 통보를 하지만 lock을 얻을 수 있는 쓰레드는 한개 뿐이다.
위처럼 코드를 짜면 각 메소드에 동시접근 문제를 해결할 수 있다.
그러나 객체의 lock은 하나이기 때문에 COOK쓰레드가 add를 하거나 CUST1, CUST2가 remove를 할때 각각 쓰레드들은 대기를 해야한다. 즉 COOK이 호출하는 table.add()와 CUST의 table.remove()는 동시에 작동할 수 없다.
또한 각 작업을 마치고 notify()가 호출되어도 CUST와 COOK중 어느쓰레드가 lock얻게되어 작업을 수행하게 될지 알 수 없다. CUST쓰레드가 많고 COOK쓰레드는 하나라면 최악의 경우 COOK은 lock을 오랜시간 얻지못하여 프로그램이 '기아(starvation) 현상'에 빠질 수 있다.
이러한 경우를 막기위해 notifyAll()을 사용하여 모든 쓰레드에게 공정하게 lock을 얻을 기회를 줄 수 있는데 이또한 문제가 발생할 수 있다. 하나의 COOK쓰레드가 많은수의 CUST쓰레드와 lock을 얻기위한 '경쟁 상태(race condition)'에 빠질 수 있기 때문이다.
두개의 쓰레드는 같은 객체의 다른메소드를 호출하기 때문에 경쟁상대가 되어서는 안된다. 하지만 객체의 lock은 하나이기 때문에 wait()과 notify()를 이용한 lock제어에서는 경쟁상대가 될 수 밖에없다.
Lock과 Condition을 이용하면 wait()과 notify()에서는 불가능한 선별적인 통지가 가능하다.
Lock & Condition
synchronized키워드가 아닌 'java.util.concurrent.locks' 패키지에서 제공하는 lock클래스를 이용하여 동기화를 하는 방법이 있다. lock클래스를 통해 동기화를 할경우 같은객체내에서 메소드별로 lock을 생성할 수 있다.
lock클래스의 종류
ReentrantLock : 재진입이 가능한 lcok, 가장 일반적인 lock. lock이 있어야만 임계영역 코드를 수행할 수 있다.
ReentrantReadWriteLock : 읽기에는 공유적이고 쓰기에는 배타적인 lock.
StampedLock : lock을 걸거나 해지할 때 '스탬프(long 타입의 정수값)'를 사용한다.
ReentrantLock의 생성자
ReentrantLock()
ReentrantLock(boolean fair)
생성자 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 얻을 수 있도록, 공정(fair)하게 처리한다. 공정하게 처리하기 위해 쓰레드들을 확인하는 과정이 생겨나므로 성능은 좀 떨어진다.
void lock() : lock을 잠근다.
void unlock() : lock을 해지한다.
boolean isLocked() : lock이 잠겼는지 확인한다.
synchronized동기화와 달리, ReentrantLock과 같은 lock클래스들은 수동으로 lock을 잠구고 해제해야한다.
임계영역 내에서 예외가 발생하거나 return으로 빠져나가게 되면 unlock()을 수행하지 못하는 경우가 생길 수 있으므로 unlock()은 try-finally문으로 감싸는것이 일반적이다.
ex)
lock.lock();
try{
...
} finally{
lock.unlock();
}
try블럭내에서 어떤 코드를 실행하더라도 마지막엔 반드시 finally블록 코드를 실행하기 때문에 finally블럭에 lock.unlock()을 넣어주는 것이다.
선별적인 통지를 위해서는 Condition이 필요하다. 위예제의 문제점을 해결하기 위해서는 손님쓰레드를 위한 Condition과 요리사 쓰레드를 위한 Condition을 만들어서 각각의 waiting pool에서 따로 기다리게 하는것이다.
Condition은 이미 생성된 lock으로 부터 newCondition()을 호출하여 생성한다.
private ReentrantLock lock = new ReentrantLock();
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
위코드에서 두개의 Condition을 생성하였다.
이후 wait(), notify()대신 await() & signal()을 사용하면된다.
Table클래스의 코드를 보면 ReentrantLock클래스를 통해 lock생성과 두개의 Condition생성을 볼 수 있다.
또한 add메서드에서 음식이 꽉찼다면 forCook.await()을 통해 Cook 쓰레드를 대기시키고 forCust.signal()을 통해 CUST쓰레드를 작업을 하도록 만든다. remove()메서드 또한 음식의 개수가 0개일때 CUST쓰쓰레드를 forCust.await()을 통해 대기시키고 forCook.signal()을 통해 COOK쓰레드를 실행시킨다.
위처럼 reentrantLock클래스와 Condition을 사용할경우 필요한 상황에 맞춰 작업을 수행하는 쓰레드들에 대해 선별적인 동기화를 적용해줄 수 있다. 기아현상이나 경쟁현상이 많이 줄어든 예제이다.
'Programming > JAVA' 카테고리의 다른 글
람다식(Lambda expression) (0) | 2021.08.21 |
---|---|
fork & join (0) | 2021.08.20 |
쓰레드의 상태 및 제어 (0) | 2021.08.16 |
데몬 쓰레드(daemon thread) (0) | 2021.08.13 |
쓰레드 우선순위와 쓰레드 그룹 (0) | 2021.08.13 |