Java. 동기화 - synchronized
Synchronized
synchronized 메서드
import static util.MyLogger.log;
import static util.ThreadUtils.sleep;
public class BankAccountV2 implements BankAccount {
private int balance;
public BankAccountV2(int initialBalance) {
this.balance = initialBalance;
}
@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); // 출금에 걸리는 시간으로 가정
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public synchronized int getBalance() {
return balance;
}
}- 모든 객체(인스턴스)는 내부에 자신만의 락
lock을 가지고 있다.- 모니터 락 (monitor lock) 이라고도 부른다.
- 스레드가
synchronized키워드가 있는 메서드에 진입하려면 반드시 해당 인스턴스의 락이 있어야한다. synchronized메서드에 처음 접하는 스레드는lock을 획득하고 실행된다.- 이후 실행되는 스레드는
lock이 없으므로RUNNABLE상태에서BLOCKED로 변하고, 락을 획득할 때 까지 무한정 대기한다. - 락을 가진 메서드가 끝나면
lock을 반납하고, 락 획득을 대기하고 있는 (무한정 대기하는) 스레드가 자동으로lock을 획득하고RUNNABLE상태가 되고 코드를 실행한다.
참고: 락을 획득하는 순서는 보장되지 않는다.
참고: volatile 키워드를 사용하지 않아도 synchronized 키워드를 사용했기 때문에 메모리 가시성 문제가 해결된다.
synchronized 코드 블럭
synchronized 메서드는 하나의 스레드만 실행할 수 있기 때문에, 성능이 떨어질 수 있다.
진짜 임계 영역은 안래와 같다.
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
// 잔고가 출금액 보다 많으면, 진행
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금에 걸리는 시간으로 가정
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);자바는 이런 문제를 해결하기 위해 특정 코드 블럭도 제공한다.
@Override
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); // 출금에 걸리는 시간으로 가정
balance -= amount;
log("[출금 완료] 출금액: " + amount + ", 잔액: " + balance);
// == 임계 영역 종료 ==
}
log("거래 종료");
return true;
}괄호()안에 들어가는 값은 락을 획득할 인스턴스의 참조이다.
핵심은 하나의 스레드만 실행할 수 있는 안전한 임계 영역은 가능한 최소한의 범위에 적용해야 한다.
이런 동기화를 사용하면 다음 문제들을 해결할 수 있다.
- 경합 조건
- 데이터 일관성
synchronized의 공유 변수 문제는 지역변수를 제외한, 인스턴스 변수와 클래스 변수에만 해당 된다.
지역 변수는 각 스레드의 스택 프레임안에 존재하기 때문에, 다른 스레드의 지역 변수에는 접근할 수 없다.
final 필드의 공유 변수는 문제가 될까?
아니다. 여러 스레드가 공유 자원을 값을 변경해버리기 때문에 문제가 발생하는 것인데, final은 항상 같은 값을 읽기 때문에,
멀티스레드 상황에 문제 없는 안전한 공유 자원이 된다.
synchronized는 매우 편리하지만, 제공하는 기능이 너무 단순하다는 단점이 있다.
멀티스레드가 더 중요해지고 더 복잡한 동시성 개발 방법이 필요해졌다.
synchronized 단점:
- 무한 대기 :
BLOCKED상태의 스레드는 락이 풀릴 때 까지 무한 대기한다.- 특정 시간까지만 대기하는 타임아웃 X
- 중간에 인터럽트가 걸려도 계속 대기
- 공정성: 락이 돌아왔을 때
BLOCKED상태의 여러 스레드 중 어떤 스레드가 락을 획득할 지 알 수 없다. 최악의 경우 특정 스레드가 너무 오랜 기간 락을 획득하지 못할 수 있다.
웹 애플리케이션 경우 고객이 요청을 했는데, 화면에 계속 요청 중만 뜨고, 응답을 못받는 것이다.
너무 오랜 시간이 지나면, 시스템에 사용자가 많아서 다음에 다시 시도해달라고 하는 식의 응답을 주는 것이 더 낫다.
자바 1.5 부터는 java.util.concurrent라는 동시성 문제 해결을 위한 패키지가 추가 된다.
편리하게 사용하기에는 synchronized가 좋으므로, 목적에 부합하면 synchronized를 사용하면 된다.