[자바의 정석 기초편] 쓰레드 2

JEREGIM·2023년 3월 13일
0

자바의 정석 기초편

목록 보기
19/23

📌쓰레드의 실행제어

쓰레드의 실행을 제어할 수 있는 메서드들

메서드설명
static void sleep(long millis)
static void sleep(long millis, int nanos)
지정된 시간(milli second 단위) 동안 쓰레드 일시 정지
시간지나면 자동적으로 다시 실행 대기 상태 전환
void join()
void join(long millis)
void join(long millis, int nanos)
지정된 시간(milli second 단위) 동안 쓰레드 독점실행
시간 경과되거나 작업이 종료되면 join()을 호출했던 쓰레드로 다시 돌아와 실행을 계속한다.
void interrupt()깨우는 것 -> sleep() 이나 join()으로 일시정지 상태인 쓰레드를 실행 대기 상태로 변환
(해당 쓰레드 : InterruptedException 발생 → 일시정지상태 탈출)
void stop()쓰레드를 즉시 종료시킨다.
void suspend()쓰레드를 일시정지시킨다.
void resume()suspend()에 의해 일시정지 상태에 있는 쓰레드를 실행대기 상태로 만든다.
static void yield()실행 중에 자신에게 주어진 실행시간을 다른 쓰레드에게 양보하고 자신은 실행대기 상태가 된다.
  • static 메서드인 sleep()yield()는 다른 쓰레드에게 적용할 수 없고 쓰레드 자기 자신에게만 호출이 가능하다.

sleep()

: 현재 쓰레드를 지정된 시간동안 멈추게 한다.

예외처리를 해야 한다.(InterruptedException이 발생하면 깨어남)

try{
	Thread.sleep(5000); // 5초동안 멈춤
} catch(InterruptedException e) {}
  • InterruptedException는 Exception의 자손이기 때문에 예외처리가 필수이다.

  • sleep()을 깨우는 방법은 2가지이다.

    1. time-out : 지정된 시간이 지나서 try-catch문을 정상적으로 빠져나온다.
    2. interrupt() : 이 메서드를 호출하면 InterruptedException 예외가 발생하게 되고 이로 인해 try-catch문을 중간에 빠져나오게 된다.
      어떤 문제가 있어서 예외처리를 하는 것이 아니라 잠자는 상태를 중간에 벗어나기 위해 try-catch문을 이용한 것이다. 따라서 catch문 안에 아무것도 쓰지 않는다.

매번 try-catch문을 작성하기 불편하기 때문에 보통 메서드를 따로 만들어서 사용한다.

void delay(long millis) {
	try{
		Thread.sleep(millis);
	} catch(InterruptedException e) {}
}

delay(5000); // 5초동안 멈춤

특정 쓰레드를 지정해서 멈추게 하는 것은 불가능하다.

//main쓰레드에서 실행
try {
	th1.sleep(2000); // 에러는 발생하지 않음
} catch (InterruptedException e) {}
  • th1.sleep(2000); : 에러는 발생하지 않지만 main 쓰레드에서 실행했기 때문에 멈춘건 th1 쓰레드가 아니라 main 쓰레드이다.

올바른 작성법🔽

try {
	Thread.sleep(2000);
} catch (InterruptedException e) {}
  • Thread.sleep(2000); : 이렇게 작성해야 오해의 여지가 없고 실수도 하지 않게 된다.

interrupt()

: 대기 상태(WAITING)인 쓰레드를 실행 대기 상태(RUNNABLE)로 만든다.

interrupt() 관련 메서드

메서드설명
void interrupt()쓰레드의 interrupted 상태를 false에서 true로 변경
boolean isInterrupted()쓰레드의 interrupted 상태를 반환
static boolean interrupted()현재 쓰레드의 interrupted 상태를 알려주고, false로 초기화
  • static boolean interrupted()은 상태를 반환하고 false로 초기화한다. interrupt() 메서드를 호출하면 interrupted 상태를 true로 변경하는데 false에서 true가 되야 interrupted가 됐다는걸 알 수 있다.
    false로 초기화하지 않고 또 호출하게 되면 true -> true가 되므로 누가 호출했는지, 제대로 호출됐는지를 알 수 없게 된다.

  • boolean isInterrupted() 메서드는 그저 interrupted 상태를 반환한다.

class DownloadThread extends Thread {
	public void run() {
    	...
        while (downloaded && !isInterrupted()) {
        	/*download 작업을 수행*/
            ...
        }
        
        System.out.println("다운로드가 끝났습니다.");
    }
}
  • while (downloaded && !isInterrupted())
    • downloaded 다운로드가 완료되서 while 문을 빠져나갈수도 있고
      !isInterrupted() 다운로드 도중 취소 버튼을 누르면 interrupted 상태가 true가 되고 !isInterrupted() == false가 되면서 while 문을 빠져나갈 수도 있다.

실습 예제

import javax.swing.JOptionPane;

class Ex13_9 {
	public static void main(String[] args) throws Exception {
		ThreadEx9_1 th1 = new ThreadEx9_1();
		th1.start();

		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + " 입니다.");
		th1.interrupt();  // interrupted 상태를 true로 변경
		System.out.println("isInterrupted():"+ th1.isInterrupted()); // true
		System.out.println("isInterrupted():"+ th1.isInterrupted()); // true
		System.out.println("Interrupted():"+ th1.interrupted());
		System.out.println("Interrupted():"+ th1.interrupted()); 
	}
}

class ThreadEx9_1 extends Thread {
	public void run() {
		int i = 5;

		while(i!=0 && !isInterrupted()) {
			System.out.println(i--);
			for(long x=0;x<2500000000L;x++); // 시간 지연
		}
		System.out.println("카운트가 종료되었습니다.");
	}
}

5
4
3
2
입력하신 값은 apple 입니다.
isInterrupted():true
isInterrupted():true
Interrupted():false
Interrupted():false
카운트가 종료되었습니다.

  • 카운트다운을 하다가 값을 입력하면 th1.interrupt() 메서드를 호출해서 카운트다운을 종료하는 프로그램이다.
System.out.println("isInterrupted():"+ th1.isInterrupted()); // true
System.out.println("isInterrupted():"+ th1.isInterrupted()); // true
  • th1.isInterrupted() 메서드는 그저 interrupted 상태를 반환하기 때문에 몇번을 호출해도 true가 나온다.
System.out.println("Interrupted():"+ th1.interrupted());
System.out.println("Interrupted():"+ th1.interrupted()); 

Interrupted():false
Interrupted():false

  • th1.interrupted() 메서드는 interrupted 상태를 반환하고 false로 초기화시킨다. 예상한 결과는 true, false가 나와야 하는데 실제 결과는 두 번 모두 false가 나왔다. 왜 그런걸까 ?
    -> 이유는 interrupted() 는 static 메서드이기 때문에 현재 쓰레드를 반환한다. 즉, th1.interrupted()는 main쓰레드의 상태를 반환하는 것이다. 따라서 ThreadEx9_1 쓰레드 안에서 호출을 해줘야 한다.

수정된 코드🔽

import javax.swing.JOptionPane;

class Ex13_9 {
	public static void main(String[] args) throws Exception {
		ThreadEx9_1 th1 = new ThreadEx9_1();
		th1.start();

		String input = JOptionPane.showInputDialog("아무 값이나 입력하세요.");
		System.out.println("입력하신 값은 " + input + " 입니다.");
		th1.interrupt();  // interrupted 상태를 true로 변경
	}
}

class ThreadEx9_1 extends Thread {
	public void run() {
		int i = 5;

		while(i!=0 && !isInterrupted()) {
			System.out.println(i--);
			for(long x=0;x<2500000000L;x++); // 시간 지연
		}
        System.out.println("isInterrupted():"+ this.isInterrupted()); // true
		System.out.println("isInterrupted():"+ this.isInterrupted()); // true
		System.out.println("Interrupted():"+ Thread.interrupted());
		System.out.println("Interrupted():"+ Thread.interrupted()); 
		System.out.println("카운트가 종료되었습니다.");
	}
}

5
4
입력하신 값은 bbbb 입니다.
isInterrupted():true
isInterrupted():true
Interrupted():true
Interrupted():false
카운트가 종료되었습니다.

  • this.isInterrupted()는 static 메서드가 아니기 때문에 main 쓰레드에서 th1.isInterrupted() 로 작성해도 문제가 없지만 해당 쓰레드안에서 작성할 때 this(생략가능) 붙여도 된다는걸 보여주기 위해 수정하였다.

  • Thread.interrupted() : 이렇게 작성해야 예상했던대로 결과가 올바르게 출력된다.
    첫 번째 Thread.interrupted()는 true를 반환하고 false로 초기화
    두 번째 Thread.interrupted()는 초기화한 false를 반환

suspend(), resume(), stop()

쓰레드의 실행을 일시정지, 재개, 완정정지 시킨다.
-> 이 3개의 메서드는 교착상태(dead-lock)에 빠지기 쉬워서 deprecated 되었다.

class Ex13_10 {
    public static void main(String[] args) {
        RunImplEx10 th1 = new RunImplEx10("*");
        RunImplEx10 th2 = new RunImplEx10("**");
        RunImplEx10 th3 = new RunImplEx10("***");
        th1.start();
        th2.start();
        th3.start();

        try {
            Thread.sleep(2000);
            th1.suspend();
            Thread.sleep(2000);
            th2.suspend();
            Thread.sleep(3000);
            th1.resume();
            Thread.sleep(3000);
            th1.stop();
            th2.stop();
            Thread.sleep(2000);
            th3.stop();
        } catch (InterruptedException e) {}
    } // main
}

class RunImplEx10 implements Runnable {
    volatile boolean suspended = false;
    volatile boolean stopped = false;

    Thread thread;

    public RunImplEx10(String name) {
        thread = new Thread(this, name);
    }

    public void start() {
        thread.start();
    }

    public void stop() {
        stopped = true;
    }

    public void suspend() {
        suspended = true;
    }

    public void resume() {
        suspended = false;
    }

    public void run() {
        while (!stopped) {
            if (!suspended) {
               System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    } // run()
}
class RunImplEx10 implements Runnable {
    volatile boolean suspended = false;
    volatile boolean stopped = false;

    Thread thread;

    public RunImplEx10(String name) {
        thread = new Thread(this, name);
    }
    ...
  • Runnable을 구현한 클래스 안에 Thread을 선언하고 생성자를 통해서 Thread 객체를 생성할 수 있다. 그럼 main메서드에서는 RunImplEx10 th1 = new RunImplEx10("*"); 이렇게 생성해주면 된다.

  • volatile boolean suspended = false; : volatile을 붙이는 이유는 자주 바뀌는 변수이기 때문이다. 멀티 쓰래드 어플리케이션에서 각 쓰래드들은 성능적이 이유로 메인 메모리로 부터 변수를 읽어 CPU 캐시에 복사하고 작업한다.
    이때 같은 변수에 대해 복사본을 가지고 각 쓰레드들이 작업을 하게 되면 동기화가 안되는 문제가 발생할 수 있다. 이를 방지하기 위해 volatile 키워드를 붙이게 되면 변수를 CPU 캐시에 복사하는게 아니라 메인 메모리에서 직접 변수를 가져와서 작업을 한다.

  • suspended 변수와 stopped 변수를 선언하고 suspended(), start(), resume(), stop() 메서드를 만든 후 run() 메서드 안에 while (!stopped), if (!suspended) 두 조건문을 붙여주면 deprecated 경고가 안뜬다.

join()

: 지정된 시간동안 특정 쓰레드가 작업하는 것을 기다린다.
void join() : 작업이 모두 끝날 때까지 기다린다.
void join(long millis) : millis 시간동안 기다린다. (단위 천분의 일초)
void join(long millis, int nanos) : 천분의 일초 + 나노초 동안 기다린다.

예외처리를 해야한다.(InterruptedException이 발생하면 작업 재개)

  • sleep() 메서드와 똑같이 해당 작업을 중간에 빠져나오기 위해 InterruptedException을 이용하는 것이다.
class Ex13_11 {
    static long startTime = 0;

    public static void main(String[] args) {
        ThreadEx11_1 th1 = new ThreadEx11_1();
        ThreadEx11_2 th2 = new ThreadEx11_2();
        th1.start();
        th2.start();
        startTime = System.currentTimeMillis();

        try {
            th1.join(); // main쓰레드가 th1의 작업이 종료될 때까지 기다린다.
            th2.join(); // main쓰레드가 th2의 작업이 종료될 때까지 기다린다.
        } catch (InterruptedException e) {
        }

        System.out.print("소요시간 : " + (System.currentTimeMillis() - Ex13_11.startTime));
    } // main
}

class ThreadEx11_1 extends Thread {
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("-");
        }
    } // run()
}

class ThreadEx11_2 extends Thread {
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.print("|");
        }
    } // run()
}

--|||||----------------------|||||||||------||||||||||||||||||||||||||||||||||||||||||||---------|||||||||||||||||||||||||||||||||||||||||||||||||||---------------|||||||||||||||||||||||||||||||||----------------------------------------|||||||--------------------------------------------------------------------------------------------------|||-----------------------|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||----|||||||||---------------------------------------------------------------------------------소요시간 : 14

  • th1과 th2쓰레드가 모두 작업을 마칠 때까지 main쓰레드를 기다리게 한 뒤 소요시간을 출력한다.

  • join()을 주석처리하게 되면 main쓰레드는 th1과 th2.start()를 호출하고 소요시간 출력 후 종료되기 때문에 소요시간이 0이 찍힌다.

yield()

남은 시간을 다음 쓰레드에게 양보하고, 자신(현재 쓰레드)은 실행대기 상태가 된다.

yield()와 interrupt()를 적절히 사용하면, 응답성과 효율을 높일 수 있다.

public void run() {
    while (!stopped) {
        if (!suspended) {
           System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        } else {
        Thread.yield();
        }
    }
} // run()
...

public void stop() {
    stopped = true;
    thread.interrupt();
}

public void suspend() {
    suspended = true;
    thread.interrupt();
}
  • if (!suspended) -> false 이고 while (!stopped) -> true라면 쓰레드가 종료되지 않고 일시정지된 상태에서 while문만 계속 돌고 있는 상태가 된다. 이런 것을 busy-waint 상태라고 하고 이를 방지하기 위해 else { Thread.yield(); } 추가해줄 수 있다.

  • stop(), suspend()interrupt()를 추가해 응답성을 높일 수 있다. 보통 쓰레드를 만들 때 특정 쓰레드가 작업을 독점하지 않도록 sleep() 메서드를 넣어준다.
    그런데 만약 일시정지를 했을때 sleep() 메서드로 인해 쓰레드가 잠을 자고 있었다면 interrupt() 메서드로 그 즉시 sleep()을 빠져나가게 할 수 있도록 해야 한다.
    이를 통해, 응답성을 높일수 있다.

yield() 메서드도 static 메서드이기 때문에 자기 자신한테만 사용할 수 있다.

하지만 yield() 또한 OS 스케쥴러에게 희망사항을 전달하는 것 뿐이다. 반드시 동작한다고 볼 수는 없다. 결국 OS 스케쥴러가 전체 프로그램들의 작업 효율을 따져 시간 배분을 한다.


📌쓰레드의 동기화(synchronization)

: 진행중인 작업이 다른 쓰레드에게 간섭하지 못하게 막는 것을 "동기화"라고 한다.

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

동기화하려면 간섭 받지 않아야 하는 문장들을 "임계 영역(critical section)"으로 설정한다.

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

synchronized를 이용한 동기화

synchronized로 임계 영역을 설정하는 2가지 방법

  1. 메서드 전체를 임계 영역으로 지정
public synchronized void withdraw(int money) {
	if (balance >= money) {
    	try {
        	Thread.sleep(1000);
        } catch (Exception e) {}
        
        balance -= money;
    }
}
  1. 특정한 영역을 임계 영역으로 지정
public void withdraw(int money) {
	synchronized(this) {
    	if (balance >= money) {
    		try {
        		Thread.sleep(1000);
        	} catch (Exception e) {}
        
        	balance -= money;
    	}
    } // synchronized(this)
}
  • 임계 영역은 개수도 최소화하고 영역도 최소화하는게 좋다. 임계 영역에는 1번에 1개의 쓰레드만 접근할 수 있기 때문에 너무 많이, 넓게 설정한다면 프로그램의 성능이 저하된다.

실습 예제

class Ex13_12 {
    public static void main(String[] args) {
        Runnable r = new RunnableEx12();
        new Thread(r).start();
        new Thread(r).start();
    }
}

class Account {
    private int balance = 500;

    public int getBalance() {
        return balance;
    }

    // 잔고(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) {
            int money = (int) (Math.random() * 3 + 1) * 100; // 100, 200, 300 중 랜덤한 money 생성
            acc.withdraw(money);
            System.out.println("balance:" + acc.getBalance());
        }
    } // run()
}

balance:400
balance:400
balance:300
balance:100
balance:0
balance:-100

withdraw() 메서드를 살펴보면 if (balance >= money) 이런 조건일 때만 메서드가 동작하게 되어있다. 즉, 잔고(balance)가 출금하려는 돈(money)보다 많거나 같을 때만 출금할 수 있는 것이다. 따라서 잔고는 -가 출력이 될 수 없는데 출력을 보면 -100이 출력되었다. 왜 그럴까?
-> 이유는 두 쓰레드가 동시에 withdraw() 접근했기 때문이다.
예를 들어, 한 쓰레드가 잔고가 100일 때 withdraw()에 접근했다. 잔고가 남아있기 때문에 if 조건문을 통과하고 잔고에서 돈을 빼는 작업을 수행하려고 할 때 다른 쓰레드가 거의 동시에 withdraw() 접근하게 되면 이 쓰레드 또한 아직 잔고는 100이기 때문에 if 조건문을 통과하게 된다.
여기서 문제가 발생한다. 먼저 접근한 쓰레드가 돈을 빼가서 잔고가 0이 되었음에도 불구하고 나중에 접근한 쓰레드는 if 조건문을 이미 통과했기 때문에 0인 잔고에서 또 돈을 빼가는 상황이 벌어지고 결과적으로 balance:-100이 찍히게 되는 것이다.

이러한 문제를 해결하기 위해 나온것이 "동기화"이다.

동기화를 통해 문제를 해결한 소스 코드🔽

...
class Account {
    private int balance = 500; // private으로 설정해야 동기화가 의미가 있다.

    // 잔고를 확인하는 중간에 출금이 되면 안되니까 여기도 동기화를 설정해줘야 한다.
    public synchronized int getBalance() {
        return balance;
    }

    // 돈을 출금할 때는 한 쓰레드만 접근할 수 있도록 동기화 설정
    public synchronized void withdraw(int money) {
        if (balance >= money) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            balance -= money;
        }
    } // withdraw
}
...
  • 잔고(balance)는 private으로 설정해줘야 동기화하는 의미가 있다. 출금하는 메서드를 동기화해줘도 잔고 자체에 다른 쓰레드가 직접 접근을 해서 변경하면 동기화해주는 의미가 없기 떄문이다.
  • withdraw()에 동기화를 해주는 것은 물론 getBalance() 또한 동기화를 해주어야 한다. getBalance()를 통해 잔고를 확인하는 동안에 출금하는 메서드가 수행되어 잔고가 바껴서는 안되기 때문이다.
    잔고를 읽고 쓰는 메서드에는 모두 동기화해줘야 한다.

📌wait()과 notify()

동기화를 하게 되면 한번에 한 쓰레드만 접근할 수 있기 때문에 프로그램의 효율이 떨어진다.
동기화의 효율을 높이기 위해 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"};
    final int MAX_FOOD = 6;
    private ArrayList<String> dishes = new ArrayList<>();

    public synchronized void add(String dish) {
        // while문 : 테이블에 음식이 가득찼으면 COOK 쓰레드 wait()
        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);
        // 음식을 추가했으니 waiting pool에서 대기중인 CUST 쓰레드 notify()
        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 {
                    // 테이블에 음식이 없으니 CUST 쓰레드 wait()
                    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);
                        // 음식을 하나 먹었으니 테이블이 꽉차서 waiting pool에 있던 COOK 쓰레드 notify()
                        notify();
                        return;
                    }
                }

                try {
                    System.out.println(name + " is waiting.");
                    // 위의 for 문에서 자기가 원하는 음식을 찾지 못한 CUST 쓰레드 wait()
                    wait();
                    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();
        // donut만 먹는 CUST1 쓰레드, burger만 먹는 CUST2 쓰레드
        new Thread(new Customer2(table, "donut"), "CUST1").start();
        new Thread(new Customer2(table, "burger"), "CUST2").start();
        Thread.sleep(2000);
        System.exit(0);
    }
}
  • 총 4개의 쓰레드가 있다.
    • 2초 일시정지했다가 프로그램을 종료해버리는 main 쓰레드
    • donut, burger 2가지 음식을 만드는 COOK 쓰레드
    • donut만 먹는 CUST1 쓰레드
    • burger만 먹는 CUST2 쓰레드
  • Table 클래스에는 음식을 만드는 add()와 음식을 먹는(제거하는) remove()가 있다. 이 두 메서드는 여러 쓰레드가 동시에 접근하면 안되기 때문에 동기화를 해주었다.

wait()

public synchronized void add(String dish) {
    // while문 : 테이블에 음식이 가득찼으면 COOK 쓰레드 wait()
    while (dishes.size() >= MAX_FOOD) {
        String name = Thread.currentThread().getName();
        System.out.println(name + " is waiting.");
       try {
            wait();
            Thread.sleep(500);
        } catch (InterruptedException e) {}
    }
    ...
  • COOK 쓰레드는 Table 객체에 dishes라는 ArrayList에 add()를 통해서 음식을 추가한다.
    이때 dishes.size()MAX_FOOD 이상이 되면 wait() 을 통해 락을 풀고 waiting pool에서 음식이 제거될 때까지 대기한다.
public void remove(String dishName) {
    synchronized (this) {
        String name = Thread.currentThread().getName();

        while (dishes.size() == 0) {
            System.out.println(name + " is waiting.");
            try {
                // 테이블에 음식이 없으니 CUST 쓰레드 wait()
                wait();
                Thread.sleep(500);
            } catch (InterruptedException e) {}
        }
        ...
  • CUST 쓰레드는 remove()로 음식을 먹는다. 만약, dishes.size()가 0이라면 음식이 없다는 뜻이고 이때 CUST 쓰레드는 wait() 을 통해 락을 풀고 waiting pool에서 음식이 추가될 때까지 대기한다.
public void remove(String dishName) {
    synchronized (this) {
        String name = Thread.currentThread().getName();
		while (dishes.size() == 0) {
			...
       }

       while (true) {
           for (int i = 0; i < dishes.size(); i++) {
               if (dishName.equals(dishes.get(i))) {
                   dishes.remove(i);
                   // 음식을 하나 먹었으니 테이블이 꽉차서 waiting pool에 있던 COOK 쓰레드 notify()
                   notify();
                   return;
               }
           }

           try {
               System.out.println(name + " is waiting.");
               // 위의 for 문에서 자기가 원하는 음식을 찾지 못한 CUST 쓰레드 wait()
               wait();
               Thread.sleep(500);
           } catch (InterruptedException e) {}
       } // while(true)
   } // synchronized
}
  • remove()의 두 번째 while 문에서 CUST 쓰레드는 음식을 먹는 작업을 수행한다.(두 번째 while 문안의 for 문)
    CUST1 쓰레드는 도넛만 먹고 CUST2 쓰레드는 버거만 먹는데 dishes에 원하는 음식이 없다면 for 문을 통과하게 되고 이때 자기가 원하는 음식이 나올 때까지 wait() 을 통해 락을 풀고 waiting pool에서 대기한다.

notify()

public synchronized void add(String dish) {
	...
    dishes.add(dish);
        // 음식을 추가했으니 waiting pool에서 대기중인 CUST 쓰레드 notify()
    notify();
    System.out.println("Dishes:" + dishes.toString());
}
  • 음식이 추가됐으니 아까 dishes.size()가 0이어서 wait() 했던 CUST 쓰레드를 notify()를 통해 깨운다.
public void remove(String dishName) {
    synchronized (this) {
        String name = Thread.currentThread().getName();
		while (dishes.size() == 0) {
			...
       }

       while (true) {
           for (int i = 0; i < dishes.size(); i++) {
               if (dishName.equals(dishes.get(i))) {
                   dishes.remove(i);
                   // 음식을 하나 먹었으니 테이블이 꽉차서 waiting pool에 있던 COOK 쓰레드 notify()
                   notify();
                   return;
               }
            }
			...
       } // while(true)
   } // synchronized
}
  • 두 번째 while 문안에서 for 문을 통해 음식을 하나 먹었으니 아까 dishes.size()가 꽉차서 wait()로 대기중인 COOK 쓰레드를 notify()를 통해 깨운다.

예제에서도 알 수 있듯이 wait(), notify() 어떤 쓰레드를 대기시키고 깨우는지 알 수 없다. 이를 보완하기 위해 Lock 인터페이스와 Condition 인터페이스가 나왔다.이 두 인터페이스를 이용하면 쓰레들을 구분해서 작업할 수 있다.

0개의 댓글