synchronized

황상익·2024년 10월 7일

Inflearn JAVA

목록 보기
46/61

출금 시작 예제

멀티 스레드 사용할때 가장 주의해야 할 점은 같은 자원에 여러 스레드가 동시에 접근할 때 발생하는 동시성 문제. 참고롤 여러 스레드가 접근하는 자원을 공유자원이라 함

public interface BankAccount {
    boolean withdraw(int amount);
    int getBalance();
}
//출금 -> 악의적 사용
//동시에 같은 계좌에 출금
public class WithdrawTask implements Runnable {

    private BankAccount account;
    private int amount;

    public WithdrawTask(BankAccount account, int amount) {
        this.account = account;
        this.amount = amount;
    }

    @Override
    public void run() {
        account.withdraw(amount);
    }
}
public class BankAccountV1 implements BankAccount {

    private int balance;

    //volatile private int balance;
    public BankAccountV1(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public boolean withdraw(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;
    }
}
public class BankMain {
    public static void main(String[] args) throws InterruptedException {
        //BankAccount bankAccount = new BankAccountV1(1000);
        //BankAccount bankAccount = new BankAccountV2(1000);
        BankAccount bankAccount = new BankAccountV3(1000);

        Thread t1 = new Thread(new WithdrawTask(bankAccount, 800), "t1");
        Thread t2 = new Thread(new WithdrawTask(bankAccount, 800), "t2");
        t1.start();
        t2.start();

        sleep(50);
        log("t1 state : "  + t1.getState());
        log("t2 state : "  + t2.getState());

        t1.join();
        t2.join();
        log("최종 잔액 : "  + bankAccount.getBalance());
    }
}


t1, t2의 스레드는 withdraw()를 실행함.
t1, t2 스레드는 BankAccount 인스턴스 withdraw 메서드 호출
두 스레드는 같은 BankAccount(x001) 인스턴스에 접근하고 또 x001 인스턴스에 있는 잔액
(balance) 필드도 함께 사용

동시성 문제
악의적 사용자가 2대 PC 사용해 이중출금 시도
원래는 검증을 통해 멈춰야 하지만, 멈추지 않고 잔액이 부족함에도 출금이 되는 상황 발생

if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}

이부분이 문제 !!

volatile을 사용하면? -> volatile은 메모리 가시성을 확보해주는 것이지, 동시성 문제와는 또 다른 문제이다.

동시성 문제

t1, t2 순서로 실행 가정

t1 실행 -> 출금 시도
t1 출금 코드에 있는 검증 로직 실행, -> 잔액 확인 -> 검증 로직 통과
t1은 출금 로직을 통과해 잠시 대기, 출금에 걸리는 시간.
t2는 검증 로직을 실행 -> 검증 로직 실행 -> 잔액 1000이 출금액 800 보다 많으므로 통과한다.

t1이 아직 balance를 줄이지 못한 상황에 t2는 검증로직에서 현재 잔액을 1000원으로 함

그러면 sleep을 사용하면?
balance = balance - amount를 계산하기 직전에 t2가 실행 되면서 검증 로직을 통과


t1, t2 모두다 통과, 출금을 위해 잠시 대기

t2 까지 출금이 된다면, 잔액은 -가 된다.

t1, t2 동시에 실행 가정



동시에 출금이 되면 200원
두 스레드 모두 1000원으로 조회
2단계 모두 1000 - 800을 계산해 200이라는 결과를 도출

임계영역

검증 단계에서 확인한 잔액은 1000원, 출금 단계에서 계산을 끝마칠 때 까지 1000원으로 유지 되어야 한다.
만약 중간에 다른 스레드가 잔액의 값을 변경한다면, 큰 혼란이 발생

공유자원

balance는 여러 스레드가 함께 공유하는 자원, 출금 로직을 실행하는 중간에 다른 스레드에서 언제든 값 변경 가능. -> 잔액이 공유되는 자원이기 때문에 변경된다.

한번에 하나의 스레드만 실행

t1의 송금이 완료된 후, t2의 스레드가 실행이 된다면, t2의 스레드가 처음부터 끝까지 출금 메서드를 완료

임계영역

여러 스레드가 동시에 접근하면 데이터 불일치나 예상치 못한 동작이 발생할 수 있는 위험하고 또 중요한 코드 부분을 뜻한다.
여러 스레드가 동시에 접근해서는 안 되는 공유 자원을 접근하거나 수정하는 부분

그렇다면 임계영역은 출금 부분

synchronized 메서드

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 = balance - amount;
        log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
        //== 임계 영역 ==

        log("거래 종료");
        return true;
    }

    @Override
    public synchronized int getBalance() {
        return balance;
    }
}

withdraw, getBalance 메서드를 하나의 한번에 하나의 스레드만 사용 가능

synchronized 분석


모든 객체는 내부에 자신만의 Lock을 소유 (=모니터 락)
메서드에 진입하려면 반드시 해당 인스턴스에 락이 있어야 한다.


t1은 인스턴스에 있는 Lock을 획득


t2의 경우 락을 획득하기 전까지 Block
withdraw, getBalance 부분은 t1만 접근이 가능, t2는 접근 X

t1 락 반납, t2 락 획득 후, 진행이 됨

따라서 t2의 경우 검증로직에 걸려 진행이 되지 않는다.

public class BankAccountV3 implements BankAccount {

    private int balance;

    public BankAccountV3(int initialBalance) {
        this.balance = initialBalance;
    }

    @Override
    public boolean withdraw(int amount) {
        log("거래 시작: " + getClass().getSimpleName());

        synchronized (this) { //lock을 얻는데 지정을 해줘야 한다.
            //== 임계 영역 ==
            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 synchronized int getBalance() {
        return balance;
    }
}

synchronized (this) { //lock을 얻는데 지정을 해줘야 한다.
//== 임계 영역 ==
log("[검증 시작] 출금액: " + amount + ", 잔액: " + balance);
if (balance < amount) {
log("[검증 실패] 출금액: " + amount + ", 잔액: " + balance);
return false;
}
log("[검증 완료] 출금액: " + amount + ", 잔액: " + balance);
sleep(1000);
balance = balance - amount;
log("[출금 완료] 출금액: " + amount + ", 변경 잔액: " + balance);
//== 임계 영역 ==
}

임계영역을 따로 설정해 최적화 해서 사용 가능
synchronized (this) -> 괄호 안으로 들어가면 락을 획득할 인스턴스의 참조

알아야 할 것들

  • 지역변수의 경우 멤버변수 즉 필드 값들과는 다르게 stack영역에 생성, stack영역의 경우 스레드를 공유하지 않기 때문에 thread 동시성 문제 없음
  • final 사용시 필드값 변경이 불가능 하기 때문에 스레드 동시셩 문제 고려 사항 X
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글