만족은 하되 안주하지는 말자

기록해야 기억한다

프로그래밍/JAVA

[JAVA] Thread 로 은행계좌 문제 (Bank Account Problem)를 확인해보자.

D36choi 2020. 9. 18. 22:06
728x90

쓰레드란?

프로세스 내 하나의 실행 흐름 단위이다.

각 프로세스는 최소 1개 이상의 쓰레드를 가진다. (만약 그 쓰레드가 1개인 경우, 그 쓰레드를 메인 쓰레드라 한다)

멀티 스레드를 잘 구성한다면, 멀티 프로세스로 구성할 때에 비해 메모리 공유량이 줄어들어 시스템 자원 소모가 줄어든다. Context Switching 에 대한 오버헤드도 줄어든다.

하지만 프로그래머가 골치를 썩일 문제 또한 많은데 그런 예시 중 대표적인게 "은행 계좌 문제", "식사하는 철학자 문제" 이다. 이중 은행계좌 문제로 알 수 있는 건, Critical Section 문제다. 임계구역 문제인데 같은 자원을 서로 다른 쓰레드가 점유하고자 해서 생기는 문제점이다. 이 임계구역 문제를 해결하기 위해선 적절한 실행 흐름 제어가 필요하다.

 

먼저 이게 어째서 문제가 될 수 있나 확인.

 

Main.java

class Main {
  public static void main(String[] args) throws InterruptedException {
    BankAccount b = new
    BankAccount();
    Parent p = new Parent(b);
    Child c = new Child(b);
    p.start();
    c.start();
    p.join();
    c.join();
    System.out.println( "\nbalance = " + b.getBalance());
  } 
}

 

BankAccount.java

class BankAccount {
  int balance=5000;
  void deposit(int amount) {
    balance = balance + amount;
  }
  void withdraw(int amount) {
    balance = balance - amount;
  }
  int getBalance() {
    return balance;
  }
}

 

Parent.java

class Parent extends Thread {
  BankAccount b;
  Parent(BankAccount b) {
    this.b = b;
  }

  public void run() {
    for (int i=0; i<100; i++){
      try {
          Thread.sleep((int)(Math.random()*10));
      } catch(InterruptedException e) { }
      b.deposit(1000);
      System.out.println("천원 저금/ 남은 금액:"+b.getBalance());
    }
  }
} 

 

Child.java

class Child extends Thread {
  BankAccount b;
  Child(BankAccount b) {
    this.b = b;
  }

  public void run() {
    for (int i=0; i<100; i++)
    {
      try {
          Thread.sleep((int)(Math.random()*10));
      } catch(InterruptedException e) { }
      b.withdraw(1000);
      System.out.println("천원 인출/ 남은 금액:"+b.getBalance());
    }
  }
} 

 

이 4개의 파일을 통해, Parent 는 쓰레드 실행 시 천원씩을 100번 저금하고, Child 는 천원을 100번 인출 한다.

두 개의 쓰레드는 balance라는, 같은 계좌를 접근한다.

자바의 쓰레드 실행 방법

1. Runnable interface 를 구현하거나, Thread 클래스를 상속해 run() method 를 오버라이딩해 적절한 실행 내용을 구현하는 2가지 방법이 있는데 후자를 택했다.

2. run() method 구현을 하고, 쓰레드를 생성하고자 하는 코드부에서 class instance 를 생성한 뒤 start() 메소드를 호출한다. 그러면 메소드가 run() 의 실행을 위해 대기하고 차례가 오면 실행한다.

 

 

예상되는 결과는 무엇일까? 선언된 순서대로 쓰레드가 각각 저금과 이체를 다 실행한다면 정상적으로 남은 계좌금액은 원래있던 잔액 5000원이 되어야 한다. Thread.join() method 덕분에, 각 쓰레드가 끝난 이후에야 잔액이 print 되기 때문에 인출과 저금의 순서에 상관없이 마지막 출력 결과는 동일해야한다.

 

하지만 결과는?

 

실행 결과

thread.sleep 에 의해, 인출(혹은 저금)의 과정이 끝나기전에 다음 저금(혹은 인출) 이 발생해 어느 순간부터 계좌의 금액은 어그러지게 된다. 당연히 이런 식이면 은행은 쫄딱 망할 것이다.

이런 임계구역에서의 동기화 문제를 해결하기 위해 등장한 것이 semaphore,Monitor,Mutex 등이다.

원리를 알아보는게 중요하지만, 자바코드로 이 동기화를 보장하는 방법만 테스트해보겠다.

 

class BankAccount {
  int balance=5000;
  synchronized void deposit(int amount) {
    balance = balance + amount;
  }
  synchronized void withdraw(int amount) {
    balance = balance - amount;
  }
  int getBalance() {
    return balance;
  }
}

BankAccount.java 의 임계구역의 상호배타성,원자성이 보장되어야 하는 메소드들에 대해 다음과 같이 'synchronized' 를 추가한다.

 

정상적으로 돈이 남아있다. 은행에 항의할 필요가 없어졌다.

 

 

출처:

m.blog.naver.com/PostView.nhn?blogId=yka21&logNo=70000243036&proxyReferer=https:%2F%2Fwww.google.com%2F

 

쓰레드 프로그래밍 예제

제2절 쓰레드 프로그래밍 예제 2.1 은행계좌 입출금 문제 은행의 계좌로부터 입금 및 출금이 이루어지는 각...

blog.naver.com

www.kocw.or.kr/home/cview.do?mty=p&kemId=978503&ar=pop

 

운영체제

운영체제의 정의 및 역할 등에 대해 알아보고, 운영체제의 주요 요소들, 즉 프로세스 관리, 주기억장치 관리, 파일 시스템 등에 대해 공부한다.

www.kocw.net