본문 바로가기
Java

Java - Thread 동시성 문제3 (Synchroized)

by sinabeuro 2022. 5. 4.
728x90

안녕하세요.

Thread 동시성 문제 3편 시작하겠습니다.

 

동시성 문제 개념과 원인은 앞서 다룬 포스트를 참고하시면 좋을 것 같습니다.

https://getthismoment.tistory.com/155

 

Synchroized

동시성 문제를 해결하는 방법으로 synchroized를 활용하면 됩니다.

synchroized 를 사용해서 스레드에 lock 을 걸어 동기화를 시킵니다.

lock을 얻은 스레드의 synchroized가 진행되는 동안 다른 스레드는 대기 상태에 놓입니다.

 

lock을 거는 방식을 사용하기 때문에 당연히 성능은 떨어지게 됩니다.

대신 동기화를 통해 스프링에서 객체 주입 받아온, 싱글톤으로 구현된 객체가

멀티 스레드 내에서 Thread Safe를 보장받을 수 있게 됩니다. 

 

동기화에 관한 용어는 아래 이미지를 참고하시면 될 것 같습니다.

https://www.crocus.co.kr/1558 일부발췌

 

 

Synchroized 구현 2가지 방법

1. 메소드에 synchroized 거는 방식

ex) public synchroized void test() { ... }

이 방법은 메소드 전체에 lock을 걸어 동기화를 진행하는 방식입니다.

메소드 전체에 lock을 걸기 때문에 아래의 방법 보다는 약간 성능이 떨어집니다.

 

2. synchroized 블록을 생성하는 방식

ex) synchroized (this) { ... } 

메소드 안에서 synchroized 블록을 생성해서 스코프 내에서 lock을 걸고 동기화를 진행하는 방식입니다.

 

Synchroized 대표적인 메소드

synchronized 내에서 사용할 수 있는 메소드는 다음과 같습니다.

- wait() : lock 을 풀고 통지를 받을 때까지 기다리다가 통지(notify)를 받으면 다시 lock을 얻어 동기화를 진행한다.

            InterruptedException 처리가 필수적입니다.

- notify() : wait 상태의 스레드를 다시 lock 주어 동기화를 진행하게 한다.

- notifyAll 모든 wait 상태의 스레드를 다시 lock 주어 동기화를 진행하게 한다.

 

운이 나쁘게도 notify를 받지 못한 스레드는 계속해서 대기 상태에 놓일 수 있는데, 이를 '기아 현상(starvation)'이라고 합니다.

기아 현상을 막기 위해 notifyAll을 사용하게 된다면, 모든 스레드가 lock을 얻기 위한 '경쟁 상태(race condition)'에 놓일 수 있습니다.

 

이 처럼 synchroized 동기화를 통해서 Thead 동시성 문제를 해결할 수 있지만, 

wait() & nofity()로는 선별적인 통지가 불가능합니다.

즉, 동일 api 함수가 계속해서 호출된다면 synchroized 가 구현된 상황에서 기아 현상 또는 경쟁 상태가 발생할 가능성이 높다는 것입니다.

 

이러한 synchroized 문제를 해결하기 위해 선별적 통지가 가능한 ReentrantLock 이나 Fork/Join을 사용하면 됩니다.

ReentrantLock 이나 Fork/Join은 나중에 시간이 여유가 생기면 다루어보겠습니다.

 

 

Synchronized 예제

이전부터 소개한 입금/출금 예제를 활용했습니다.

@Slf4j
public class BankBook {

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

    // 입금
    public void deposit() {
        String name = Thread.currentThread().getName();
        while(true) {
            synchronized (this) {
                try {
                    Thread.sleep(100);
                    balance += 400;
                    wait();
                } catch (InterruptedException e) {}

                System.out.println(name + " [입금] 남은 금액: " + balance);
            }
        }
    }

    // 출금
    public void withdraw() {
        String name = Thread.currentThread().getName();
        while(true) {
            synchronized (this) {
                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);
                } else {
                    notifyAll();
                }
            }
        }
    }
}

이전 포스트에서 다뤘던 예제와 거의 동일하지만,

synchronized 블록(동기화 블록)이 추가되었습니다.

@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);
    }
}

 

스레드 실행 결과

이제 드디어 남은 금액이 출금액 보다 적을 떄 출금되지 않습니다!!

다시말해 Thread 동시성 문제가 해결된 것이죠.

 

이제 동기화를 통해서 멀티 스레드에서 Thread Safe를 보장하게 되었습니다.

하지만 성능은 이전 보다 떨어지게 됩니다 ㅠㅠ

 

여기까지 Thread 동시성 문제에 대해서 알아보았습니다!!

 

 

728x90

'Java' 카테고리의 다른 글

JAVA - 리플렉션 getField, getDeclaredField 차이  (0) 2023.05.20
Java - 람다 스트림 예제  (0) 2023.02.10
Java - Thread 동시성 문제2 (ThreadLocal )  (0) 2022.05.04
Java - Thread 동시성 문제1  (0) 2022.05.04
Jave - Thread 개괄  (0) 2022.05.03

댓글