개발일지/Java

Java 스레드 동기화

E-room 2022. 9. 25. 12:16
728x90

싱글 스레드 프로세스 : 데이터에 단 하나의 스레드만 접근하기 때문에 상관없음

멀티 스레드 프로세스 : 두 스레드가 동일한 데이터를 공유하게 되어 문제 발생 가능성 존재함

 

하나의 계좌에서 현금을 출금한다고 가정한다

더보기
package 스레드;

public class Main {
    public static void main(String[] args) {

        Runnable threadTask = new ThreadTask();
        Thread thread1 = new Thread(threadTask);
        Thread thread2 = new Thread(threadTask);

        thread1.setName("김이룸");
        thread2.setName("이자바");

        thread1.start();
        thread2.start();
    }
}

class Account {
    // 잔액을 나타내는 변수
    private int balance = 1000;

    public int getBalance() {
        return balance;
    }

    // 인출 성공 시 true, 실패 시 false 반환
    public boolean withdraw(int money) {
        // 인출 가능 여부 판단 : 잔액이 인출하고자 하는 금액보다 같거나 많아야 합니다.
        if (balance >= money) {
            // if문의 실행부에 진입하자마자 해당 스레드를 일시 정지 시키고,
            // 다른 스레드에게 제어권을 강제로 넘깁니다.
            // 일부러 문제 상황을 발생시키기 위해 추가한 코드입니다.
            try {
                Thread.sleep(1000);
            } catch (Exception error) {
            }
            // 잔액에서 인출금을 깎아 새로운 잔액을 기록합니다.
            balance -= money;
            return true;
        }
        return false;
    }
}

class ThreadTask implements Runnable {
    Account account = new Account();

    public void run() {
        while (account.getBalance() > 0) {
            // 100 ~ 300원의 인출금을 랜덤으로 정합니다.
            int money = (int) (Math.random() * 3 + 1) * 100;
            // withdraw를 실행시키는 동시에 인출 성공 여부를 변수에 할당합니다.
            boolean denied = !account.withdraw(money);
            // 인출 결과 확인
            // 만약, withraw가 false를 리턴하였다면, 즉 인출에 실패했다면,
            // 해당 내역에 -> DENIED를 출력합니다.
            System.out.println(String.format("출금 %d₩ By %s. 잔액 : %d %s",
                    money, Thread.currentThread().getName(), account.getBalance(), denied ? "-> 거부" : "")
            );
        }
    }
}
// 출력
출금 100₩ By 이자바. 잔액 : 700 
출금 200₩ By 김이룸. 잔액 : 700 
출금 200₩ By 이자바. 잔액 : 500 
출금 200₩ By 김이룸. 잔액 : 300 
출금 300₩ By 이자바. 잔액 : 0 
출금 300₩ By 김이룸. 잔액 : -300

 

출력 결과를 보면 정상적이지 않다

  1. 인출금과 잔액이 정상적이지 않다
  2. if (balance >= money) 조건문이 무시된 것처럼 음수 잔액이 발생한다
  3. -> 거부 가 제대로 출력되지 않는다

이는 두 스레드 간에 객체가 공유되기 때문에 발생하는 오류이다.

 

이러한 상황이 발생하지 않게 하기 위해 스레드 동기화를 사용한다

 

임계 영역(Critical section)과 락(Lock)

  • 임계 영역 : 오직 하나의 스레드만 코드를 실행할 수 있는 코드 영역
  • 락 : 임계 영역을 포함하고 있는 객체에 접근할 수 있는 권한
  1. 임계 영역으로 설정된 객체가 다른 스레드에 의해 작업이 이루어지지 않을 때, 임의의 스레드 A는 해당 객체에 대한 락을 획득하여 임계 영역 내의 코드를 실행할 수 있다
  2. 스레드 A가 임계영역임계 영역 내의 코드를 실행 중일 때, 다른 스레드들은 락이 없으므로 임계 영역 내의 코드를 실행할 수 없음
  3. 스레드 A가 코드를 모두 실행한 뒤 락을 반납하면 다른 스레드들 중 하나가 락을 획득하여 임계 영역 내의 코드를 실행

 

오류를 막기 위해서

synchronized 키워드를 사용

임계영역을 설정해주어서 두 스레드가 동시에 실행하면 안 되는 구간을 설정해준다

 

1. 메서드 전체를 임계 영역으로 지정

public synchronized boolean withdraw(int money) {
    if (balance >= money) {
        try {
            Thread.sleep(1000);
        } catch (Exception error) {}
        balance -= money;
        return true;
    }
    return false;
}

 

2. 특정 영역을 임계 영역으로 지정

public boolean withdraw(int money) {
    synchronized (this) { // 임계 영역 지정
        if (balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (Exception error) {}
            balance -= money;
            return true;
        }
        return false;
    }
}
728x90