* 해당 포스팅은 김영한님의 고급 자바편 영상을 시청 완료후 정리한 내용입니다.
안녕하세요 오늘로 추석 마지막 날인데 다들 즐거운 시간 보내셨는지요~ ㅎㅎ
이번 추석은 정말 이상하게 더운 날씨였습니다.. 추석에 폭염이라니 정말 믿겨지지 않네요..
오늘 포스팅은 지난 포스팅에 쭉 이어가서
ReentrantLock 객체에 대해서 사용법, 내부적으로 어떻게 동작하는지 살펴보는 시간이 되겠습니다.
ReentrantLock 이론
자바는 1.0에 synchronize 키워드를 통해 쉽게 임계영역을 설정하고 병목화를 최소화 할 수 는 있지만
각 스레드를 세밀하게 제어할 수 없는 단점이 있다고 했습니다
synchronize 단점
무한대기 : Blocked 상태의 스레드는 락이 풀릴 때 까지 무한 대기한다.
공정성: 락이 돌아왔을 때 Blocked 상태의 여러 스레드 중 어떤 스레드가 락을 획득할 지 알 수 없다.
ReentrantLock 은 Lock 인터페이스를 구현한 객체입니다 그렇다면 Lock 인터페이스를 살펴볼까요?
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
Lock: 락을 획득합니다. 만약 다른 스레드가 이미 락을 획득했다면 락이 풀릴때까지 현재 스레드는 대기 상태로 들어가며 이 메소드는
인터럽트에 응답하지 않습니다.
LockInterruptibly : 락 획득을 시도하되, 다른 스레드가 인터럽트할 수 있도록 합니다. 만약 다른 스레드가 락을 획득했다면, 현재 스레드는 락을 획득할 때 까지 대기합니다. 대기 중에 인터럽트 발생 시 InterruptedException 발생하며 락 획득을 포기합니다.
tryLock: 락 획득을 시도하고, 즉시 성공 여부 반환
tryLock(long time, TimeUnit unit) : 주어진 시간 동안 락 획득 시도, 주어진 시간 안에 락을 획득하면 true, 주어진 시간이 지나면 false 반환 인터럽트 발생 시 락 획득 포기
unLock: 락을 해제합니다. 락을 획득한 스레드가 호출해야 하며, 그렇지 않는 경우 IllegalMonitorStateException 발생할 수 있습니다.
newCondition : conditioin 객체를 생성하여 반납합니다 해당 객체는 결합되어 사용되며, 특정 조건을 기다리거나 신호를 받을 수 있도록 합니다.
이로써 고수준의 스레드를 제어할 수 있으며, 기존 synchronize 의 단점인 무한대기 문제를 해결할 수 있습니다만 공정성에 대한 문제가 남아있습니다.
ReentrantLock 객체에서 이문제를 해결하기 위해 공정모드, 비공정모드가 있습니다.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Lock;
public class ReentrantLockEx {
// 비공정 모드 락
private final Lock nonFairLock = new ReentrantLock();
// 공정 모드 락
private final Lock fairLock = new ReentrantLock(true);
public void nonFairLockTest() {
nonFairLock.lock();
try {
// 임계 영역
} finally {
nonFairLock.unlock();
}
}
public void fairLockTest() {
fairLock.lock();
try {
// 임계 영역
} finally {
fairLock.unlock();
}
}
}
비공정 모드
비공정모드는 ReentrantLock의 기본모드입니다, 이 모드에서는 락을 먼저 요청한 스레드가 락을 먼저 획득한다는 보장이 없습니다, 락을 풀었을 때, 대기 중인 스레드 중 아무나 락을 획득할 수 있습니다. 이는 락을 빨리 획득할 수 있지만, 특정 스레드가 장기간 락을 획득하지 못할 가능성도 있습니다
비공정 모드 특징
성능 우선: 락을 획득하는 속도가 빠르다
선점 가능: 새로운 스레드가 기존 대기 스데르보다 먼저 락을 획득할 수 있다.
기아 현상 가능성: 특정 스레드가 계속해서 락을 획득하지 못할 수 있다.
공정모드
생성자에서 true 값을 전달하면 됩니다. 공정 모드는 락을 요청한 순서대로 스레드가 락을 획득할 수 있게 합니다. 하지만 이로 인해 성능이 저하 될 수 있습니다.
공정 모드 특징
공정성 보장: 대기 큐에서 먼저 대기한 스레드가 락을 먼저 획득합니다.
기아 현상 방지: 모든 스레드가 언젠가 락을 획득할 수 있게 보장된다.
성능 저하: 락을 회득하는 속도가 느려질 수 있다.
ReentrantLock 활용
import static util.ThreadUtils.sleep;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BankAccountV4 implements BankAccount {
private int balance;
private final Lock lock = new ReentrantLock();
public BankAccountV4(int initialBalance) {
this.balance = initialBalance;
}
@Override
public boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
lock.lock(); // ReentrantLock 이용하여 lock을 걸기
try {
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000);
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
} finally {
lock.unlock(); // ReentrantLock 이용하여 lock 해제
}
log("거래 종료");
return true;
}
@Override
public int getBalance() {
lock.lock(); // ReentrantLock 이용하여 lock 걸기
try {
return balance;
} finally {
lock.unlock(); // ReentrantLock 이용하여 lock 해제
}
}
}
BankAccountV4 객체는 필드로 초기 잔액, ReentarntLock 객체를 선언합니다.
기존의 synchronized 의 임계영역을 설정하는 대신 lock.lock() 메서드를 호출하여 락을 겁니다.
lock -> unlock 까지 임계영역이라고 생각하면 됩니다.
여기서 제일 중요한건 임계영역 마지막에 unlock 메서드를 호출해야 다른 스레드가 락을 획득할 수 있습니다.
락을 반납하지 않는다면 계속 기다리게 되겠죠? (* 이때 tryLock(long time, TimeUnit unit 메서드를 활용할 수 있다.)
* 주의 여기서는 기본 객체의 Object 의 모니터 락을 사용하는 것이 아닌 ReentrantLock 객체가 제공하는 기능입니다!
실행 결과
12:09:20.185 [t1] 거래 시작: BankAccountV4
12:09:20.185 [t2] 거래 시작: BankAccountV4
12:09:20.191 [t1] [검증 시작] 출금액: 800, 잔액: 1000
12:09:20.191 [t1] [검증 완료] 출금액: 800, 잔액: 1000 12:09:20.673 [main] t1 state: TIMED_WAITING
12:09:20.674 [main] t2 state: WAITING
12:09:21.196 [t1] [출금 완료] 출금액: 800, 변경 잔액: 200 12:09:21.197 [t1] 거래 종료
12:09:21.197 [t2] [검증 시작] 출금액: 800, 잔액: 200 12:09:21.198 [t2] [검증 실패] 출금액: 800, 잔액: 200 12:09:21.204 [main] 최종 잔액: 200
실행 결과 분석
t1, t2 각 스레드가 출금을 시작합니다, 여기서 t1이 약간 먼저 실행된다고 가정하겠습니다.
ReentrantLock 내부에는 락과 락을 얻지 못해 대기하는 스레드를 관리하는 대기 큐가 존재합니다.
* 여기서 이야기 하는 락은 기본 객체의 내부에 존재하는 모니터 락이 아닌 해당 객체가 제공하는 기능입니다.
t1이 ReentrantLock 에 있는 락을 획득합니다.
락을 획득한 경우 Runnable 상태가 유지 되고 임계영역의 코드를 실행할 수 있습니다.
t1 임계영역 코드를 실행합니다.
t2도 락을 획득 하려고 하지만 락이 존재하지 않아 대기합니다.
t2처럼 락을 획득하지 못하다면 대기큐에서 관리하게 됩니다.
LockSupport.park() 가 내부에서 호출합니다.
* tryLock(long time, TimeUnit unit)와 같은 시간 대기 기능을 사용하면 Timed_Wating 이 되고, 대기 큐에서 관리합니다.
(Wating 과 Timed_Wating 은 엄연히 다름 !!)
t1이 임수수행을 완수했고 잔액을 수정했습니다.
t1은 이제 락을 반납하고 대기큐에서 대기하던 t2가 락을 획득합니다.
t2의 상태는 Wating -> Runnable 상태로 변경되면서 락 획득 시도 하고,
획득 성공하면 대기큐에서 제거됩니다. * 획득하지 못하면 대기큐 대기함
t2가 임계영역 코드를 실행함
t2의 스레드는 잔액이 200원 이므로 검증 로직을 통과하지 못하고 return false를 호출하고 finally 구문으로 이동합니다.
t2: lock.unlock()을 호출해서 락을 반납하고, 대기 큐의 스레드를 하나 깨우려고 시도하지만 대기 큐에 스레드가 없으므로 깨우지 않습니다.
완료 상태.
이처럼 오늘 포스팅을 통해서 기존의 synchronized 의 한계점을 극복한 ReentarntLock 객체를 통해
무한 대기 상태로 해결하고, 락의 공정성 문제도 해결했습니다.
사실 최종적으로 우리가 스레드를 제어하고 사용할때 ReentrantLock를 직접 사용하지는 않습니다.
하지만 대부분의 동시성 문제를 해결하는 새로운 기술들은 ReentrantLock의 기능을 바탕으로 세워진 기능이므로
내부 동작방식을 이해하는것이 동시성 문제를 해결할 때 큰 도움이 된것이라고 믿습니다.
오늘도 긴글 읽어주셔서 감사하며 다음엔 생성자, 소비자 문제에 대해 알아보도록 하겠습니다.
행복한 추석 마무리 잘하세요~ ^^
'Java' 카테고리의 다른 글
[Java] 스레드 풀과 Excutor 프레임워크 (3) | 2024.09.29 |
---|---|
자바의 고오급 동기화 concurrent.Lock (7) | 2024.09.14 |
Java 프로세스와 스레드 (4) | 2024.08.30 |