본문 바로가기
JAVA공부/2-쓰레드

쓰레드의 동기화(2)

by 으노으뇨 2021. 10. 15.
728x90
반응형
SMALL

동기화 하는 이유 : 한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막기위함

 

동기화의 단점이 동기화를 하면 데이터는 보호는 되지만 비효율적이다. 한번에 한쓰레드만 사용되니까!

그래서 동기화를 하면 프로그램의 효율이 떨어진다. 그래서 그 떨어진 효율을 높이기 위해서 

만들어진 메서드가 wait, notify를 사용한다.

즉 synchronized로 동기화 해서 공유 데이터를 보호하는 것 까지는 좋은데, 특정 쓰레드가 객체의 락을 가진 상태로

오랜 시간을 보내지 않도록 하는 것도 중요하다. 

만일 계좌에 추름할 돈이 부족해서 한 쓰레드가 락을 보유한 채로 돈이 입금될때까지 
오랜시간을 보낸다면, 다른 쓰레드들은 해당 객체의 락을 기다리느라 다른 작업들도
원활히 진행되지 않기 때문이다.

이러한 상황을 개선하기 위해 고안되었다.

동기화된 임계 영역의 코드를 수행하다가 작업을 더 이상 진행할 상황이 아니면 일단 wait()을 호출하여 

쓰레드가 락을 반납하고 기다리게 한다. 그러면 다른 쓰레드가 락을 얻어 해당 객체에 대한 작업을 수행할 수 있게된다.

나중에야 뭐 작업할 상황이 되면 notify()를 호출해서 , 작업을 중단했던 쓰레드가 다시 락을 얻어 작업을 진행할 수 있다.

마치 빵을 사려고 빵집 앞에 줄을 서있는데, 자신의 차례가 되었는데도 자신이 원하는 빵이
나오지 않았다면, 다음 사람에게 순서를 양보하고 기다리다가 자신이 원하는 빵이 나오면
통보를 받고 빵을 가지고 나가는 것이다.

차이가 있다면, 오래 기다린 쓰레드가 락을 얻는 다는 보장이 없다는 것이긴하다..

이 친구들은 우선 Object 클래스에 정의도이ㅓ 있으며, 동기화 블록 내에서만 사용할 수 있다.

wait()  // lock을 풀고 쓰레드를 해당 객체의 waiting pool에 넣는다.
notify() // waiting pool 에서 대기중인 쓰레드 중의 하나를 깨운다.
notifyAll() // waiting pool에서 대기중인 모든 쓰레드를 깨운다.

wait()은 notify()또는 notifyAll()이 호출될 때까지 기다리지만, 매개변수가 있는 wait()은 지정된 시간동안만 기다린다.

지정된 시간이 지난후에 자동적으로 notify()가 호출되는것과 같다.

notify()는 하나씩 꺠운다.

notify와 notifyAll의 차이점은 wait()하는 쓰레드가 여러개가 있을 수 있다. 전부다 깨우는 것과 같다. 
- 만약 마지막 쓰레드의 경우 실행을 못하는 경우가 있기에 모든 쓰레드를 한번에 꺠우는 것이다.

package ch13.Synchronized;

import java.util.ArrayList;

public class SynchronizedEx3 {

	public static void main(String[] args) throws Exception {
		// TODO Auto-generated method stub
		Table table = new Table();

		new Thread(new Cook(table), "COOK1").start();
		new Thread(new Customer(table, "pizza"), "CUST1").start();
		new Thread(new Customer(table, "bread"),"CUST2").start();
		Thread.sleep(100);
		System.exit(0);
	}

}

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

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

	@Override
	public void run() {
		// TODO Auto-generated method stub
		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... : (");
			}
		}
	}

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

class Cook implements Runnable {
	private Table table;

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

	@Override
	public void run() {
		// TODO Auto-generated method stub
		while(true) {
			int idx = (int) (Math.random() * table.dishNum());
			table.add(table.dishNames[idx]);
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {

			}
		}
	}

}

class Table {
	String[] dishNames = { "pizza", "pizza", "bread" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();

	public void add(String dish) {
		if (dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dished : " + dishes.toString());
	}

	public boolean remove(String dishName) {
		for (int i = 0; i < dishes.size(); i++) {
			if (dishName.equals(dishes.get(i))) {
				dishes.remove(i);
				return true;
			}
		}
		return false;
	}

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

예제를 작성했다. 식당에서 요리사, 손님1, 손님2 이렇게 각 쓰레드를 만들고 

요리사는 계속 요리를 만들고, 대신 6개가 넘어가면 만들지않는다.

손님1, 손님2는 각각 피자와 빵을 소비한다.

그래서 먹게되면 요리사의 호주머니에 있는 빵과 피자(??)가 사라지도록 했다.

그러다가 예외가 발생했다.

현재 오류는 요리사가 음식을 추가하는 과정에 손님이 요리를 먹었다라는 뜻이다.

ArrayList를 읽기 수행중에 add, remove 즉 변경이 발생하면 나타나는 에러이다. 

그리고 지금 내가 작성한 예제에서는 죽어도 안나타났지만

IndexOutOfBoundsException이 나타난다.

이것은 손님 1쓰레드가 테이블의 마지막남은 음식을 먹었는데 손님2는 그걸 받아서 먹었다라는 즉 있지도 않은 음식을 

테이블에서 제거하려 했기 때문에 발생하는 예외이다.

Table 을 여러 쓰레드가 공유하기 때문에 작업 중에 끼어들기가 발생한다.
-> Table의 add()와 remove()를 synchronized로 동기화

동기화 추가

class Table2 {
	String[] dishNames = { "pizza", "pizza", "bread" };
	final int MAX_FOOD = 6;
	private ArrayList<String> dishes = new ArrayList<>();

	public synchronized void add(String dish) {
		if (dishes.size() >= MAX_FOOD) {
			return;
		}
		dishes.add(dish);
		System.out.println("Dished : " + dishes.toString());
	}

	public boolean remove(String dishName) {
		synchronized (this) {

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

동기화를 추가해준 부분이다.

예외는 발생하지않았다. 그런데 뭔가 깔끔하지 않았다.

음식이 없을때 손님이 Table에 lock을 쥐고 안놓는다.

음식이 없을때 wait()을 추가해주어 손님이 lock을 풀고 기다리게 하자

요리사가 음식을 추가하면, notify()로 손님에게 알리자

음식을 소비하는 쪽에서는 wait()을 넣어주었다.

이걸 호출하여 쓰레드가 음식먹으러 왔다가 음식이없으면 락을 반납하고 대기실에 간다.

만약, 테이블의 음식이 0이 아니면, 음식을 하나먹는다. (음식소비) 한다음 

요리사에게 notify()를 해준다. 테이블에 요리가 꽉차면 요리사가 테이블에 음식을 넣어주면 안된다.

요리사가 기다리는 경우는 테이블에 음식이 가득찬경우이다. 

손님은 테이블에 음식이 없을때, 등에 대기를 하도록해준다.

손님이 음식을 먹는다. = 음식이 하나 줄어든다. -> 요리사에게 notify() 를 해준다.
요리가 꽉찼다. = 요리사에게 wait()전달하여 잠시 멈추게한다. 요리가 줄을 때까지
반복..

 

 

728x90
반응형
LIST

'JAVA공부 > 2-쓰레드' 카테고리의 다른 글

쓰레드의 동기화(4)  (0) 2021.10.16
쓰레드의 동기화(3)  (0) 2021.10.15
쓰레드의 동기화  (0) 2021.10.15
쓰레드 제어문(4)  (0) 2021.10.14
쓰레드 제어문(3)  (0) 2021.10.14

댓글