이번엔 Java로 Thread를 사용하면서 쓰게되는 wait()과 notify() 그리고 synchronized에 대해 정리하고자 한다.
먼저 wait()함수는 Java의 모든 클래스의 조상 클래스인 Object 클래스에 정의된 함수로 호출 시 쓰레드가 현재 사용하고 있는 공유 객체에 대한 락을 놓고 해당 공유 객체의 waiting-pool 영역(각 공유 객체 당 따로 대기 영역이 존재한다.)으로 들어가 대기하게 된다.
notify() 함수는 waiting-pool에서 대기하고 있는 쓰레드 중 하나를 임의로 깨운다. 깨운다고 해서 바로 해당 쓰레드가 일을 하는 것은 아니고 Runnable 상태로 만들어 스케줄러 정책에 의해 자원을 할당 받아야 실행 상태가 된다.
여기서 중요한 것은 위 두 함수는 synchronized 블럭 안에서만 호출 가능하다는 것인데, synchronized는 해당 구역에 대해 lock을 걸어 하나의 쓰레드만 공유 자원을 사용할 수 있도록 해준다.
다음은 쓰레드의 라이프 사이클을 그림으로 나타낸 것이다.
wait을 호출하면 Running 상태이던 쓰레드가 Non-Runnable 상태로 들어가고 notify를 호출하면 Non-Runnable 상태에서 Runnable 상태로 돌아간다. 이 중 스케줄러 정책에 의해 쓰레드들이 실행된다.
class Producer extends Thread {
private Buffer blank;
//constructor goes here
public Producer(Buffer b) {
this.blank = b;
}
public void run() {
for (int i=0; i<10; i++) {
blank.put(i);
System.out.println("Producer: Produced " + i);
try {
sleep((int)(Math.random()*100));
} catch (InterruptedException e) {
}
}
}
}
class Consumer extends Thread {
private Buffer blank;
//constructor goes here
public Consumer(Buffer b) {
this.blank = b;
}
public void run() {
int value = 0;
for (int i=0; i<10; i++) {
value = blank.get();
System.out.println("Consumer: Consumed " + value);
}
}
}
class Buffer {
private int contents;
private boolean available = false;
public synchronized int get() {
//if there is no available contents, keep waiting
//if there is available contents, print the contents on screen (this is like consuming new contents), and notify
//set available to false
//return contents
if (this.available == false) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("contents: " + contents);
this.available = false;
notify();
return this.contents;
}
public synchronized void put(int value) {
//if there is available contents, keep waiting
//if there is no available contents, store value to contents (this is like producing new contents), and notify
//set available to true
if(this.available == true) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.contents = value;
this.available = true;
System.out.println("produce: " + value);
notify();
}
}
public class Homework01ProducerConsumerProblem {
public static void main(String[] args) {
Buffer b = new Buffer();
Producer p1 = new Producer(b);
Consumer c1 = new Consumer(b);
p1.start();
c1.start();
}
}
코드를 간략히 설명하면 Consumer와 Producer가 하나의 Buffer 객체를 생성자로 주입받아 공유하며 1부부터 10까지 Consumer는 출력하고 Producer는 contents 변수에 값을 담는 코드이다. 여기서 주목할 것은 Buffer 클래스의 get(), put()함수가 공유 자원인 contents와 available을 사용하는 critical section이고 여기에 synchronized 키워드가 사용되어 동기화를 하는 것을 볼 수 있다.
Java에서는 쓰레드간 동기화를 위해 Monitor lock을 사용하는데 모든 객체는 고유의 Monitor를 가지고 있다. 모니터 구조는 다음과 같다.
이 링크에 설명이 굉장히 잘 되어 있다.
Monitor 설명
이해한 대로 다시 정리해보면,
synchronized 키워드가 사용된 객체(클래스)는 공유 객체가 되고, 그 객체가 가진 멤버 변수들이 공유 자원이 되며 synchronized 키워드가 사용된 메소드가 프로시저가 된다. 특징을 정리하면,