-
Lock, ConditonJava/Java 문법 종합반 강의 정리 2024. 1. 3. 20:37
!- 코드 예시는 가장 마지막에 -!
Lock
- synchronized 블럭으로 동기화하면 자동적으로 Lock이 걸리고 풀리지만 같은 메서드 내에서만 Lock을 걸 수 있다는
제약 이 있다.
- 이런 제약을 해결하기 위해 Lock 클래스를 사용한다.
ReentrantLock
- 재진입 가능한 Lock, 가장 일반적인 베타 Lock
- 특정 조건에서 Lock을 풀고, 나중에 다시 Lock을 얻어 임계영역으로 진입이 가능하다.
public class MyClass { private Object lock1 = new Object(); private Object lock2 = new Object(); public void methodA() { synchronized (lock1) { methodB(); } } public void methodB() { synchronized (lock2) { // do something methodA(); } } }
- methodA 는 lock1을 가지고, methodB는 lock2를 가진다.
- methodB 에서 methodA를 호출하고 있으므로, methodB에서 lock2를 가진 상태에서 methodA를 호출하면 lock1을
가지려고 할 것이다.
- 그러나 이때, methodA에서 이미 lock1을 가지고 있으므로, lock2를 기다리는 상태가 되어 데드락이 발생할
가능성이 있다.
- 하지만 ReentrantLock을 사용하면, 같은 쓰레드가 이미 락을 가지고 있더라도 락을 유지하며 계속 실행할 수 있기
때문에 데드락이 발생하지 않는다.
- 즉, ReentrantLock을 사용하면 코드의 유연성을 높일 수 있다.
ReentrantReadWriteLock
- 읽기를 위한 Lock과 쓰기를 위한 Lock을 따로 제공한다
- 읽기에는 공유적이고, 쓰기에는 베타적인 Lock이다.
- 읽기 Lock이 걸려있으면 다른 쓰레드들도 읽기 Lock을 중복으로 걸고 읽기를 수행할 수 있다.(read-only)
- 읽기 Lock이 걸려있는 상태에서 쓰기 Lock을 거는 것은 허용되지 않는다.(데이터 변경 방지)
StampedLock
- ReentrantReadWriteLock에 낙관적인 Lock의 기능을 추가했다.
- 낙관적인 Lock : 데이터를 변경하기 전에 락을 걸지 않는 것을 말한다. 낙관적인 락은 데이터 변경을 할 때 충돌이
일어날 가능성이 적은 상황에서 사용한다.
- 낙관적인 락을 사용하면 읽기와 쓰기 작업 모두가 빠르게 처리된다. 쓰기 작업이 발생했을 때, 데이터가 이미 변경된 경 우 다시 읽기 작업을 수행하여 새로운 값을 읽어들이고, 변경 작업을 다시 수행한다. 이러한 방식으로 쓰기 작업이 빈번하 지 않은 경우에는 낙관적인 락을 사용하여 더 빠른 처리가 가능하다.
- 낙관적인 읽기 Lock은 쓰기 Lock에 의해 바로 해제 가능하다.
- 무조건 읽기 Lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기 후 읽기 Lock을 건다.
Condition
wait() & notify()의 문제점인 waiting pool 내 쓰레드를 구분하지 못한다는 것을 해결한 것이 Condition 이다.
추가 설명 : wait() 와 notify() 는 대기중인 스레드를 구분하지 못한다.
Condition은 waiting pool 내의 쓰레드를 분리하여 특정 조건이 만족될 때만 깨우도록 할 수 있으며,
ReetrantLock 클래스와 함께 사용된다. 따라서 Condition을 사용하면 wait(), notify()의 문제점을 보완할 수 있다.
- wait() & notify() 대신 Condition의 await() & signal() 을 사용한다.
- 아래 코드와 같이 Condition 을 만들어서 대기줄(waiting pool)을 사용할 수 있다.
public class Main { public static final int MAX_TASK = 5; private ReentrantLock lock = new ReentrantLock(); // lock으로 condition 생성 private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private ArrayList<String> tasks = new ArrayList<>(); // 작업 메서드 public void addMethod(String task){ lock.lock(); // 임계영역 시작 try{ while(tasks.size() >= MAX_TASK){ String name = Thread.currentThread().getName(); System.out.println(name + " is waiting."); try{ condition1.await(); // wait() : condition1 쓰레드를 기다리게 한다. Thread.sleep(500); } catch (InterruptedException e) { } } tasks.add(task); condition2.signal(); System.out.println("Tasks:" + tasks.toString()); } finally { lock.unlock(); } } }
https://tangpoo.tistory.com/137
wait(), notify()
침범을 막은 코드(synchronized)를 수행하다가 작업을 더 이상 진행할 상황이 아니면, wait()을 호출하여 쓰레드가 Lock을 반납하고 기다리게 할 수 있다. - 그럼 다른 쓰레드가 락을 얻어 해당 객체에
tangpoo.tistory.com
위 글에서 겪었던 waiting poll 안의 쓰레드를 구분하지 못하여 발생했던 문제들을 개선할 수 있었다.
여전히 점원과 고객1, 2 가 모두 멈추는 상황이 나올 수 있어서 적절치 않은 예시라는 생각이 들기는 한다...
그래도 wait(), notify() 의 개선점을 위주로 초점을 맞춰 학습하도록 하자.
'Java > Java 문법 종합반 강의 정리' 카테고리의 다른 글
함수형 프로그래밍 실전 예제(함수형 인터페이스, 람다 함수) (0) 2024.01.09 Java8 에서의 변경점 (1) 2024.01.09 wait(), notify() (1) 2024.01.03 join, yield, synchronized (4) 2024.01.03 쓰레드의 상태와 제어 (2) 2024.01.03