Java adv1 - 동시성 문제3 / synchronized

dev1·2024년 11월 28일

synchronized 키워드를 어떻게 사용할 수 있을까?

그냥, synchronized 를 써야하는 코드블럭에 키워드를 넣어주면된다.

즉, synchronized 키워드는 락을 가져가는 방식으로 한번에 하나의 스레드만 작동할 수 있게 한다.

======================================

package thread.sync;

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);
        log("[처리중]");
        sleep(1000);

        balance -= amount;
        log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);
        log("거래를 종료합니다.");
        return true;
    }

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

}

이렇게 작성하게되면, withdraw ,getBalance 메서드는 synchronized 에 적용되어져 있는 상태

==> 코드를 실행하는것은 한개의 메서드만 가능하고, 나머지 스레드는 실행하고있는 스레드가 끝나면 접근 ㄱㄴ

이런식으로 하나의 스레드만 접근할 수 있게 해주는게 synchronized !!

==========================================

실행 결과를 확인해보자.

17:15:01.067 [       t1] 거래 시작 : BankAccountV2
17:15:01.071 [       t1] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 1000
17:15:01.071 [       t1] [검증 완료] 출금액 : 800 보유중인 금액 : 1000
17:15:01.071 [       t1] [처리중]
17:15:01.560 [     main] t1 state : TIMED_WAITING
17:15:01.562 [     main] t2 state : BLOCKED
17:15:02.077 [       t1] [검증 완료] 출금액 : 800 출금 후 잔액 : 200
17:15:02.078 [       t1] 거래를 종료합니다.
17:15:02.078 [       t2] 거래 시작 : BankAccountV2
17:15:02.079 [       t2] [검증을 시작합니다] 출금액 : 800 보유중인 금액 : 200
17:15:02.081 [       t2] [검증 실패] 출금액 : 800 보유중인 금액 : 200, 잔액 부족입니다.
17:15:02.085 [     main] 최종 잔액 : 200

===> ㅇㅋ 로직짠대로 잘 작동

================================

그렇다면, 어떻게해서 이런식으로 작동하게 되는걸까? ( 어떻게 한개의 스레드만 실행할 수 있게 하는걸까? )

=====> 객체에 접근하려면, 락이 필요하다 !!!!!!!!!!!!!!

우선, 객체를 생성할때 락 이라는것이 생성된다는것을 알고있자. ( 접근할 수 있는 카드키 ? )

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

public class BankMain2 {

    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccountV2(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());
    }
}

account 라는 객체가 만들어질때, 생성자를 통해서 값들이 생성되기도하고, 추가로 객체안에 락 을 생성한다.

그렇다면, 이 락을 통해서 어떻게 하게될까?

즉, synchronized 가 어떻게해서 작동해야할까?

===> 실행할 스레드가 객체에 있는 락을 가져가면된다. ( 락은 하나의 객체에 1개만 존재 )

==> 락이 없으면 접근이불가능하니, 먼저 실행할 스레드가 락을 가지고있으면 해당 객체에는 락이 존재하지 않는상태.

==> 다른 스레드는 해당 객체에 접근이 불가능함.

==> 이후에, 실행하고있던 스레드가 작업을 끝내고나면, 다시 객체에 락을 반환

==> 또 다른 스레드가 접근하고 => 또 실행, => 실행 끝 => 다시 락 객체에 반환 ...

==> 이러한 과정을 거쳐서, 한번에 하나의 스레드만 접근할 수 있게된다.

즉, synchronized 키워드는 락을 가져가는 방식으로 한번에 하나의 스레드만 작동할 수 있게 한다.

===> synchronized 키워드를 사용하게되면, volatile 을 사용하지 않아도 문제가없다 .

( 메모리가시성 문제도 해결 ).
왜 ? ==> 메모리가시성에 대한 문제가,

캐시메모리에 있던값을 메인메모리에 반영하는 도중에, 다른 스레드이 접근하게되면서 문제가 생기게됐던것이니 ...

synchronized 는 한번에 하나의 스레드만 접근할 수 있게됨 ( 작업이 완료되고, 다른 스레드가 접근 가능 )

==> 딜레이가 생기지 않음 애초에 ( 다 반영하고 다른스레드가 접근할 수 있게하는것이니 )

이런식으로 장점이 굉장히 많아보이는데,

synchronized 는 사용에 굉장히 제한적으로 사용해야한다.

왜냐하면, 멀티스레드가 연산속도를 굉장히 빠르게 해주게 되는것인데 ....

==> 멀티스레드의 장점을 없애버리는것이니까, 꼭 필요한 곳에만 synchronized 를 활용하자.

package thread.sync;

import static util.MyLogger.log;
import static util.ThreadUtils.sleep;

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) {
            log("[검증을 시작합니다] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            if ( balance < amount ) {
                log("[검증 실패] 출금액 : " + amount + " 보유중인 금액 : " + balance + ", 잔액 부족입니다.");
                return false;
            }

            // 출금가능상태
            log("[검증 완료] 출금액 : " + amount + " 보유중인 금액 : " + balance);
            log("[처리중]");
            sleep(1000);
            balance -= amount;
            log("[검증 완료] 출금액 : " + amount + " 출금 후 잔액 : " + balance);
        }


        log("거래를 종료합니다.");
        return true;
    }

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

}

이렇게 꼭 필요한 부분만 synchronized 를 사용하자.

0개의 댓글