synchronized 키워드 이상의 동기화 방법을 사용하여 보다 복잡한 멀티스레드 환경에서의 문제를synchronized 단점
BLOCKED 상태의 스레드는 락이 풀릴 때 까지 무한 대기.BLOKCED 상태의 여러 스레드 중에 어떤 스레드가WAITING 상태로 변경.WAITING 상태는 스레드가 특정 조건이나 시간 동안 대기할 때 발생하는 상태(누군가 꺠워줘야 한다.).| 메서드 | 설명 | 시그니처 |
|---|---|---|
park() | 현재 스레드를 차단 (다른 스레드가 unpark() 호출 시까지 대기) | public static void park() |
unpark(Thread thread) | 특정 스레드를 깨움 (해당 스레드가 park() 상태일 경우 차단 해제) | public static void unpark(Thread thread) |
parkNanos(long nanos) | 지정된 나노초 동안 스레드를 차단 | public static void parkNanos(long nanos) |
parkUntil(long deadline) | 특정 시간(밀리초 기준)까지 스레드를 차단 | public static void parkUntil(long deadline) |
park(Object blocker) | 차단될 때의 이유 또는 차단 객체를 지정하여 스레드를 차단 | public static void park(Object blocker) |
package thread.sync.lock;
import java.util.concurrent.locks.LockSupport;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class LockSupportMain {
public static void main(String[] args) {
Thread thread1 = new Thread(new ParkTest(), "Thread-1");
thread1.start();
// 잠시 대기하여 Thread-1이 park 상태에 빠질 시간을 준다.
sleep(100);
log("Thread-1 state: " + thread1.getState());
log("main -> unpark(Thread-1)");
LockSupport.unpark(thread1); // 1. unpark 사용
//thread1.interrupt(); // 2. interrupt() 사용
}
static class ParkTest implements Runnable {
@Override
public void run() {
log("park 시작");
LockSupport.park(); // Runnable -> WAITING
//LockSupport.parkNanos(2000_000000); // parkNanos 사용
log("park 종료, state " + Thread.currentThread().getState());
log("인터럽트 상태: " + Thread.currentThread().isInterrupted());
}
}
}
LockSupport를 사용하면 스레드는 WAITING, TIMED_WAITING 상태로 변경할 수 있고,
interrupt를 받아서 스레드를 깨울 수도 있다. -> synchronized의 단점인 무한 대기 문제 해결 가능
LockSupport를 활용하면, 무한 대기하지 않는 락 기능을 만들 수 있다.
if(!lock.tryLock(10초)) { // 내부에서 parkNanos() 사용
log("[진입 실패] 너무 오래 대기했습니다.");
return false;
}
// 임계 영영 시작
...
// 임계 영역 종료
lock.unlock(); // 내부에서 unpark() 사용
락(lock)이라는 클래스를 만들고, 특정 스레드가 먼저 락을 얻으면 RUNNABLE로 실행하고,
락을 얻지 못하면 park()를 사용해서 대기 상태로 만든다. 스레드가 임계 영역의 실행을 마치고 나면 락을
반납하고, unpark()를 사용해서 대기중인 다른 스레드를 깨운다.
-> 이런 기능을 직접 구현하기는 매우 어렵다.
Lock 인터페이스에서 사용하는 락은 객체 내부에 있는 모니터 락이 아닌, Lock 인터페이스와ReentrantLock이 제공하는 기능.Lock 인터페이스와 ReentrantLock 구현체를 사용하면 synchronized 단점인 무한 대기와 공정성 문제를 모두 해결할 수 있다. | 메서드 | 설명 |
|---|---|
void lock() | 스레드가 락을 획득할 때까지 대기 (즉, 락이 사용 가능해질 때까지 차단) |
void lockInterruptibly() throws InterruptedException | 락을 획득할 때 인터럽트가 발생하면 예외를 던짐 (대기 중 인터럽트를 처리할 수 있음) |
boolean tryLock() | 락을 획득하려 시도하고, 락이 사용 중일 경우 즉시 false 반환 |
boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 주어진 시간 동안 락을 시도하고, 시간이 초과되면 false 반환 |
void unlock() | 락을 해제하여 다른 스레드가 획득할 수 있게 함 |
Condition newCondition() | Condition 객체를 생성하여 락과 함께 사용할 수 있게 함 |
void lock() - 맛집에 한번 줄을 서면 끝가지 기다린다. 친구가 다른 맛집을 찾았다고 중간에
연락해도 포기하지 않고 기다린다.
void lockInterruptibly() - 맛집에 한번 줄을 서서 기다린다. 다만 친구가 다른 맛집을 찾았다고 중간에 연락하면 포기한다.
boolean tryLock() - 맛집에 대기 줄이 없으면 바로 들어가고, 대기 줄이 있으면 즉시 포기한다.
boolean tryLock(long time, TimeUnit unit) - 맛집에 줄을 서지만 특정 시간 만큼만 기다린다.
특정 시간이 지나면 계쏙 줄을 서야 한다면 포기한다. 친구가 다른 맛집을 찾았다고 중간에 연락해도 포기한다.
void unlock() - 식당안에 있는 손님이 밥을 먹고 나간다. 식당에 자리가 하나 난다. 기다리라는 손님께
이런 사실을 알려주어야 한다. 기다리던 손님중 한명이 식당에 들어간다.
자바는 1.0부터 존재한 synchronized와 BLOCKED 상태를 통한 임계 영역 관리의 한계를 극복하기 위해
자바 1.5부터 Lock 인터페이스와 ReentrantLock 구현체를 제공
ReentrantLock의 기본 모드.new ReentranLock(true); -> 공정모드public class ReentrantLock {
// 비공정 모드 락
private final Lock nonFairLock = new ReentranLock();
// 공정 모드 락
private final Lock nonFairLock = new ReentranLock(true);
}
ReentrantLock 활용
package thread.sync;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
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-finally 사용
try {
// ==임계 영영 시작==
log("[검증 시작] 출금액 : " + amount + ", 잔액: " + balance);
if(balance < amount){
log("[검증 실패]" + "출금액 : " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액 : " + amount + ", 잔액: " + balance);
sleep(1000);
balance -= amount;
log("[출금 완료] 출금액 : " + amount + ", 잔액: " + balance);
// ==임계 영영 종료==
} finally {
lock.unlock();
}
log("거래 종료");
return true;
}
@Override
public int getBalance() {
lock.lock(); // ReentrantLock 이용하여 lock을 걸기
try {
return balance;
} finally {
lock.unlock(); // ReentrantLock 이용하여 lock 해제
}
}
}
if(!lock.tryLock(500, TimeUnit.MILLISECONDS)) { // 시간
log("[진입 실패] 이미 처리중인 작업이 있습니다.");
return false;
}
lock() -> unlock() 까지는 안전한 임계 영역이 된다.unlock()은 반드시 finally 블럭에 작성. synchronized의 단점을 해결해줄 LockSupport가 있지만 실제로 구현하기에는 어려운 부분이 있기 때문에
단순하게 편리하게 사용하기에는 synchronized가 좋아보이지만 더 세밀한 동기화 제어와
향상된 성능, 비차단락 획득, 조건 대기 및 신호, 경량화된 동기화, 데드락 회피, 공정성 보장,
인터럽트 가능한 락 대기의 기능을 사용하고싶다면 고급동기화 Lock 인터페이스와 ReentantLock 구현체를 사용해야 한다.
고급동기화를 배움으로서 멀티스레드에 대한 이해가 잘 되고 있는 거 같다.
예제 출처: 김영한의 실전 자바 - 고급1편, 멀티스레드와 동시성