멀티스레드 사용 시 가장 주의할 점은
여러 스레드가 같은 자원에 동시에 접근할 때 발생하는 동시성 문제다.
대표적인 공유 자원은 인스턴스의 필드(멤버 변수) 다.
synchronized 키워드를 붙이면, 한 번에 하나의 스레드만 실행할 수 있는 코드 구간을 만들 수 있다.
public class BankAccountV2 implements BankAccount {
private int balance; // 잔액
public BankAccountV2(int balance) {
this.balance = balance;
}
@Override
public synchronized boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
sleep(1000); // 출금 소요 시간 1초
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
log("거래 종료");
return true;
}
@Override
public synchronized int getBalance() {
return balance;
}
}
모든 인스턴스는 자신만의 Lock 을 가지고 있다.
- 이것을 모니터 락(monitor lock) 으로도 불린다.
- 객체 내부에 있는데 개발자가 확인하기는 어렵다.
- t1 스레드 작업이 종료되면, synchronized 블럭을 나갈 때, BankAccount 인스턴스의 락을 반납한다.
- BLOCKED 상태로 락 획득을 대기하는 t2 스레드는 자동으로 락을 획득한다.
- t2 스레드 상태변경: BLOCKED → RUNNABLE
- [참고] BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다.
- t2 스레드는 검증 로직을 통과하지 못한다. 락을 반납하면서 return 한다.
- volatile 를 사용하지 않아도, synchronized 안에서 접근하는 변수의 메모리 가시성 문제는 해결된다.
BLOCKED 상태 스레드는
- 락이 풀릴 때 까지 무한 대기한다
- 중간에 인터럽트를 걸 수 없다
- BLOCKED 상태인 스레드가 여러개일 때, 락을 획득하는 순서는 보장되지 않는다
- → 순서를 모르니까 특정 스레드가 엄청 오래 기다리게 될 수도 있다.
synchronized 코드 블럭
synchronized 는 전체적으로 보면 성능이 떨어질 수 있다.
따라서 동시에 실행할 수 없는 코드 구간은 꼭! 필요한 곳으로 한정해서 설정해야 한다.
특정 코드 블럭만 synchronized 를 붙이자
- 메서드 단위가 아니라 특정 블럭에 최적화하여 적용할 수 있다.
public boolean withdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
synchronized (this) {
log("[검증 시작] 출금액: " + amount + ", 잔액:" + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액:" + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액:" + balance);
sleep(1000); // 출금 소요 시간 1초
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액:" + balance);
}
log("거래 종료");
return true;
}
- synchronized (this) {} 임계 영역을 코드 블럭으로 지정한다
- synchronized (this) : 여기서 괄호 안에 들어가는 값은 락을 획득할 인스턴스의 참조다.
- 여기서는 BankAccountV3(x001) 의 인스턴스의 락을 사용하므로 이 인스턴스의 참조인 this 를 넣는다.