Java

[Java 기초 공부] 쓰레드(thread) -3 (동기화)

동그리담 2024. 3. 21. 16:35

쓰레드의 동기화 (synchronization)

한 쓰레드가 진행 중인 작업을 다른 쓰레드가 간섭하지 못하도록 막는 것

만일 쓰레드 A가 작업하던 도중에 다른 쓰레드 B에게 제어권이 넘어갔을 때, 쓰레드 A가 작업하던 공유데이터를 쓰레드 B가 임의로 변경하였다면, 다시 쓰레드A가 제어권을 받아서 난머지 작업을 마쳤을 때 다른 결과를 얻을 수 있다.
  이러한 일을 방지하기 위해서 한 쓰레드가 특정 작업을 끝마치기 전까지 방해받지 않도록 하는 것이
임계영역(critical section) 과 잠금(락, lock)이다. 
  공유 데이터를 사용하는 코드 영역을 임계 영역으로 지정한 뒤 데이터가 소유한 lock을 흭득한 단 하나의 쓰레드만 영역 내의 코드를 수행할 수 있다 한다. 그리고 해당 쓰레드가 영역 내의 모든 코드를 수행하고 벗어나서 lock을 반납해야만 다른 쓰레드가 반납된 lock을 흭득하여 임계 영역의 코드를 수행할 수 있게 된다.

synchronized를 이용한 동기화

 

가장 간단한 동기화 방법

1. 메서드 전체를 임계 영역으로 지정
public synchronized void calcSum() { ...(메소드 안이 모두 임계영억) }
2. 특정한 영역을 임계 영역으로 지정
synchronized(객체의 참조변수) { ... }

1보단 2의 방법을 써 임계 영역을 최소화해서 쓰는게 효율적인 프로그램이 된다.
(임계영역 = 멀티 쓰레드 프로그램의 성능을 좌우함) 

동기화를 적용 안한 예시

public class Test {
    public static void main(String[] args) {
        Runnable r = new RunnableEx21();
        new Thread(r).start(); //ThreadGroup에 의해 참조되므로 gc 대상이아님
        new Thread(r).start(); //위와동일
    }
}
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;
        }
    }
}
class RunnableEx21 implements Runnable{
    Account acc = new Account();
    @Override
    public void run() {
        while(acc.getBalance()>0){
            //100, 200, 300
            int money = (int) (Math.random() * 3 + 1) *100;
            acc.withdraw(money);
            System.out.println("balance:"+acc.getBalance());
        }
    }
}
===========
실 행 결 과
===========
balance:700
balance:700
balance:400
balance:300
balance:-200
balance:-200

잔고가 음수면 안되는데 음수가 됌, 그 이유는 한 쓰레드가 if문의 조건식을 통과하고 출금하기 직전에 다른 쓰레드가 끼어들어서 출금을 먼저 했기 때문이다.
 synchronized을 사용하면 한 쓰레드에 의해서 먼저 withdraw()가 호출되면, lock이 반납될 떄까지 다른 쓰레드는 withdraw()를 호출하게 되더라도 대기상태에 머물게 된다. 

메서드 전체를 임계영역으로 지정:  publc synchronized void withdraw(int money) { ... }
블럭만 임계영역으로 지정: publc synchronized void withdraw(int money) { synchronized(this){if(...){ ...//if문} } }

이 경우 어느 쪽을 선택해도 같으니 메서드 전체를  synchronized메서드로 하는것이 낫다.

wait()과 notify()

synchronized로 동기화하게 되면, 특정 쓰레드가 객체의 락을 오랜 시간을 가지게 된다. 이러면 다른 작업들이 원활이 진행되지 않을 수 있기 때문에 개선 방안으로 wait()과 notify()가 등장했다. 동기화된 임계 영역의 코드를 수행하다가 적업을 더 이상 진행할 상황이 아니면, wait()을 호출하여 락을 반납하고 기대리게 한다. 그러면 다른 쓰레드가 락을 얻어 작업을 수행할 수 있게한다. 반납한 쓰레드가 다시 작업을 진행할 수 있는 상황이 되면 notify()를 호출해, 다시 락을 얻어 작업을 진행하도록 한다. (wait()을 호출한 쓰레드가 다시 얻는다는 보장이 없음)

synchronized블록 내에서만 사용 가능

notify()가 호출되면, 해당 객체의 대기실(waiting pool)에 있던 모든 쓰레드 중 임의의  쓰레드만 통지받는다.
notifyAll()은 기다리던 모든 쓰레드에게 통보를 하지만, lock을 얻을 수 있는 쓰레드는 하나의 쓰레드 뿐이다.

wait()은 [notify()|notifyAll()]이 호출 될때까지 기다리지만 매게변수가 있는 wait()은 지정된 시간만 기다리다가 자동으로 notify()를 호출한다. 그리고 waiting pool은 객체마다 존재하는 것이므로 notifyAll()이 호출된다고 해서 모든 객체의 쓰레드가 깨워지는 것이 아니다. notifyAll()이 호출된 객체의 waiting pool에 대기 중인 쓰레드만 해당된다.

import java.util.ArrayList;

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

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "CUST1").start();
        new Thread(new Customer(table, "berger"), "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() {
        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() {
        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 = { "donut", "donut", "berger" }; // donut이 더 자주 나온다.
    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("Dishes:"+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; }
}

===========
실 행 결 과
===========
Dishes:[berger]
Dishes:[berger, donut]
Dishes:[berger, donut, donut]
Dishes:[berger, donut, donut, berger]
Dishes:[berger, donut, donut, berger, donut]
Dishes:[berger, donut, donut, berger, donut, donut]
Dishes:[donut, berger, donut, donut, donut]
Dishes:[donut, berger, donut, donut, donut, donut]
CUST2 ate a berger
CUST1 ate a donut
CUST2 ate a berger
CUST1 ate a donut
Dishes:[donut, donut, donut, donut, donut, donut]
CUST1 ate a donut
Dishes:[donut, donut, donut, donut, donut, donut]
CUST2 failed to eat. :(
CUST2 failed to eat. :(
CUST1 ate a donut
CUST2 failed to eat. :(
Dishes:[donut, donut, donut, donut, donut, berger]
CUST1 ate a donut
Dishes:[donut, donut, donut, donut, berger, donut]
CUST2 ate a berger

Process finished with exit code 0

음식을 놓는 중에 가져가거나 마지막 음식을 가져가는 도중 다른 쓰레드가 먼저 음식을 낚아채는 예외가 발생한다.
이 코드를 동기화하면

import java.util.ArrayList;

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

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "CUST1").start();
        new Thread(new Customer(table, "berger"), "CUST2").start();

        Thread.sleep(5000);
        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() {
        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() {
        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 = { "donut", "donut", "berger" }; // donut이 더 자주 나온다.
    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("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;
                }

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

===========
실 행 결 과
===========
Dishes:[donut]
CUST2 is waiting.
CUST1 ate a donut
CUST2 is waiting.
CUST2 is waiting.
CUST2 is waiting.

Table클래스에 add메서드를 임계영역으로 지정하고, remove메스드 내부 블럭에 임계영역을 지정했다.
동기화는 됫지만 이상하다. 손님 쓰레드가 테이블 객체의 lock을 쥐고 기다리기 때문에 요리사 쓰레드가 음식을 새로 추가하려해도 테이블 객체의 lock을 얻을 수 없다. 이럴 때 사용하는 것이 'wait() & notify()'이다.

import java.util.ArrayList;

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

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "CUST1").start();
        new Thread(new Customer(table, "berger"), "CUST2").start();

        Thread.sleep(5000);
        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() {
        while(true){
            try { Thread.sleep(100);}catch (InterruptedException e){}
            String name = Thread.currentThread().getName();

           table.remove(food);
            System.out.println(name + " eat a "+ food);
        }
    }

}

class Cook implements Runnable {
    private Table table;
    Cook(Table table) { this.table = table; }

    @Override
    public void run() {
        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 = { "donut", "donut", "berger" }; // 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(); // 기다리고 있는 CUS를 깨우기 위함
        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;
                    }
                try{
                    System.out.println(name +" is waiting.");
                    wait(); //원하는 음식이 없는 CUST쓰레드를 기다리게한다
                    Thread.sleep(500);
                } catch (InterruptedException e){}
            }
        }
    }
    public int dishNum() { return dishNames.length; }
}

===========
실 행 결 과
===========
Dishes:[donut]
Dishes:[donut, berger]
Dishes:[donut, berger, donut]
Dishes:[donut, berger, donut, donut]
Dishes:[donut, berger, donut, donut, donut]
Dishes:[donut, berger, donut, donut, donut, donut]
COOK1 is waiting. <-- 테이블이 가득차서 요리사가 기다리고있다.
CUST2 eat a berger<-- 음식이 소비되어 요리사 notify() 호출
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut]
CUST1 eat a donut
CUST2 is waiting. <-- 원하는 음식이 없어서 손님이 기다리고 있다.
Dishes:[donut, donut, donut, donut, berger]
CUST1 eat a donut
Dishes:[donut, donut, donut, berger]
CUST2 eat a berger
Dishes:[donut, donut, donut, berger, berger]
Dishes:[donut, donut, donut, berger, berger, donut]
COOK1 is waiting.
CUST2 eat a berger
Dishes:[donut, donut, donut, berger, donut, donut]
CUST2 eat a berger
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, berger]
COOK1 is waiting.
CUST2 eat a berger
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, berger]
CUST2 eat a berger
CUST1 eat a donut
Dishes:[donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, berger]
COOK1 is waiting.
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, berger, donut]
CUST1 eat a donut
CUST2 eat a berger
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, berger]
COOK1 is waiting.
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, berger, donut]
CUST1 eat a donut
CUST2 eat a berger
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, donut]
COOK1 is waiting.
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut, donut]
CUST1 eat a donut
CUST2 is waiting.
Dishes:[donut, donut, donut, donut, donut, donut]
CUST2 is waiting.
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut, donut]

이전 예제에 적절하게 wait()과 notify()를 추가하였다. 그리고 테이블에 음식이 없을 때 뿐만아니라 원하는 음식이 없을 때도 손님이 기다리도록 했다.
여기에도 한 가지 문제가 있는데 테이블 객체의 waiting pool에 요리사 쓰레드와 손님 스레드가 같이 기다린다는 것이다.
그래서 notify()가 호출되었을 때, 요리사와 손님 중에서 누가 통지 받을지 알 수 없다.

기아 현상과 경쟁 상태

지독히 운이 나쁘면 요리사 쓰레드는 계속 통지를 받지 못하고 오랫동안 기다리게 될 수도 있는데 이러한 현상을
기아(starvation) 현상 이라고 한다. 이 현상을 막으려면, notifyAll()을 사용해야 한다. 일단 모든 쓰레드에게 통지하면, 손님 쓰레드는 다시 들어가더라도 요리사 쓰레드는 결국 lock을 얻어서 작업을 진행 할 수 있기 때문이다. 이렇게 요리사 쓰레드의 기아현상은 막았지만, 불필요한 경우 손님 쓰레드까지 통지를 받아서 lock을 얻기위해 서로 경쟁하게 된다. 이러한 상황을 경쟁 상태(race condition) 라고 하는데, 이 경쟁 상태를 개선하기 위해서는 각각을 구별해서 통지하는 것이 필요하다.
밑에 정리한 Lock과 Condition을 이용하면 선별 통지가 가능하다.

Lock인터페이스를 이용한 동기화

'java.util.concurrent.locks' 패키지가 제공하는 'lock'클래스를 이용한 동기화 방법

ReentrantLock (1) 재진입 가능한 lock, 가장 일반적인 베타 lock
ReentrantReadWriteLock (2) 읽기에는 공유적이고 쓰기에는 베타적인 lock
StampedLock  (2)에 낙관적인 lock의 가능을 추가
(Lock인터페이스를 구현하지 않음)

ReentrantLock이 가장 일반적으로 쓰이는 lock이다. wait() & notify()와 같이 특정 조건에서 lock을 풀고 나중에 다시 얻고 임계영억으로 들어와서 이후의 작업을 수행할 수 있기 때문에 재진입할 수 있는 lock이라고 붙힌다.

ReentrantRedWriteLock은 읽기를 위한 lock, 쓰기를 위한 lock을 제공 (1)은 베타적이라서 무조건 lock이 있어야만 임계영역의 코드를 수행할 수 있지만. 이는 읽기 lock이 걸려있으면, 다른 쓰레드가 읽기 lock을 중복해서 걸고 읽기를 수행할 수 있다. 읽기는 내용을 변경하지 않으므로 동시에 여러 쓰레드가 읽고 있어도 문제되지 않는다, 그러나 읽기 lock이 걸린 상태에서 쓰기 lock을 거는 것은 허용되지 않는다. 반대의 경우도 마찬가지다. 읽기를 할 때는 읽기 lock을 걸고, 쓰기를 할 때는 쓰기 lock을 거는 것일 뿐 lock을 거는 방법은 동일하다.

StampedLock은 lock을 걸거나 해지할 때 '스탬프(long타입의 정수값')를 사용하며, 읽거와 쓰기를 위한 lock외에 '낙관적 읽기 lock'이 추가된 것이다. 읽기 lock이 걸려있으면, 쓰기 lock을 얻기 위해서는 읽기 lock이 풀릴때까지 기다려야하는데 비해 '낙관적 읽기 lock'은 쓰기 lock에 의해 바로 풀린다. 그래서 낙관적 읽기에 실패하면, 읽기 lock을 얻어서 다시 읽어와야 한다. 무조건 읽기 lock을 걸지 않고, 쓰기와 읽기가 충돌할 때만 쓰기가 끝난 후에 읽기 lock을 거는 것이다.

StampedLock을 이용한 낙관적 읽기의 예

int getBalnace() {
	long Stamp = lock.tryOptimisticRead(); // 낙관적 읽기 lock을 건다.
    
    int curBalance = this.balance; //공유 데이터인 balance를 읽어온다,,
    
    if(!lock.validate (stamp) { // 쓰기 lock에 의해 낙관적 읽기 lock이 풀렸는지 확인
    	stamp = lock.readLock(); // lock을 풀렸으면, 읽기 lock을 얻으려고 기다린다.
		try{
        	curBalance = this.balance; //공유 데이터를 다시 읽어온다.
        } finally{
        lock.unlockRead(stamp); // 읽기 lock을 푼다.
        }
    }
	return curBalance; // 낙관적 읽기 lock이 풀리지 않았으면 곧바로 읽어온 값을 반환
}
생성자 및 메소드 설명
ReentrantLock()
ReentrantLock(boolean fair)
fair을 true로 주면, lock이 풀렸을 때 가장 오래 기다린 쓰레드가 lock을 흭득할수 있게 공정(fair) 처리 한다. = 확인 과정이 추가됨으로 성능은 저하
void lock() lock을 잠금
void unlock() lcok을 해지
boolean isLoocked lock이 잠겼는지 확인

synchronized 블럭과 달리 수동으로 lock을 잠그고 해제해야 한다. 

임계 영역 내에서 예외가 발생하거나 return문으로 빠져나가게 되면 lock이 풀리지 않을 수 있으므로 unlock()은 try - finally문으로 감싸는 것이 일반적이다. 

lock.lock();
try {
	//임계영역 
} finally {
	lock.unlock();
}

 대부분의 경우 synchronized블럭을 사용하는 것이 나을 수도 있다.
 이외에도 tryLock()라는 메서드가 있는데, 이 메서드는 lock()과 달리, 다른 쓰레드에 의해 lock이 걸려 있으면 lock을 얻으려고 기다리지 않는다. 또는 지정된 시간만큼만 기다린다. lock을 얻으면 true를 반환

boolean tryLock()
boolean tryLock(long timeout, TimeUnit unit)throwss InterruptedException

 ReentrantLock과 Condition

요리사 쓰레드와 손님 쓰레드를 구분해서 통지하지 못한 문제점을 해결하기 위한 Condition

wait() & notify()로 쓰레드의 종류를 구분하지 않고, 공유 객체의 waiting pool에 같이 몰아넣는 대신, 손님 쓰레드를 위한 Condition과 요리사 쓰레드를 위한 Condition을 만들어서 각각의 waiting pool에서 따로 긷리도록 하면 문제는 해결된다.

Condition은 이미 생성된 lock으로부터 newCondition()을 호출해서 생성한다.

private ReentrantLock lock = new ReentrantLock();
private Condition forCook = lock.newCondition();
private Condition forCust = lock.newCondition();

Object Condition
void wait() void await()
void awaitUninterruptibly()
void wait(long timeout) boolean await(long time, TimeUnit unit)
long awaitNanos(long nanosTimeout)
boolean awaitUntil (Date deadline)
void notify() void signal()
void notifyAll() void signalAll()
import java.util.ArrayList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

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

        new Thread(new Cook(table), "COOK1").start();
        new Thread(new Customer(table, "donut"), "CUST1").start();
        new Thread(new Customer(table, "berger"), "CUST2").start();

        Thread.sleep(2000);
        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() {
        while(true){
            try { Thread.sleep(100);}catch (InterruptedException e){}
            String name = Thread.currentThread().getName();

            table.remove(food);
            System.out.println(name + " eat a "+ food);
        }
    }

}

class Cook implements Runnable {
    private Table table;
    Cook(Table table) { this.table = table; }

    @Override
    public void run() {
        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 = {"donut", "donut", "berger"}; // donut이 더 자주 나온다.
    final int MAX_FOOD = 6; //테이블에 놓을 수 있는 최대 음식 개수

    private ArrayList<String> dishes = new ArrayList<>();

    private ReentrantLock lock = new ReentrantLock();
    private Condition forCook = lock.newCondition();
    private Condition forCust = lock.newCondition();

    public void add(String dish){
        lock.lock();
        try {
            while (dishes.size() >= MAX_FOOD) {
                String name = Thread.currentThread().getName();
                System.out.println(name + " is waiting.");
                try {
                    forCook.await(); //wait(); COOK쓰레드를 기다리게 한다.
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }
            dishes.add(dish);
            forCust.signal(); //notify() CUST를 깨우기 위함
            System.out.println("Dishes:" + dishes.toString());
        }finally {
            lock.unlock();
        }
    }

    public void remove(String dishName) {
        lock.lock(); // synchronized(this){
        String name = Thread.currentThread().getName();

        try{
            while(dishes.size()==0){
                System.out.println(name + " is waiting.");
                try{
                    forCust.await(); //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);
                        forCook.signal(); //notify() 잠자고 있는 COOK을 깨움
                        return;
                    }
                }
                try{
                    System.out.println(name+" is waiting.");
                    forCust.await();
                    Thread.sleep(500);
                }catch (InterruptedException e){}
            }
        }finally {
            lock.unlock();
        }
    }
    public int dishNum() { return dishNames.length; }
}

===========
실 행 결 과
===========
Dishes:[donut]
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]
COOK1 is waiting. // COOk 테이블에 음식이 가득차 기다림
CUST2 is waiting. // CUST 원하는 음식이 없어서 기다림
CUST1 eat a donut // 음식이 소비되면서 요리사 쓰레드가 통지받음
Dishes:[donut, donut, donut, donut, donut, berger]
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut]
CUST1 eat a donut
CUST2 eat a berger
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, berger]
COOK1 is waiting. 
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, berger, donut]
CUST2 eat a berger
CUST1 eat a donut
Dishes:[donut, donut, donut, donut, donut]
Dishes:[donut, donut, donut, donut, donut, donut]
COOK1 is waiting.
CUST2 is waiting. // 통지받았으나, 원하는 음식이 없어서 기다림
CUST1 eat a donut

volatile

멀티 코어 프로세서에서는 코어마다 별도의 캐시를 가지고있다.
코어는 메모리에서 읽어온 값을 캐시에 저장하고 값을 읽어서 작업한다. 다시 같은 값을 읽어올 때는 먼저 캐시에 있는지 확인하고 없을 때만 메모리에서 읽어온다.
  그러다보니 도중에 메모리에 저장된 변수의 값이 변경되었는데도 캐시에 저장된 값이 갱신되지 않아서 메모리에 저장된 값이 다른 경우가 발생한다. 그래서 변수 stopped의 값이 바뀌었는데도 쓰레드가 멈추지 않고 실행되는 것이다.

그럴때 변수 앞에 volatile을 붙이면, 코어가 변수의 값을 읽어올 때 캐시가 아닌 메모리에서 읽어오기 때문에 캐시와 메모리간의 값의 불일치가 해결된다.
변수에 volatile을 붙이는 대신에 변수 synchronized블럭을 사용해도 같은 효과를 얻을 수 있다. 쓰레드가 블럭을 들어갈 때와 나올 때, 캐시와 메모리간의 동기화가 이루어지기 때문이다.

volatile로 long과 double의 원자화

JVM은 데이터를 4 byte(=32bit)단위로 처리하기 때문에, int와 그 보다 작은 타입들은 한 번에 읽거나 쓰기가 가능하다. 즉, 하나의 명령어로 R/W가 가능하다는 뜻이다. 하나의 명령어는 더 이상 나눌 수 없는 최소의 작업단위이므로, 작업의 중간에 다른 쓰레드가 끼어들 틈이 없다.
  그러나, 크기가 8byte인 long과 double타입의 변수는 하나의 명령어로 값을 읽거나 쓸 수 없기 떄문에, 변수의 값을 R/W하는 과정에 다른 쓰레드가 끼어들 여지가 있다. 그러지 못하게 volatile나 synchronized블럭을 사용해서 R/W를 원자화할 수 있다.

volatile은 원자화를 할 순 있지만 동기화는 할 수 없다 volatile 대신 synchronized블럭을 사용할 수 있지만
syschronized블럭 대신 volatile은 쓸 수 없다.
또한 상수에는 붙힐 수 없다.