Lock
쓰레드를 동기화할 수 있는 또다른 방법으로, 'java.util.concurrent.locks'패키지에서 제공합니다.
synchronized 블럭을 사용하면 자동으로 lock이 잠기고 풀리기 때문에 간편하지만 같은 메서드 내에서만 lock을 걸 수 있다는 제약사항과 쓰레들를 구분해서 통지가 불가능하다는 제약사항이 있었습니다.
이럴 때 lock 클래스를 사용할 수 있습니다.
종류
ReentrantLock : 재진입이 가능한 lock. 가장 일반적인 배타적(Exclusive) lock
ReentrantReadWriteLock : 읽기에는 공유적이고, 쓰기에는 배타적인 lock
StampedLock : ReentrantReadWriteLock에 낙관적 읽기(Optimistic Reading) lock의 기능을 추가
ReentrantLock
특정 조건에서 lock을 풀고 나중에 다시 lock을 얻어 임계영역으로 재진입하여 이후의 작업을 수행할 수 있는 lock입니다.
가장 기본적인 lock으로, 특정 수식어가 없는 lock의 경우 대부분 해당 lock을 지칭합니다.
ReentrantReadWriteLock
읽기를 위한 lock과 쓰기를 위한 lock을 제공합니다.
ReentrantLock은 배타적인 lock이므로 무조건 lock이 있어야만 임계 영역의 코드를 수행할 수 있지만,
ReentrantReadWriteLock은 읽기 lock이 걸려있으면 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있습니다.
그러나 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용하지 않습니다.
반대의 경우 역시 마찬가지입니다.
읽기를 하는 경우 읽기 lock을, 쓰기를 하는 경우 쓰기 lock을 거는 것일 뿐 lock을 거는 방법은 같습니다.
StampedLock
lock을 걸거나 해지할 때 stamp(long 타입의 정수값)를 사용합니다.
읽기와 쓰기를 위한 lock 외에 낙관적 읽기 lock (optimistic reading lock)이 추가된 것입니다.
읽기 lock이 걸려있으면 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴 때까지 기다려야 하는데 비해,
낙관적 읽기 lock은 쓰기 lock에 의하여 바로 풀립니다.
그래서 낙관적 읽기에 실패하면 읽기 lock을 다시 얻어와야 합니다.
무조건 읽기 lock을 거는 것이 아니라 쓰기와 읽기가 충돌하는 경우에만 쓰기가 끝난 후에 읽기 lock을 거는 것입니다.
사용 예시
ReentrantLock만 이해한다면 나머지는 자바 API를 참조하여 응용 가능하므로 나머지는 생략하겠습니다.
ReentrantLock
생성자는 다음 두가지가 있습니다.
ReentrantLock()
ReentrantLock(boolean fair)
생성자의 매개변수로 true를 주면 lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 획득할 수 있게, 즉 공정(fair)하게 처리합니다.
그러나 공정하게 처리하기 위해서는 어떤 쓰레드가 가장 오래 기다렸는지 확인해야 하므로 성능은 떨어집니다.
대부분의 경우 굳이 공정하게 처리하지 않더라도 문제되지 않으므로 성능을 선택합니다.
제공하는 메서드는 다음과 같습니다.
void lock() : lock을 잠급니다.
void unlock() : 잠금을 해지합니다.
boolean isLocked() : lock이 잠겼는지 확인합니다.
자동적으로 lock의 잠금과 해제가 관리되는 synchronized 블럭과 달리, ReentrantLock과 같은 lock 클래스들은 수동으로 lock을 잠그고 해제해야 합니다.
이해를 돕기 위해 synchronized 블럭과 ReentrantLock에서 임계 영역을 설정하는 방법을 비교하여 살펴보도록 하겠습니다.
임계 영역 내에서 예외가 발생하거나 return 문으로 빠져나가게 되면 lock이 풀리지 않을 수 있으므로
unlock()은 try-catch로 감싸 finally에 명시하는 것이 일반적입니다.
Condition
이전 포스팅(https://ttl-blog.tistory.com/798?category=916885 )에서 wait()와 notify()는 쓰레드를 구분하여 통지하는 것이 불가능하다는 것을 살펴보았습니다.
Condition은 이러한 문제를 해결합니다.
구분할 쓰레드 종류에 따라 각각의 Condition을 따로 만들어서 각각의 waiting pool에서 따로 기다리도록 할 수 있습니다.
Condition은 이미 생성된 lock으로부터 newCondition()을 호출하여 생성할 수 있습니다.
다음은 예시입니다.
이후 wait()와 notify() 대신 Condition의 await()와 signal()을 호출하면 끝입니다.
예제
public class Table {
String[] dishNames = {"피자", "피자", "치킨"};//피자가 더 자주 나온다
final int MAX_FOOD = 6;
private ArrayList<String> dishes = new ArrayList<>();
private ReentrantLock lock = new ReentrantLock();
private Condition custCond = lock.newCondition();
private Condition cookCond = lock.newCondition();
public void add(String dish) {
lock.lock();
try {
if (isFull()) {
String name = Thread.currentThread().getName();
System.out.println(name + " is waiting");
try {
//wait();
cookCond.await();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
dishes.add(dish);
//notify();
cookCond.signal();
System.out.println("Dishes: " + dishes.toString());
} finally {
lock.unlock();
}
}
private boolean isFull() {
return dishes.size() >= MAX_FOOD;
}
public boolean remove(String dish) {
lock.lock();
try {
while (isEmpty()) {
System.out.println(Thread.currentThread().getName() + " is waiting");
try {
custCond.await();
Thread.sleep(500);
} catch (InterruptedException e) {}
}
while (true) {
for (String dishName : dishes) {
if (dishName.equals(dish)) {
dishes.remove(dish);
cookCond.signal();
return true;
}
}
try {
System.out.println(Thread.currentThread().getName() + " is waiting");
custCond.await();
Thread.sleep(500);
} catch (InterruptedException e) {
}
}
} finally {
lock.unlock();
}
}
private boolean isEmpty() {
return dishes.isEmpty();
}
public double dishNum() {
return dishNames.length;
}
}
public class Customer implements Runnable {
private Table table;
private String food;
public Customer(Table table, String food) {
this.table = table;
this.food = food;
}
@Override
public void run() {
while (true) {
try { Thread.sleep(100); } catch (InterruptedException e) {}
eatFood();
}
}
private void eatFood() {
String name = Thread.currentThread().getName();
table.remove(food);
System.out.println(name + " ate a " + food);
}
}
public class Cook implements Runnable {
private Table table;
public Cook(Table table) {
this.table = table;
}
@Override
public void run() {
while (true) {
int idx = (int)(Math.random() * table.dishNum());
table.add(table.dishNames[idx]);
try {
Thread.sleep(10);
} catch (InterruptedException e) {}
}
}
}
public class Example {
public static void main(String[] args) throws InterruptedException {
Table table = new Table();
new Thread(new Cook(table), "COOK1").start();
new Thread(new Customer(table, "피자"), "CUST1").start();
new Thread(new Customer(table, "치킨"), "CUST2").start();
Thread.sleep(2000);
System.exit(0);
}
}
이전 포스팅의 예제와 달리 요리사 쓰레드가 통지를 받아야하는 상황에서 손님 쓰레드가 통지를 받는 경우가 사라졌습니다.
기아현상과 경쟁상태가 확실히 개선되었지만 아직 발생할 가능성이 남아있습니다.
음식의 종류의 따라 Condition을 세분화하면 통지를 받고도 원하는 음식이 없어서 기다리는 현상도 없어지도록 할 수 있습니다.
'☕️ Java > 기본' 카테고리의 다른 글
[Java] groupingBy를 통해 동일한 자료의 개수 구하기 (0) | 2022.11.12 |
---|---|
[Java] Thread (9) - ForkJoin 프레임워크 (0) | 2022.07.15 |
[Java] Thread (7) - 쓰레드의 동기화(1) - synchronized와 wait(), notify() (0) | 2022.07.15 |
[Java] Thread (6) - 쓰레드의 실행제어 (0) | 2022.07.14 |
[Java] Thread (5) - 데몬 쓰레드 (0) | 2022.07.13 |