synchronized 블록 외에도 동기화할 수 있는 방법이 있다.
java.util.concurrent.locks 패키지가 제공하는 lock 클래스들을 이용하는 방법이다.
ReentrantLock 재진입이 가능한 lock, 가장 일반적은 배타 lock
ReentrantReadWriteLock 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock ReentrantReadWriteLock에 낙관적인 lock의 기능을 추가
ReentrantLock은 wait(), notify()처럼 특정 조건에서 lock을 풀고 나중에 다시 lock을 얻고 임계영역으로 들어와서 이후의 작업을 수행한다. 앞서 다뤘던 lock과 일치한다.
ReentrantReadWriteLock은 읽기 lock이 걸려있으면 다른 스레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있다. 읽기는 내용을 변경하지 않기 때문에 여러 스레드가 읽어도 문제가 없다.
하지만 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않는다. 반대의 경우도 마찬가지이다.
StampedLock은 lock을 걸거나 해지할 때 '스탬프'를 사용하며 읽기와 쓰기를 위한 lock 외에 '낙관적 읽기 lock'이 추가된 것이다. 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야 하지만, 낙관적 읽기 lock은 쓰기 lock에 의해 바로 풀린다.
ReentrantLock
ReentrantLock()
ReentrantLock(boolean fair)
생성자의 매개변수를 true로 주면, lock이 풀렸을 때 가장 오래 기다린 스레드가 lock을 획득할 수 있게 처리한다. (이 경우 가장 오래 기다린 스레드를 확인해야 하므로 성능은 떨어진다.)
void lock() lock을 잠근다.
void unlock() lock을 해지한다.
boolean isLocked() lock이 잠겼는지 확인한다.
ReentrantLock은 synchronized 블록과 달리 수동으로 lock을 잠그고 해제해야 한다.
lock.lock();
try {
// 임계영역
} finally {
lock.unlock();
}
unlock()은 try-finally문으로 감싸는 것이 일반적이다. 그래야 try블록에서 어떤 일이 발생해도 finally 블록에 있는 unlock()이 수행되어 lock이 풀리지 않는 일이 발생하지 않기 때문이다.
boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
tryLock()은 다른 스레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않거나 지정된 시간만큼만 기다린다. lock을 얻으면 true를, 얻지 못하면 false를 반환한다.
Condition
식당에서 요리사는 음식을 만들어 테이블에 제공하고, 손님은 그 음식을 먹는 상황이라고 해보자.
요리사와 손님이 각각 스레드라고 할 때, 요리사는 음식을 계속 만들어 테이블에 놓다가 테이블에 자리가 없으면 음식을 그만 만들고 대기상태로 들어간다. 손님들은 자신이 원하는 음식이 테이블에 있으면 먹고 없다면 나올 때까지 대기한다.
손님이 음식을 먹으면 요리사 스레드를 깨워 다시 음식을 만들게 하고, 음식이 테이블에 제공되면 손님 스레드를 깨워 원하는 음식을 먹을 수 있게 한다.
그런데 이때 대기하는 요리사 스레드와 손님 스레드는 같은 대기열에서 대기하게 된다. 그리고 notify()는 손님 스레드, 요리사 스레드 구분 없이 대기열에 있는 스레드들 중 하나를 깨운다.
테이블의 음식이 줄어서 notify()가 호출되었다면 요리사 스레드가 통지받고, 음식에 테이블에 추가되면 손님 스레드가 통지를 받아야 한다. 하지만 notify()는 그저 대기열에서 대기 중인 스레드 중에서 하나를 임의로 선택해 통지한다.
Condition은 이러한 문제를 해결하기 위한 것이다. 손님 스레드를 위한 Condition과 요리사 스레드를 위한 Condition을 만들어서 각각의 대기열에서 따로 기다리도록 하면 된다.
private ReentrantLock lock = new ReentrantLock(); // lock을 생성
// lock으로 condition을 생성
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();
Object | Condition |
void wait() | void await() void awaitUninterruptibly() |
void wait(long timeout) | boolean await(long tiem, TimeUnit unit) long awaitNanos(long nanosTimeout) |
void notify() | void signal() |
void notifyAll() | void signalAll() |
wait()과 notify() 대신 Condition의 await()과 signal()을 사용하면 된다.
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
class Customer4 implements Runnable {
private Table4 table;
private String food;
Customer4(Table4 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
}
}
class Cook4 implements Runnable {
private Table4 table;
Cook4(Table4 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 Table4 {
String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
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(); COOK쓰레드를 기다리게 한다.
Thread.sleep(500);
} catch(InterruptedException e) {}
}
dishes.add(dish);
forCust.signal(); // notify(); 기다리고 있는 CUST를 깨우기 위함.
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(); CUST쓰레드를 기다리게 한다.
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 ThreadWaitEx4 {
public static void main(String[] args) throws Exception {
Table4 table = new Table4();
new Thread(new Cook4(table), "COOK1").start();
new Thread(new Customer4(table, "donut"), "CUST1").start();
new Thread(new Customer4(table, "burger"), "CUST2").start();
Thread.sleep(2000);
System.exit(0);
}
}
'JAVA' 카테고리의 다른 글
[OOP] 객체지향언어, 클래스와 객체 (0) | 2023.02.28 |
---|---|
[Thread] volatile (0) | 2023.02.28 |
[Thread] wait(), notify() (0) | 2023.02.27 |
[Thread] 스레드의 동기화 - synchronized (0) | 2023.02.27 |
[Thread] join(), yield() (0) | 2023.02.27 |