관리 메뉴

나구리의 개발공부기록

자바의 정석 기초편 ch13 - 33 ~ 36 [쓰레드의 동기화, wait(), notify()] 본문

유튜브 공부/JAVA의 정석 기초편(유튜브)

자바의 정석 기초편 ch13 - 33 ~ 36 [쓰레드의 동기화, wait(), notify()]

소소한나구리 2023. 12. 19. 11:56

1) 쓰레드의 동기화(synchronization)

  • 한 쓰레드가 진행중인 작업을 다른 쓰레드가 간섭하지 못하게 막는 것
  • 멀티 쓰레드 프로세스에서는 쓰레드의 작업이 다른쓰레드에 영향을 미칠 수 있음
  • 진행중인 작업이 다른 쓰레드에게 간섭받지 않게하려면 '동기화'가 필요하며 동기화를 하려면 간섭받지 않아야 하는 문장들을 '임계 영역'으로 설정해야함
  • 임계영역은 락(lock)을 얻은 단 하나의 쓰레드만 출입가능함(객체 1개에 락 1개)

(1) synchronized를 이용한 동기화

  • synchronized로 임계영역(lock이 걸리는 영역)을 설정할 때 메서드 전체를 임계영역으로 설정하거나 특정영역을 임계영역으로 지정할 수 있음
  • 임계영역은 한번에 한 쓰레드만 사용할 수 있기 때문에 영역을 최소하는 것이 좋으며 임계영역이 많을 수록 성능이 떨어짐
  • 즉 가능한 임계영역의 개수를 최소한으로 좁게작성하는 것이 좋으므로 메서드 전체보단 특정한 영역을 임계 영역으로 지정하는것을 권장함
  • 변수의 선언을 꼭 private로 해야 동기화의 의미가 있음(외부에서 사용 불가)
// 1. 메서드 전체를 임계 영역으로 설정 - 메서드 반환타입 앞에 synchronized를 붙임
public synchronized void calcSum() {	// 임계영역 시작
	//...
}	// 임계영역 끝


// 2. 특정한 영역을 임계 영역으로 지정 
synchronized(객체의 참조변수) {	// 임계영역 시작
	//...
}	// 임계영역

 

(1) 예제 - 동기화 안된 ver.

  • 잔돈을 출금하는 예제로 잔고에는 음수가 나오지 않을 것을 기대하고 프로그래밍을 하였으나 실행 결과 음수가 나옴
  • 각 쓰레드가 run()메서드 작업 중 slepp()으로 인하여 1초동안 멈춘뒤 다시 실행함
  • 이때 쓰레드들이 동기화가 되어있지 않았으므로 if문의 조건을 만족한 먼저 도착쓰레드가 sleep으로 설정한 대기 시간때문에 그다음 코드에서 잔액에서 출금액을 바로 제외하지 못할 때 다른 스레드가 if의 조건을 만족하여 sleep 상태에 빠짐
  • 이 때 먼저 sleep상태에있던 쓰레드가 깨어나서 잔고에서 출금액을 빼가고 두번째 스레드가 깨어나서 다시 잔고에서 출금액을 빼갔을 때 잔고가 출금액보다 작다면 음수로 반환함
class Ex13_12 {
	public static void main(String args[]) {
		Runnable r = new RunnableEx12();
		new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc대상이 아니다.
		new Thread(r).start(); // ThreadGroup에 의해 참조되므로 gc대상이 아니다.
	}
}

class Account {
	private int balance = 1000;

	public  int getBalance() {
		return balance;
	}

	public void withdraw(int money){
		if(balance >= money) {
			try { Thread.sleep(1000);} catch(InterruptedException e) {}
			balance -= money;
		}
	} // withdraw
}

class RunnableEx12 implements Runnable {
	Account acc = new Account();

	public void run() {
		while(acc.getBalance() > 0) {
			// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println("balance:"+acc.getBalance());
		}
	} // run()
}

 

예제- 동기화 ver.

  • 실제 로직을 수행하는 메서드의 전체를 synchronized를 붙혀서 임계영역을 지정
  • 임계영역의 메서드는 쓰레드가 동시에 동작하지 않기 때문에 1초동안 멈추고 잔고에서 출금액을 반환하고 잔고의 변수 값을 읽어오는 메서드가 싱글 쓰레드로 동작함
  • 즉 하나의 쓰레드가 잔고에서 출금을 완료해야 다음 쓰레드가 잔고에서 출금을 할 수 있기 때문에 if문을 두 쓰레드가 동시에 만족할 수 없게되어 잔고는 음수가 나올 수 없음
class Ex13_13 {
	public static void main(String args[]) {
		Runnable r = new RunnableEx13();
		new Thread(r).start();
		new Thread(r).start();
	}
}

class Account2 {
	private int balance = 1000; // private으로 해야 동기화가 의미가 있다.

	public synchronized int getBalance() {	// 읽을 때도 동기화를 해줘야 됨
		return balance;
	}

	public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
		if(balance >= money) {
			try { Thread.sleep(1000);} catch(InterruptedException e) {}
			balance -= money;
		}
	} // withdraw
}

class RunnableEx13 implements Runnable {
	Account2 acc = new Account2();

	public void run() {
		while(acc.getBalance() > 0) {
			// 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
			int money = (int)(Math.random() * 3 + 1) * 100;
			acc.withdraw(money);
			System.out.println("balance:"+acc.getBalance());
		}
	} // run()
}

 


2) wait() 와 notify()

  • 동기화를 하면 한번에 한개의 쓰레드만 사용 가능하기 때문에 효율이 떨어짐
  • 효율(성능)을 높이기 위해 wait()와 notify()를 사용
  • wait() - 객체의 lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣음
  • notify() - waiting pool에서 대기중인 쓰레드 중의 하나를 깨움 (대기중인 쓰레드를 랜덤하게 깨우기 때문에 운이 나쁘면 특정 쓰레드가 계속 대기 상태로 있을 수 있음)
  • notifyAll() - waiting pool에서 대기중인 모든 쓰레드를 깨움(일반적으로 더 좋음)

(1) 출금 메서드와 입금 메서드에 동기화

  • 출금, 입금 메서드를 synchronized로 동기화
  • 잔고가 출금액보다 적으면 반복문으로 잔고가 출금액보다 많아질 때까지 해당 스레드를 계속 대기시킴(깨워도 다시 반복문으로 다시 대기상태로 만듦)
  • wait()를 호출하여 출금메서드에서 동작하던 쓰레드를 waiting pool로 이동시켜 입금 메서드를 작동(입금 메서드가 동기화 접근 권한을 받음)
  • 입금을 완료하면 notify()로 대기중인 쓰레드를 깨워서 일어난 쓰레드가 멈췄던 작업을 계속 수행함

예시

(2) 예제의 구조

  • 요리사(쓰레드1)는 Table에 음식을 추가, 손님(쓰레드2)은 Table의 음식을 소비하는 프로그래밍 작성
  • 요리사와 손님이 같은 객체(Table)을 공유하기 때문에 동기화가 필요함(동기화 하지 않으면 예외 발생)
  • [예외 상황1] - 요리사가 Table 요리를 추가하는 과정에 손님이 요리를 먹어버려서 컬렉션을 작업을 두 쓰레드가 동시에 작업을 하여 ConcurrentModificationException이 발생함
  • [예외 상황2] - 하나 남은 요리를 손님2 먹으려하는데, 손님1 먹어버려서 IndexOutOfBoundsException이 발생함

예외 발생

(3) 예제 - 동기화만 진행ver.

  • 동기화로 예외를 해결 하였으나 더이상 작업이 진행 되지 않음
  • 예외는 발생하지 않지만 손님이 Table에 먹을 음식이 없으면 해당 메서드에서 빠져나오지 못하고 계속 코드를 수행함
  • 요리사가 Table lock 얻을 없어서 음식을 추가하지 못하게 되어 더이상 프로그램이 실행되지 않음

동기화로 예외는 해결 했으나 프로그램이 더이상 실행 되지 않음

import java.util.ArrayList;

class Customer implements Runnable {
	private Table table;
	private String food;

	Customer(Table table, String food) {
		this.table = table;  
		this.food  = food;
	}

	public void run() {
		while(true) {
			try { Thread.sleep(10);} catch(InterruptedException e) {}
			String name = Thread.currentThread().getName();

			if(eatFood())
				System.out.println(name + " ate a " + food);
			else 
				System.out.println(name + " failed to eat. :(");
		} // while
	}

	boolean eatFood() { return table.remove(food); }
}

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]);
			try { Thread.sleep(100);} catch(InterruptedException e) {}
		} // while
	}
}

class Table {
	String[] dishNames = { "donut","donut","burger" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();
	public synchronized void add(String dish) { // synchronized를 추가
		if(dishes.size() >= MAX_FOOD)	
			return;
		dishes.add(dish);
		System.out.println("Dishes:" + dishes.toString());
	}

	public boolean remove(String dishName) {
		synchronized(this) {	
			while(dishes.size()==0) {
				String name = Thread.currentThread().getName();
				System.out.println(name+" is waiting.");
				try { Thread.sleep(500);} catch(InterruptedException e) {}	
			}

			for(int i=0; i<dishes.size();i++)
				if(dishName.equals(dishes.get(i))) {
					dishes.remove(i);
					return true;
				}
		} // synchronized

		return false;
	}

	public int dishNum() { return dishNames.length; }
}

class Ex13_14 {
	public static void main(String[] args) throws Exception {
		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(5000);
		System.exit(0);
	}
}

(4) 예제 - wait(),  notify() 사용 ver.

  • [문제점] 음식이 없을 때 손님이 Table의 lock을 계속 가지고 있어서 요리사가 lock을 얻지 못해 Table에 음식을 추가 못함
  • [해결책] 음식이 없을 때 wait()으로 손님이 lock을 풀고 대기상태로 만들어 요리사가 lock을 획득하여 음식을 추가하고 lock을 반납 후 notify()로 손님에게 알려서 대기하던 스레드를 호출하여 프로그램을 가동
  • 단, wait()와 notify()가 어떤 쓰레드에게 적용 되는지 불분명한데 이를 해결하기 위해 Lock & Condition을 사용하면 됨(자바의 정석 3판 - 필요하면 검색해서 찾아보기)
  • wait(), notify()로 동기화가된 정상적인 멀티쓰레스 프로그래밍이 가능해짐
import java.util.ArrayList;

class Customer2 implements Runnable {
	private Table2 table;
	private String food;

	Customer2(Table2 table, String food) {
		this.table = table;  
		this.food  = food;
	}

	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
	}
}

class Cook2 implements Runnable {
	private Table2 table;
	
	Cook2(Table2 table) { this.table = table; }

	public void run() {
		while(true) {
			int idx = (int)(Math.random()*table.dishNum());
			table.add(table.dishNames[idx]);
			try { Thread.sleep(10);} catch(InterruptedException e) {}
		} // while
	}
}

class Table2 {
	String[] dishNames = { "donut","donut","burger" }; // donut의 확률을 높인다.
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();

	public synchronized void add(String dish) {
		while(dishes.size() >= MAX_FOOD) {
				String name = Thread.currentThread().getName();
				System.out.println(name+" is waiting.");
				try {
					wait(); // COOK쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}	
		}
		dishes.add(dish);
		notify();  // 기다리고 있는 CUST를 깨우기 위함.
		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(); // CUST쓰레드를 기다리게 한다.
						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(); // 잠자고 있는 COOK을 깨우기 위함 
						return;
					}
				} // for문의 끝

				try {
					System.out.println(name+" is waiting.");
					wait(); // 원하는 음식이 없는 CUST쓰레드를 기다리게 한다.
					Thread.sleep(500);
				} catch(InterruptedException e) {}	
			} // while(true)
		} // synchronized
	}
	public int dishNum() { return dishNames.length; }
}

class Ex13_15 {
	public static void main(String[] args) throws Exception {
		Table2 table = new Table2();

		new Thread(new Cook2(table), "COOK").start();
		new Thread(new Customer2(table, "donut"),  "CUST1").start();
		new Thread(new Customer2(table, "burger"), "CUST2").start();
		Thread.sleep(2000);
		System.exit(0);
	}
}

 

 

** 출처 : 남궁성의 정석코딩_자바의정석_기초편 유튜브