본문 바로가기
Java

Java - Thread 동시성 문제1

by sinabeuro 2022. 5. 4.
728x90

이전 포스트와 이어서 이번에도 스레드를 다루어보겠습니다.

https://getthismoment.tistory.com/152

 

스레드의 정의는 알겠는데 그런데 왜 스레드를 배워야할까요?

 

Thread를 왜 배워야할까?

스레드를 배워야하는 이유는 크게 다음과 같습니다.

1. 우리는 멀티 스레드를 지원하는 자바를 사용하고 있습니다.

2. 스프링의 객체관리 기능(빈 등록 및 빈의 모든 생명주기 관리)이 싱글톤 패턴을 베이스로 하고 있습니다.

3. 객체 주입을 통해 가져온 객체를 수정하게 된다면 그 객체는 멀티 스레드에서 동기화를 보장할 수 없습니다.

Thread Safe
멀티 스레드의 서로 다른 스레드에서 같은 메소드를 호출해도 동일한 결과를 보장하는 것.

즉, 스프링에서 Rest api를 통해서 함수를 호출하게 된다면, 아무 처리없이는 멀티 스레드의 Thread Safe를 보장할 수 없습니다. 

이로 인해 스레드의 동시성 문제가 발생합니다.

동시성 문제란 1초 안에 동일한 함수가 호출 된다면 동기화(synchroized) 되지 못한 엉뚱한 결과값을 받게 되는 것입니다.

코드 예제를 보면서 동시성 문제를 다시 살펴보겠습니다.

 

 

Thread 동시성 문제 예제

Thread에서 나름 유명한 예제 중하나를 소개합니다.

통장에서 입금, 출금하는 예제인데요.

통장에 남은 금액이 출금액 이상일 때 출금이 가능합니다.

조건이 있음에도 불구하고 결과값은 우리가 예상하지 못한 값이 나올 것입니다.

public class BankBook {

    private int balance = 0;    // 남은 금액

    // 입금
    public void deposit() {
        String name = Thread.currentThread().getName();
        while(true) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {}
            balance += 400;
            System.out.println(name + " [입금] 남은 금액: " + balance);
        }
    }

    // 출금
    public void withdraw() {
        String name = Thread.currentThread().getName();
        while(true) {
            int randomMoney = (int) Math.ceil(Math.random() * 10)*100; // 100~1000 자연수
            if(balance >= randomMoney) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {}
                balance -= randomMoney;
                System.out.println(name + " [출금] 남은 금액: " + balance);
            }
        }
    }
}

위의 코드는 통장 코드이며, 입금과 출금 메소드가 있습니다.

출금액이 남은 금액 이상일 때 가능하다면 출력되는 balance 값이 양수만 출력되겠죠??

결론을 미리 말씀드리자면 balance 값은 음수도 출력됩니다.

 

@Slf4j
@RequiredArgsConstructor
public class DepositThread implements Runnable {

    private final BankBook bankBook;

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        log.info("{} (입금 스레드)", name);
        bankBook.deposit();
    }
}

위의 코드는 입금 스레드 코드입니다.

 

@Slf4j
@RequiredArgsConstructor
public class WithdrawThread implements Runnable {

    private final BankBook bankBook;

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        log.info("{} (출금 스레드)", name);
        bankBook.withdraw();
    }
}

위의 코드는 출금 스레드 코드입니다.

 

public class BankBookTest {

    private BankBook bankBook = new BankBook();

    @Test
    public void test() throws InterruptedException {
        Thread depositThread = new Thread(new DepositThread(bankBook));
        depositThread.setName("deposit Thread  ");
        Thread withdrawThread1 = new Thread(new WithdrawThread(bankBook));
        withdrawThread1.setName("withdraw Thread1");
        Thread withdrawThread2 = new Thread(new WithdrawThread(bankBook));
        withdrawThread2.setName("withdraw Thread2");

        depositThread.start();
        withdrawThread1.start();
        Thread.sleep(100);
        withdrawThread2.start();

        Thread.sleep(2000);
    }
}

이제 쓰레드 만들어 실행해보면 다음과 같은 결과가 나옵니다.

스레드 결과값

통장 스레드 예제 코드를 실행하면 매순간 결과값이 다르겠지만, 위와 같이 남은 금액이 마이너스가 출력되는 현상이 나타납니다.

분명 조건문에 출금액이 남은 금액이상일 때만 출금이 가능한데 말이죠.

 

1초 안에 동일한 객체 (BankBook) 를 여러번 호출해서 값을 변경해서 사용하게 된다며,

멀티 스레드 내 스레드들의 각각 동일한 객체에 대해서 Thread Safe를 보장하지 못합니다. 

이를 Thread 동시성 문제라고 합니다.

 

depositThread, withdrawThread1, withdrawThread2 객체들을 우리가 호출하는 api라고 생각하고,

BankBook 객체를 각 api 에서 주입받아 사용하고 있는 객체라고 보시면,

스레드 동시성 문제에 대해 감이 오실 것입니다.

 

주입받아 사용하는 객체의 변경이 없는 경우에는 동시성 문제는 신경쓰지 않으셔도 됩니다.

하지만 주입받아 사용하는 객체를 변경해서 사용한다면 동시성 문제를 고려해서 코드를 짜야합니다.

 

동시성 문제 해결에 대해서는 다음 포스트에서 다루겠습니다.

728x90

'Java' 카테고리의 다른 글

Java - Thread 동시성 문제3 (Synchroized)  (0) 2022.05.04
Java - Thread 동시성 문제2 (ThreadLocal )  (0) 2022.05.04
Jave - Thread 개괄  (0) 2022.05.03
Java - 객체 지향 설계 원칙 SOLID  (0) 2022.04.09
Java - JUnit과 Mock  (0) 2021.09.27

댓글