
멀티스레드를 사용할 때 가장 주의해야 할 점은 여러 스레드가 공유 자원에 동시에 접근하는 것으로 인해 발생하는 동시성 문제입니다.
대표적인 공유 자원은 인스턴스 필드(멤버 변수)입니다.
만약 여러 스레드가 동시에 공유 자원을 읽고 수정한다면, 데이터 불일치나 예상치 못한 동작이 발생할 수 있습니다.
아래는 은행 계좌에서 출금을 처리하는 BankAccountV1과 BankAccountV2 클래스입니다.
BankAccountV1public class BankAccountV1 implements BankAccount {
private int balance;
public BankAccountV1(int initialBalance) {
this.balance = initialBalance;
}
@Override
public boolean widthdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금 처리 시간
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public int getBalance() {
return balance;
}
}
아래처럼 두 개의 스레드(t1, t2)가 동시에 출금을 시도하면 문제가 발생할 수 있습니다.
public static void main(String[] args) throws InterruptedException {
BankAccount account = new BankAccountV1(1000);
Thread t1 = new Thread(new WithdrawTask(account, 800), "t1");
Thread t2 = new Thread(new WithdrawTask(account, 800), "t2");
t1.start();
t2.start();
sleep(500);
log("t1 state: " + t1.getState());
log("t2 state: " + t2.getState());
t1.join();
t2.join();
log("최종 잔액: " + account.getBalance());
}
t1과 t2가 거의 동시에 balance를 확인합니다. (balance = 1000)t1은 800원을 출금하기 위해 검증을 통과합니다.t2도 같은 시점에서 800원을 출금할 수 있다고 판단합니다.t1이 출금을 완료하여 잔액이 200원이 됩니다.t2도 출금을 시도하여 잔액이 -600원이 되어버립니다. ❌(비정상 동작)synchronized 키워드 사용멀티스레드 환경에서 한 번에 하나의 스레드만 실행하도록 하려면 임계 영역(critical section)을 보호해야 합니다.
Java에서는 synchronized 키워드를 사용하여 공유 자원에 대한 동기화를 적용할 수 있습니다.
BankAccountV2public class BankAccountV2 implements BankAccount {
private int balance;
public BankAccountV2(int initialBalance) {
this.balance = initialBalance;
}
@Override
public synchronized boolean widthdraw(int amount) {
log("거래 시작: " + getClass().getSimpleName());
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000); // 출금 처리 시간
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
log("거래 종료");
return true;
}
@Override
public int getBalance() {
return balance;
}
}
synchronized가 하는 일synchronized 키워드를 메서드에 추가하면 한 번에 하나의 스레드만 해당 메서드를 실행할 수 있습니다.widthdraw() 메서드가 실행 중이면 다른 스레드는 해당 메서드에 접근할 수 없습니다.balance 값이 변경되는 과정이 안전하게 보호됩니다.t1이 balance를 확인하고 출금을 시작하면, t2는 대기합니다.t1이 출금을 완료한 후 t2가 실행되므로, t2는 출금 불가(잔액 부족) 상태를 올바르게 인식합니다.| 해결 방법 | 동시성 문제 발생 여부 |
|---|---|
BankAccountV1 (동기화 X) | ✅ 발생 (출금 중간에 다른 스레드 개입 가능) |
BankAccountV2 (synchronized 사용) | ❌ 해결 (한 번에 하나의 스레드만 실행) |
멀티스레드 환경에서 공유 자원에 대한 동기화는 필수입니다. synchronized를 사용하면 임계 영역을 보호하여 데이터 불일치를 방지할 수 있습니다. 🚀
김영한님의 강의를 참고해서 만들었습니다.