1. Lock 클래스
- synchronized 블럭 외에도 'java.util.concurrent.locks' 패키지가 제공하는 lock 클래스들을
이용하는 방법이 있다.
- synchronized 블럭으로 동기화를 하면 자동적으로 lock이 잠기고 풀리며 심지어 예외가
발생하여도 lock은 자동으로 풀린다.
- 그러나, 같은 메서드 내에서만 lock을 걸 수 있다는 제약이 있어서 불편할 때가 있다.
- 그럴 때 아래와 같은 lock 클래스의 종류 3가지를 사용한다.
클래스 | 설명 |
ReentrantLcok | 재진입이 가능한 lock, 가장 일반적인 배타 lock |
ReentrantReadWriteLock | 읽기에는 공유적이고, 쓰기에는 배타적인 lock |
StampedLock | ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가. |
( ReentrantLcok이 가장 일반적인 lock이다. )
( Lock 앞에 Reentrant이 붙은 이유는 앞서 wait()¬ify()에서 배운 것처럼, 특정 조건에서 lock을 풀고
나중에 다시 lock을 얻어 CS에 들어와서 작업할 수 있기 때문이다. )
( ReentrantReadWriteLock은 읽고 쓸 때 lock을 건다. )
( 한 쓰레드가 읽기 lock을 쓰고 있을 때, 다른 쓰레드가 중복해서 lock을 중복해서 읽기를 할 수 있다. )
( 읽기는 내용을 변경하지 않으므로 상관없지만, 쓰기같은 경우는 중복허용을 하지 않는다. )
( 또한 읽기의 lock이 걸려있는 상태에서 쓰기 lock을 걸 수 없다. 반대의 경우도 마찬가지 )
( StampedLock는 lock을 걸거나 해지할 때 '스탬프(long 타입의 정수값)'을 사용하며, 읽기와 쓰기 lock외에
낙관적 읽기가 추가된 것이다. )
( 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야하는데 비해
낙관적 읽기 lock은 쓰기 lock에 의해 바로 풀린다. )
( 그래서 낙관적 읽기에 실패하면 다시 읽기 lock을 읽어와야 한다. )
( 그러므로, 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기에 충돌할 때만 쓰기가 끝난 후에 읽기 lock을
거는 것이다. )
( 아래는 가장 일반적인 StampedLock을 이용한 낙관적 읽기의 예이다. )
int getBalance(){
long stamp = lock.tryOptimisticRead(); //낙관적 읽기 lock을 건다.
int curBalance = this.balance; //공유 데이터인 balance를 읽어온다.
if(!lock.validate(stamp)){ //쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
stamp = lock.readLock(); //lock이 풀렸으면, 읽기 lock을 얻으려고 기다린다.
try{
curBalance = this.balance //공유데이터를 다시 읽어온다.
} finally{
lock.unlockRead(stamp); //읽기 lock을 푼다.
}
}
return curBalance;
}
1-1. ReetrantLock의 생성자
- ReentrantLock의 생성자는 아래와 같이 두 개의 생성자를 가지고 있다.
ReentrantLock()
ReentrantLock(boolean fair)
( 만약 fair에 true로 주면 lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock 획득할 수 있게, 즉 공정하게
처리한다. )
( 그러나 공정하게 처리한다면 성능이 떨어질 수 밖에 없다. )
( 대부분은 공정하게 처리하지 않아도 별 문제가 생기지 않기 때문에 성능을 선택하는 것이 좋다. )
- ReentrantLock은 synchronized 블럭과 달리 수동으로 아래와 같이 잠그고 해제해야 한다. )
lock.lock();
try{
//CS
} finally{
lock.unlock();
}
( 위 try-finally를 쓴 이유는 CS에서 예외가 발생하거나 return 문으로 빠져 나가게 되면 lock이 풀리지
않을 수 있으므로 unlock()을 꼭 finally에 쓰는 것이 좋다. )
- 또 한 가지 tryLock()이 있는데, 지정된 시간만큼 기다린다. 만약 얻지 못한다면 false, 얻으면 true를
반환한다.
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
( lock()은 lock을 얻기 위해서는 쓰레드를 블락시키므로, 응답성이 나빠질 수 있다. )
( 그러므로, 응답성을 중요시 여기는 프로그램은 tryLock()을 적절히 사용하여 일정시간동 기다린다음
포기하도록 결정할 수 있게 사용할 수 있다. )
( 시간이 지나면 interrupt()에 의해 취소된다. )
1-2. ReentrantLock과 Condition
- 앞서 wait()¬ify() 예제에서 요리사 쓰레드와 손님 쓰레드를 구분해서 통지하는 것을 구현하지
못하였다.
- 그러나 Condition은 이 문제점을 손님 쓰레드를 위한 Condition과 요리사 쓰레드를 위한 Condition을
만들어서 각각의 waiting pool에서 따로 기다리도록 하면 문제는 해결할 수 있다.
- Condition은 이미 생성된 lock으로부터 newCondition()을 호출해서 아래와 같이 생성한다.
private ReentrantLock lock = new ReentrantLock(); //lock 생성
//lock으로 condition 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
( 또한 wait()¬ify() 대신 Condition의 await()&signal()을 사용하면 된다. )
Object | Condition |
void wait() | void await() void awaitUninterruptedtibly() |
void wait(long timeout) | boolean await(long time, TimeUnit unit) long awaitNanos(long nanosTimeout) boolean awaitUntil(Date deadline) |
void notify() | void signal() |
void notifyAll() | void signalAll() |
1-2-1.ReentrantLock과 Condition을 이해하기 위한 예제(1)
: 앞서 wait()¬ify()에서 구분해서 통지못한 것을 await()&signal로 해결한 예제이다.
import java.util.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Exercise020 {
public static void main(String[] args) throws Exception {
Table3 table3 = new Table3();
new Thread(new Cook3(table3), "요리사").start();
new Thread(new Customer3(table3, "donut"), "손님 1").start();
new Thread(new Customer3(table3, "burger"), "손님 2").start();
//3초 뒤에 프로그램을 종료시킨다.
Thread.sleep(3000);
System.exit(0);
}
}
class Table3 {
String[] dishNames = { "donut","burger" };
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
public void add(String dish) {
lock.lock();
try {
while(dishes.size() >= MAX_FOOD) {
String name = Thread.currentThread().getName();
System.out.println(name+" is waiting.");
try {
forCook.await(); // wait(); 요리사 쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
dishes.add(dish);
forCust.signal(); // notify(); 손님 깨운다.
System.out.println("Dishes:" + dishes.toString());
} finally {
lock.unlock();
}
}
public void remove(String dishName) {
lock.lock(); // synchronized(this) {
String name = Thread.currentThread().getName();
try {
while(dishes.size()==0) {
System.out.println(name+" is waiting.");
try {
forCust.await(); // wait(); 손님 쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
while(true) {
for(int i=0; i<dishes.size();i++) {
if(dishName.equals(dishes.get(i))) {
dishes.remove(i);
forCook.signal(); // notify();잠자고 있는 COOK을 깨움
return;
}
} // for문의 끝
try {
System.out.println(name+" is waiting.");
forCust.await(); // wait(); // CUST쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
} // while(true)
// } // synchronized
} finally {
lock.unlock();
}
}
public int dishNum() { return dishNames.length; }
}
class Cook3 implements Runnable {
private Table3 table;
Cook3(Table3 table) { this.table = table; }
public void run() {
while(true) {
int idx = (int)(Math.random()*table.dishNum());
table.add(table.dishNames[idx]);
try { Thread.sleep(10);} catch(InterruptedException e) {}
} // while
}
}
class Customer3 implements Runnable {
private Table3 table;
private String food;
Customer3(Table3 table, String food) {
this.table = table;
this.food = food;
}
public void run() {
while(true) {
try { Thread.sleep(100);} catch(InterruptedException e) {}
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
} // while
}
}
( 구분하여 통지한 결과 '기아 현상'과 '경쟁 상태'가 확실히 개선되게 되었다. )
( 그러나 쓰레드의 종류에 따라 구분지어 통지하더라도 여전히 같은 종류의 쓰레드끼리는 '기아 현상'과 '경쟁 상태'를
해결하지 못하였다. )
( 그 해결책은 손님에서 더 세분화하여 Condition을 만드는 것이다. )
( 아래의 링크에서 1-3 예제(3)이랑 비교를 통해 어떤 것이 달라졌는지 확인하자. )
쓰레드의 동기화(2): wait() & notify() (tistory.com)
쓰레드의 동기화(2): wait() & notify()
1. wait()와 notify() - 동기화도 중요하지만, 특정 쓰레드가 객체의 락을 가진 상태로 오랜 시간을 보내지 않도록 하는 것 또한 중요하다. - 이러한 것을 구현하기 위해 wait()와 notify()가 등장하게 되
kind-coding.tistory.com
'자바 > 프로세스와 쓰레드' 카테고리의 다른 글
fork & join 프레임웍 (0) | 2022.05.10 |
---|---|
volatile (0) | 2022.05.10 |
쓰레드의 동기화(2): wait() & notify() (0) | 2022.05.07 |
쓰레드의 동기화(1): Critical Section & Lock, snychronized (0) | 2022.05.07 |
쓰레드의 실행제어 (0) | 2022.02.20 |