지난 번에 올린 이후 포스트가 한동안 뜸했네요. 저번에 설명을 하다 마무리가 안된 multi-thread programming에 대해 마지막 포스트를 올려봅니다.

사용하려는 thread 보다 리소스가 많은 경우엔 별다른 문제가 발생하지 않을것입니다. 하지만 리소스는 한정되어 있고 사용하려는 thread가 이보다 많은 경우는 thread는 리소스가 available할 때까지 대기해야 합니다. 가령 웹서버에 동시에 request가 1000건이고 이 request가 모두 db와 transaction을 해야하는 상황에서, db와 연결된 connection 은 100개 뿐이라면 리소스(connection)을 차지하지 못한 나머지 900개 thread는 다른 thread가 처리를 끝내고 가능한 connection 이 생길때 까지 기다려야 해야 하는 것입니다.

자바에서는 이러한 thread 대기를 위해 wait()/notify()/notifyAll() 메서드를 제공합니다. 이 메서드들은 Thread 클래스의 메서드가 아닌  Object 클래스의 메서드 입니다. 모든 클래스는 Object에서 상속을 받으므로 모든 클래스가 이 메서드들을 사용할 수 있습니다.
앞에서 예를 든 것처럼 thread가 사용하려는 리소스가 없는 경우 thread는 잠시 대기 상태로 빠져야 합니다. thread를 대기 상태로 만드는 메서드가 wait() 메서드입니다. 대기 상태를 다른말로 non-runnable 상태라고 합니다.  
non-runnable상태인 thread를 깨우는 메서드가 notify()나 notifyAll()입니다. 두 메서드의 차이는 잠시 후에 설명하겠습니다.
그럼 직접 예제를 보면서 확인해 보도록 하겠습니다.
예제의 내용을 간략히 설명하면 도서관에 책이 3권이 있습니다. 그리고 6명의 학생이 책을 빌리려고 합니다. 책을 빌리려는 학생을 Thread 클래스로 구현합니다(이하 Student). Student 클래스가 하는 일은 라이브러리에서 책을 빌리고, 5초간 책을 읽고 (sleep(5000);) 반납합니다. 이때 3권의 책은 Student가 사용하려는 리소스가 되고 6명의 학생이 동시에 책을 빌리는 것은 6개의 thread가 한정된 리소스를 동시에 사용하려는 상황과 같습니다. 책(리소스)가 3권이기때문에 단 3명의 학생만이 책을 빌릴 수 있습니다. 도서관에 빌릴 책이 없을 때(리소스가 유효하지 않을 때) wait()메서드를 호출하여 해당 메서드를 수행하고 있는 thread를 대기 상태로 만듭니다. (아래 코드 lendBook() 메서드 부분)

누군가 책을 다 읽고 나서 반납을 하게 되면 기다리는 thread를 깨워야 합니다. 이때 호출되는 메서드가 notify()/notifyAll()입니다.  (아래 코드 returnBook()부분)
소스는 다음과 같습니다.

package thread;
import java.util.ArrayList;
class NextLibrary{
public ArrayList<String> shelf = new ArrayList<String>();

public NextLibrary(){
shelf.add(“태백산맥 1”);
shelf.add(“태백산맥 2”);
shelf.add(“태백산맥 3”);
}

public synchronized String lendBook() throws InterruptedException{
Thread t = Thread.currentThread();  //현재 이 메서드를 실행하고 있는 thread
while( shelf.size() == 0 ){
System.out.println(t.getName() + ” waiting start”);
wait();
System.out.println(t.getName() + ” waiting end”);
}
String book = shelf.remove(0);
System.out.println(t.getName() + “: ” + book + ” lend”);
return book;
}

public synchronized void returnBook(String book){
Thread t = Thread.currentThread();
shelf.add(book);
notifyAll();
System.out.println(t.getName() + “: ” + book + ” return”);
}
}

class Student extends Thread{
public void run(){
try{
String title = LibraryMain.library.lendBook();
if( title == null ) return;
sleep(5000);
LibraryMain.library.returnBook(title);
}catch (InterruptedException e) {
System.out.println(e);
}
}
}

public class LibraryMain {
public static NextLibrary library = new NextLibrary();
public static void main(String[] args) {
Student std1 = new Student();
Student std2 = new Student();
Student std3 = new Student();
Student std4 = new Student();
Student std5 = new Student();
Student std6 = new Student();

std1.start();
std2.start();
std3.start();
std4.start();
std5.start();
std6.start();
}
}

책을 저장하고 있는 배열은 ArrayList로 구현하였습니다. 각 thread가 대기하고 깨어날때 로그를 찍기 위한 코드가 있습니다. 
lendBook() 메서드를 보면 책장에 책이 더이상 없는 경우 wait()메서드를 호출하여 thread를 대기 상태로 만듭니다. 그리고 returnBook() 메서드에서  shelf.add()가 불려지면(책이 반납되면 ) notifyAll() 메서드를 호출하여 대기중인 thread를 runnable 상태로 만듭니다.
notify() 와 notifyAll() 의 차이는 notify()는 wait()에 의해 대기중인 thread를 단 하나만 깨우는 것이고 notifyAll()은 대기중인 모든 thread를 깨우게 됩니다. 자바doc 을 보면 notify() 메서드는 무작위로
thread를 깨우기 때문에 영원히 못 깨어나는 thread가 있을 수도 있고 notifyAll()을 사용하여 모든
thread를 깨우고 동일한 경쟁 상태에서 scheduler에  CPU를 할당 받도록 하는 것이 더 공정하다고 되어있습니다. 따라서 notify()보다는 notifyAll()을 사용하는 것을 권장 합니다.  이때 CPU를 할당받지 못해 리소스를 차지하지 못한 thread의 경우에 다시 대기 상태가 되도록 합니다. (위에 lendBook() 메서드 코드를 참고하시기 바랍니다)
위 코드의 출력 결과는 다음과 같습니다. (출력 결과는 사용하는 머신에 따라 약간의 차이가 있을 수 있습니다)

각 Thread의 대여 상태와 반납 상태를 확인할 수 있습니다.
처음 3개의 Thread(0,1, 2) 가 대여를 했고, 나머지 Thread(3,4,5)은 대기 상태가 되었습니다.
Thread-0 이 책을 반납하면 3, 5, 4 가 모두 깨어나 그중 Thread-3 이 책을 대여하고 나머지 5, 4 는 다시 대기 상태가 되는 것을 알 수 있습니다.
실행 해보면 매번 약간의 차이는 있지만, 책이 없을 때 Thread가 대기하고 notifyAll()에 의해 모든
Thread가  깨어난 다음 리소스를 차지하지 못한 Thread는 다시 대기 상태가 되는것을 알 수 있습니다.
모든 Thread가 책을 대여하고 반납하면 프로그램은 종료됩니다.
여기까지가 자바 multi- thread programming 에 대한 내용이었습니다.