코린이의 소소한 공부노트

쓰레드의 동기화와 실행제어 본문

Java

쓰레드의 동기화와 실행제어

무지맘 2022. 12. 9. 21:35

[동기화가 필요한 이유]

1. 멀티 쓰레드 프로세스에서는 다른 쓰레드의 작업에 영향을 미칠 수 있다.

  - 진행 중인 작업이 다른 쓰레드에게 간섭받지 않게 하려면 동기화가 필요하다.

2. 동기화를 하려면 다른 쓰레드에게 간섭받지 않아야 하는 문장들을 임계 영역(critical section)으로 설정한다.

  - 임계 영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입이 가능하다.(객체 1개에 락 1)

  - 락을 걸어둠으로써 데이터의 일관성을 유지하게 한다.

  - 임계 영역이 많을수록 성능이 떨어지기 때문에 최소한의 영역만 설정한다.

 

[synchronized 키워드를 이용한 동기화]

1. 임계 영역을 설정하는 방법

// 1) 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) { // 임계 영역 시작
    // ...
} // 임계 영역 끝
 
// 2) 메서드 전체를 임계 영역으로 지정
public synchronized void method() { // 임계 영역 시작
    // ...
} // 임계 영역 끝

2. 이해를 돕기 위한 예시

class Account{
    private int balance = 1000; // private를 해야 동기화가 의미가 있다.
    public int getBalance(){
        return balance;
    }
    public void withdraw(int money){ // 동기화가 되지 않은 상태
        if(balance>=money){
            try{ Thread.sleep(1000); } catch(InterruptedException e) { }
            balance -= money;
        }
    } // 이론상으로는 잔액이 음수가 나올 수 없다.
}

class RunnableEx implements Runnable{
    Account acc = new Account();
    public void run(){
        while(acc.getBalance()>0){
            int money = (int)(Math.random()*3+1)*100; // 100, 200, 300중 하나
            acc.withdraw(money);
            System.out.println(“잔액 = ” + acc.getBalance());
        }
    }
}

// 실행 결과
잔액= 700
잔액= 700
잔액= 400
잔액= 200
잔액= 200
잔액= 0
잔액= -100 // 동기화가 되어 있지 않아 두 쓰레드가 동시에 잔액을 인출해서 음수가 나온 상황

// 동기화: Account 클래스 -> public synchronized void withdraw(int money)
잔액= 800
잔액= 700
잔액= 500
잔액= 400
잔액= 200
잔액= 0
잔액= 0 // 동기화가 되어있어 다른 쓰레드에 의해 잔액이 0이 됐을 때 인출하지 않고 잔액만 출력

 

[쓰레드의 실행제어]

1. 동기화의 효율을 높이기 위해 wait(), notify()를 사용한다.

  - Object 클래스에 정의되어 있으며, 동기화 블록 내에서만 사용할 수 있다.

2. wait()  객체의 lock을 풀고 해당 객체의 쓰레드를 waiting pool에 넣는다.

3. notify()  waiting pool에서 대기 중인 쓰레드중의 하나를 깨운다.

4. notifyAll()  waiting pool에서 대기 중인 모든 쓰레드를 깨운다.

  - notify()를 쓸 경우 계속 통지를 못 받는 쓰레드가 생길 수 있으므로, 일단 모두 깨운 후 그중 하나에게 락을 획득시킨다.

5. wait()와 notify()를 쓸 때, 누굴 기다리게 하고 누굴 깨우는지 코드를 면밀히 읽지 않는 이상 헷갈린다.

// 예시1 - 위에서 본 코드
class Account{
    int balance = 1000;

    public synchronized void withdraw(int money){
        while(balance<money){ // 출금할 돈이 없다면
            try{
                wait(); // 대기 - 락을 풀고 기다리다가 통지를 받으면 락을 재획득(reentrance)한다.
            } catch(InterruptedException e) { }
        }
        balance -= money;
    }

    public synchronized void deposit(int money){
        balance += money; // 락을 얻은 쓰레드가 입금을 한다.
        notify(); // 통지 - 대기중인 쓰레드 중 하나에게 입급됐다고 알린다.
    }
}
// 예시2 - 요리사 vs 고객
import java.util.ArrayList;

class Table { // 음식이 놓일 테이블
    String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
    final int MAX_FOOD = 6;
    private ArrayList<String> dishes = new ArrayList<>(); // ArryaList는 동기화X

    public synchronized void add(String dish) { // 요리사가 테이블이 음식을 추가
        while(dishes.size() >= MAX_FOOD) { // 접시가 최대치보다 많다면
            String name = Thread.currentThread().getName();
            System.out.println(name+" is waiting.");
            try {
                wait(); // 요리사를 기다리게 한다.
                Thread.sleep(500);
            } catch(InterruptedException e) {}	
        }
        dishes.add(dish);
        notify();  // 기다리고 있는 고객을 깨운다.
        System.out.println("Dishes:" + dishes.toString()); // 테이블 위의 음식 목록을 출력한다.
    }

    public void remove(String dishName) { // 고객이 먹고 난 접시를 치운다.
        synchronized(this) {	
            String name = Thread.currentThread().getName();

            while(dishes.size()==0) { // 음식이 없다면
                System.out.println(name+" is waiting.");
                try {
                    wait(); // 고객을 기다리게 한다.
                    Thread.sleep(500);
                } catch(InterruptedException e) {}	
            }

            while(true) { // 음식이 있다면
                for(int i=0; i<dishes.size();i++) {
                    if(dishName.equals(dishes.get(i))) { // 고객이 원하는 음식을 찾아
                        dishes.remove(i); // 먹는다
                        notify(); // 요리사를 깨운다.
                        return; // 먹고 끝난다.
                    }
                } // for문의 끝

                try { // 원하는 음식이 없다면
                    System.out.println(name+" is waiting.");
                    wait(); // 원하는 음식이 나올 때까지 고객을 기다리게 한다.
                    Thread.sleep(500);
                } catch(InterruptedException e) {}	
            } // while(true)
        } // synchronized
    }
    
    public int dishNum() { return dishNames.length; } // 이 문제의 경우에는 3
}


class Cook implements Runnable { // 요리사
    private Table table;

    Cook(Table table) { this.table = table; }

    public void run() {
        while(true) {
            int idx = (int)(Math.random()*table.dishNum());
            table.add(table.dishNames[idx]); // { "donut","donut","burger" } 중의 하나 추가
            try { Thread.sleep(10);} catch(InterruptedException e) {}
        } // while
    }
}

class Customer implements Runnable { // 고객
    private Table  table;
    private String food;

    Customer(Table table, String food) {
        this.table = table;  
        this.food  = food; // 이 문제의 경우 도넛 or 버거
    }

    public void run() {
        while(true) {
            try { Thread.sleep(100);} catch(InterruptedException e) {}
            String name = Thread.currentThread().getName();
			table.remove(food);
            System.out.println(name + " ate a " + food);
        } // while
    }
}

// main()
Table table = new Table();
new Thread(new Cook(table), "COOK").start();
new Thread(new Customer(table, "donut"),  "CUST1").start();
new Thread(new Customer(table, "burger"), "CUST2").start();
Thread.sleep(2000); // 20초동안 메인쓰레드는 일시정지
System.exit(0);

// 결과
Dishes:[donut] // 최대 6접시가 될 때까지 add() 호출
Dishes:[donut, donut]
Dishes:[donut, donut, donut]
Dishes:[donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, donut]
COOK is waiting. // 6접시가 다 차서 요리사는 대기중
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
Dishes:[donut, donut, donut, donut, donut, donut] // 고객이 1접시를 먹어 요리사가 1접시를 더 채움
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
COOK is waiting. // 6접시가 다 차서 요리사는 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
CUST2 is waiting. // 원하는 음식(버거)이 없어 대기중
CUST1 ate a donut // 원하는 음식(도넛)을 먹음
// 20초가 끝나 종료됨