제 13 장 쓰레드(Thread)
Objective
▶Thread란 무엇인가?
▶Java에서 Thread를 정의하고 Create하는 방법 (implements Runnable)
▶Thread를 시작하는 방법
▶Thread의 기본적인 Control
▶Thread를 만드는 또 하나의 방법(extends Thread)
▶Synchronized
▶Thread Interaction
▶Thread control in JDK1.2
Thread
▷ 쓰레드란 프로그램 내에서의 순차적인 제어의 흐름(flow of control)이다. 즉 동일한 프로그램 내에서 다른 제어의 흐름과 독립적으로 실행될 수 있는 코드를 뜻한다.
▷ 쓰레드와 프로세스는 서로 다르다. 프로세스가 서로 완전히 독립적으로 수행되는 것이라면, 한 프로그램내의 쓰레드들은 서로 완전히 독립적이 아니어서 어떤 변수나 파일을 쓰레드들이 서로 공유하도록 하기에 편리하다. 즉, 다중 쓰레드는 다중 프로세스보다 다루기가 편리하다고 할 수 있다.
▷ 즉, 자바는 운영체제의 도움 없이도 언어 자체에서 일종의 멀티프로세싱인 멀티 쓰레드를 지원하는데 이것을 이용하면 unix 같은 운영체제에서 지원하는 멀티프로세싱보다 훨씬 프로그래밍작성 하기에 편리하다.
Thread 종류
▷ 데몬쓰레드는 입출력처리, garbage collection, 사용자 쓰레드의 구동 등을 지원해주는 JVM이 제공하는 쓰레드이다.
▷ 사용자쓰레드는 일반 프로그램에서 만들어지는 쓰레드이다.
▶ 예를들어 main() 메소드에서 만든 사용자쓰레드는 main()이 수행되는 동안 살아있고(alive) main()이 종료될 때 같이 종료된다. 특별한 언급이 없으면 사용자쓰레드에 관한 사항만 다룬다.
쓰레드 사용 예제)
- public class OurClass{
public void run(){
for(int i=1;i<100;i++){
System.out.println("Hello");
}
}
}
- import java.applet.Applet;
- public class OurApplet extends Applet{
public void init(){
OurClass oc=new OurClass();
oc.run();
}
}
- import java.applet.Applet;
- public class OurApplet extends Applet{
public void init(){
OurClass oc=new OurClass();
oc.start();
}
}
start()가 run()를 호출하는 것은 틀림없는 사실이지만 이것이 다른 쓰레드에서 이뤄진다는 것은 큰 차이점이다. 이것은 start()가 다른 쓰레드를 생성한다는 것을 의미한다. 이새로운 쓰레드는 어느정도 초기화 작업을 수행한후 run()를 호출하게 되고 run()가 수행 완료되었을 쓰레드의 소멸작업을 맏게 된다.start()의 호출은 신속히 리턴된다. 그러므로 start()가 최초의 쓰레드로 리턴됨과 동시에 run()는 새롭게 생성된 쓰레드에서 실행된다.
- import java.util.*;
- public class ThreeThread{
- public static void main(String args[]){
- TestThread t1 = new TestThread("Thread1",(int)(Math.random()*2000));
- TestThread t2 = new TestThread("Thread2",(int)(Math.random()*2000));
- TestThread t3 = new TestThread("Thread3",(int)(Math.random()*2000));
- t1.start();
- t2.start();
- t3.start();
- try{
- t1.join();
- t2.join();
- t3.join();
- //join() // 다른 쓰레드가 종료하면 바로 시작하도록 대기함
- }catch(InterruptedException e){
- System.out.println("Interrupted Exception ::"+ e);
- }
- }// end of main
- }//end of ThreeThreads
- class TestThread extends Thread{
- private String whoami;
- private int delay;
- public TestThread(String s, int d){
- whoami =s;
- delay = d;
- }
- public void run(){
- try{
- sleep(delay);
- }catch(InterruptedException e){}
- System.out.println("Hello World!"+whoami+""+new Date());
- }//end of run()
- }// end of class TestThread
Thread 생성
▷ Thread를 상속하고 run() 메소드의 내용을 중복하여 정의하는 방법
▷ Runnable 인터페이스를 구현하는 방법
▷ 첫 번째 방법에서 쓰레드를 생성하려면 Thread 타입의 객체를 만들고 이 객체의 start() 메소드를 호출하면 쓰레드가 생성되어 구동된다.
▷ Thread 객체에서 start()를 호출하면 이 쓰레드를 thread scheduler 라고 하는 프로그램에 등록하는데 thread scheduler는 모든 쓰레드의 실행을 관리하는 시스템 프로그램이다.
▷ start()를 호출한다고 그 객체가 즉시 실행되는 것이 아니며 그 쓰레드가 실행될 차례가 되면 run() 메소드에 구현해둔 내용을 실행하는 것이다.
▷ 즉, 이 쓰레드 객체가 실행된때 동작할 내용을 미리 run() 메소드에 구현해 두어야 한다.
▷ 쓰레드에서 일반 프로그램(즉, 단일 쓰레드)의 main()과 같은 기능을 하는 것이 run() 메소드이다.
▷ main() 메소드를 JVM이 자동으로 호출해주는 것과 달리 쓰레드의 run() 메소드는 start()를 호출한 이후에 실행될 수 있는 것이다(차례가 되면).
▷ run() 메소드는 static 메소드가 아니므로 여러 가지 오버로딩된 메소드를 만들어 사용할 수 있다.
기본적으로 대부분이 void run()이다.
▷ 쓰레드를 만드는 방법은 위에 언급한 방법을 포함하여 두가지가 있다.
(1) Thread를 상속하는 방식
- public class Test {
- public static void main(String[] args) {
- new A().start();
- }
- }
- class A extends Thread {
- public void run() {
- System.out.println("Thread of Class A is running now...");
- }
- }
▷ Test에서 만일 new A().run()과 같이 run()을 직접 호출하여도 화면에 "Thread ..."이 나타나지만 이것은 쓰레드가 생성되어 나타난 결과가 아니라 단순히 클래스 A의 run() 이라는 메소드가 호출된 것이다.
▷ 반드시 쓰레드 타입 객체 A의 start()를 호출하여야만 클래스 A가 자신을 쓰레드로서 thread schedulrer에 등록하게 되며 이 쓰레드가 실행될 차례가 오면 run()이 병행하여(in parallel) 실행되는 것이다.
▷ run()외에 클래스 Thread가 지원하는 주요 메소드는 다음과 같다.
start()/* 쓰레드를 시작한다 */
destroy()/* 쓰레드를 소멸시킨다 */
getName()/* 현재 실행중인 쓰레드의 이름을 얻는다 */
interrupt()/* 쓰레드에게 인터럽트를 알린다 */
▷ 이 외에도 쓰레드를 일시 중지시키기 위해서는(예를 들면 브라우져에서 다른 웹 페이지로 가는 경우) suspend()메소드가 사용되며, 일시 중지된 쓰레드를 다시 동작 시키기 위해서는 resume()메소드가 사용된다.
▷ Applet이 완전히 종료될 때 destroy() 메소드가 호출되는데 destroy() 메소드에서 쓰레드의 동작을 완전히 정지시키는 stop()을 호출한다.
▷ 위에서 예제를 inner class로 만든 것은 아래와 같다.
- public class Test {
- public static void main(String[] args) {
- new Test().new A().start();
- }
- class A extends Thread {
- public void run() {
- System.out.println("Thread of Class A is runnign now...");
- }
- }
- }
▷ 위에서 외부 클래스(Test)의 객체를 먼저 만든 후 내부클래스를 아래와 같이 new로 만들고 최종적으로 start()를 부르고 있다.
new Test().new A().start();
▷ 이 프로그램에서 다음과 같이 내부클래스를 static으로 선언하면(즉, top-level의 클래스로 하여) 외부클래스의 객체를 만들 필요 없이 바로 내부클래스를 객체화하여 부를 수 있다.
- public class Test {
- public static void main(String[] args) {
- new A().start();
- }
- static class A extends Thread {
- public void run() {
- System.out.println("Thread of Class A is runnign now...");
- }
- }
- }
▷ 위의 프로그램을 더욱 간단히 하기 위하여 쓰레드 구동 부분을 다음과 같이 익명클래스로 만들 수도 있다.
- public class Test {
- public static void main(String[] args) {
- Thread t = new Thread() { // 익명클래스 시작
- public void run() {
- System.out.println("Thread is running now..");
- }
- };// 익명클래스 실행문 마감을 ; 로 해야 한다
- t.start();
- }
- }
(2) Runnable 인터페이스를 구현하는 방법
▷ Runnable이라는 인터페이스에 Thread와 관련된 클래스와 메소드들이 정의되어 있어 Runnable 인터페이스를 불러 쓰면 Thread와 관련된 각종 기능들을 수행할 수 있다.
▷ 쓰레드를 만드는 두 번째 방법으로 Runnable 인터페이스를 구현한다고 선언하고 이 클래스의 인스턴스를 Thread() 생성자의 인자로 주는 방법이다.
▷ 이 방법에서는 start()를 호출하기 위하여 Thread 타입의 객체를 하나 만들고 그 객체의 start()를 실행하여야 한다. Thread를 상속 받지 않았기 때문이다.
- public class Test {
- public static void main(String[] args) {
- Thread t = new Thread(new A());
- t.start();
- }
- }
- class A implements Runnable {
- public void run() {
- System.out.println("Thread of Class A is runnign now...");
- }
- }
▷ 위에서 Thread 객체 생성문 Thread t = new Thread(new A()) 는 다음의 표현과 같다.
- Runnable r = new A();// 타겟 객체
- Thread t = new Thread(r);
▷ 위와 같은 경우에 객체 r을 쓰레드 t의 target 이라고 한다.
▷ 한편 애플릿에서 init() 메소드 내에서 start()를 호출하는 경우에는 다음과 같이 target으로 this를 사용할 수 있다.
- public void init() {
- Thread t = new Thread(this);
- t.start();
- }
▷ 참고로 Runnable 인터페이스는 구현해야 할 메소드로 하나의 run() 만을 포함하고 있다.
▷ 위에 소개한 Runnable 구현 방식도 내부클래스를 사용하여 다시 작성할 수 있다.
- public class Test {
- public static void main(String[] args) {
- Thread t = new Thread(new A());
- t.start();
- }
- static class A implements Runnable {
- public void run() {
- System.out.println("Thread of Class A is runnign now...");
- }
- }
- }
▷ 한편 (1)에서는 new A().start()와 같이 start()를 직접 호출하였는데 여기서는 A 객체를 target으로 하여 Thread 객체 t를 먼저 만든 후 t.start() 로 호출했다.
▷ 이렇게 해야 하는 이유는 Runnable은 start() 메소드를 제공하지 않기 때문이다. //Thread 에서만 제공
▷ 위의 내부클래스를 사용하는 방식은 다시 익명클래스를 사용하는 방법으로 고쳐 쓸 수가 있다.
- public class Test {
- public static void main(String[] args) {
- Runnable r = new Runnable() {
- public void run() {
- System.out.println("Thread is runnign now");
- }
- };// 익명클래스 실행문 마감
- Thread t = new Thread(r);
- t.start();
- }
- }
▷ 지금까지 run() 이 정의되어 있는 객체의 쓰레드를 구동하는 방법만을 설명하였다. 그러나 다른 객체에 있는 run() 메소드를 구동하려면 target r 자리에 다른 객체의 참조를 쓰면 된다.
▷ (1)과 (2)에서 소개한 쓰레드 생성법 두가지를 정리하면 다음과 같다.
1) Thread의 하위클래스를 사용하고 run()의 내용을 중복구현
2) Runnable을 구현하는 클래스를 선언하고 이 객체의 인스턴스를 Thread() 생성자의 인자(target)으로 전달
▷ 자바는 다중 상속, 즉 둘 이상의 상위 클래스로부터 동시에 계승받는 것을 허용하지 않는다.
▷ 1)의 방법은 Test가 이미 Thread라는 클래스로부터 한번 계승을 받았기 때문에 TestThread가 또 다른 클래스로부터 계승을 받을 필요가 있을 때 다중 계승이 되어 오류가 발생한다.
▷ 2)의 방법은 Test가 쓰레드를 사용할 수 있으면서 필요하다면 동시에 다른 클래스로부터 계승을 받을 수 있다는 장점이 있다.
▷ 쓰레드는 다음과 같은 메소드들을 제공한다.
- String getName()// 쓰레드의 이름을 리턴
- void setName(String s)// 쓰레드의 이름을 지정
▷ 아래는 쓰레드에 이름을 주어 초기화 한 후에 그 이름을 화면에 출력하도록 위의 예제에서 main() 부분을 고친 것이다.
- public static void main(String[] args) {
- Runnable r = new A();
- Thread t = new Thread(r, "FirstThread");
- t.start();
- System.out.println("Thread name = " + t.getName());
- }
출력:
Thread name = FirstThread
Thread of Class A is running now...
(3)쓰레드의 종료
▷ run() 메소드가 리턴되면 쓰레드도 종료되는데 이때 쓰레드가 dead 되었다고 한다.
▷ 이러한 dead 쓰레드를 다시 실행시키려면 start()를 다시 호출하여 run() 메소드가 실행되도록 하여야만 한다.
▷ 한편 어떤 쓰레드가 stop()을 호출하면 이 쓰레드는 바로 dead가 된다.
t.stop();// 외부에서 쓰레드 t를 죽일 때
Thread.currentThread().stop();// 쓰레드가 스스로 죽을 때
Basic Control of Threads
sleep()
▷ 쓰레드가 지정된 시간 동안 동작을 멈추게 하는데 다음과 같이 ms 단위 또는 ns 단위로 멈추는 시간을 조정할 수 있다. (이것은 static 메소드이다.) 지정된 시간이 지나면 이 쓰레드는 바로 실행되는 것이 아니라 ready 상태로 간다.
- sleep(long millisec);// InterruptedException을 throw
- sleep(long milliSec, int nanoSec);// InterruptedException을 throw
▷ sleep()에서는 InterruptedException 예외를 처리할수 있다.
- try {
- t.sleep(10)// sleep 기본 단위는 ms
- } catch (InterrruptedException e) { }
- block()
▷ 네트웍으로부터 데이터 수신을 기다리는 경우에 데이터가 도착할 때까지 오래 기다릴 수도 있다. 이와같이 I/O에 오랜시간을 기다리게 되면 자바는 자동으로 이 쓰레드를 block 상태로 보낸다. 나중에 데이터를 읽을 수 있어 해당 I/O가 이루어지면 쓰레드는 ready 상태로 가게 된다.
▷ 한편 쓰레드는 synchronized로 선언된 코드부분의 monitor를 lock 하지 못하는 경우도 block이 될 수 있다. (뒤에 설명함)
▷ 기타 쓰레드가 제공하는 일반적인 메소드는 다음과 같다.
- isAlive()// 쓰레드가 살아있는지 확인
- join() // 다른 쓰레드가 종료하면 바로 시작하도록 대기함
▷ 어떤 쓰레드 ss가 현재 실행중인 쓰레드이면 ss.join()을 호출한 곳에서 프로그램이 기다리다가 쓰레드 ss가 종료하면 프로그램이 그곳에서부터 계속된다.
- ss.join();
한편 join() 될 때까지 기다리는 최대시간을 지정할 수도 있다.
- nowt.join(long milliSec, int nanoSec);
Thread의 상태
▷ 쓰레드 객체에 start()를 호출하면 쓰레드가 바로 실행되는 것이 아니라 "new thread" 상태에서 "alive" 상태로 간다.
▷ alive 상태 내부에서도 이 쓰레드가 현재 실행중인가 아닌가에 따라 다시 runnable 또는 not runnable 상태로 나뉘어지게 된다.
▷ 쓰레드가 종료되면 dead 상태로 간다. 즉 크게 보면 쓰레드는 아래와 같은 상태 변화를 격게 된다.
new thread --> alive --> dead
▷ alive 상태에 있는 동안은 (runnable 상태이든 not runnable 상태이든) isAlive() 메소드 호출에 대해 true를 리턴한다.
▷ alive 상태 내에서도 runnable 이 아닌 상태를 다음과 같이 여러 세부적인 중간상태와 ready 상태로 다시 나눌 수 있다.
▷ 이 중간상태와 ready 상태들은 모두 어떤 변화를 기다리는 상태이며 항상 ready를 거쳐서만 runnable 상태 즉, CPU의 서비스를 받는 상태로 갈 수 있다.
not runnable --> ready --> runnable
( waiting, asleep, blocked )
▷ 상태의 이동 원칙은 다음과 같다.
▶ ready 상태에서는 thread scheduler에 의해서 runnable 상태로만 갈 수 있고,
▶ runnable 상태에서는 ready 상태 또는 not runnable (즉, 모든 중간상태)로 갈 수 있다.
▶ 중간 상태에서는 runnable 상태로 직접 갈 수는 없고 반드시 ready 상태를 거쳐서만 갈 수 있다.
▷runnable 상태에서 ready 상태 또는 not runnable (즉, 모든 중간상태)로 가는 경우를 다음과 같이 나눌 수 있다.
1) thread scheduling에 의한 이동
2) 특정 메소드 호출에 의한 이동
1) thread scheduling에 의한 이동
▷ 먼저 쓰레드의 우선순위에 대하여 설명하겠다.
▷ 모든 쓰레드는 우선순위를 갖는데 스케줄러가 ready 상태에 있는 쓰레드들을 runnable 상태로 바꿀 때 즉, 실행시킬 때 이 우선순위를 참조한다.
▷ 높은 우선순위의 쓰레드를 먼저 서비스 하며 동일한 우선순위의 쓰레드들은 "랜덤"하게 선택한다. (즉, 오래 기다렸다고 먼저 runnable이 된다는 보장은 없다.)
▷ 우선순위는 1부터 10까지의 값을 가지는데 (큰값이 높은 우선순위임) 초기의 디폴트 우선순위 값은 5이다.
▷ 우선순위는 쓰레드를 처음 만들 때(즉 생성자에서) 임의의 값으로 지정할 수는 없으며 일단 쓰레드를 디폴트 우선순위로 생성한 후에 setPriority() 메소드를 이용하여 변경하여야 한다.
▷ 우선순위의 확인은 getPriority()로 한다.
▷ 쓰레드 우선순위를 하나 올리는 코드 부분은 다음과 같다.
int oldP = MyThread.getPriority();
int newP = Math.min(oldP+1, Thread.MAX_PRIORITY);
MyThread.setPriority(newP);
▷ 쓰레드 클래스가 제공하는 상수로 MAX_PRIORITY는 10, MIN_PRIORITY는 1, NORM_PRIORITY는 5의 값을 갖는다.
▷ 보통 사용자의 입력을 처리하는 쓰레드에 높은 우선순위를 주며 배치작업을 하는 쓰레드는 낮은 우선순위를 주는 것이 일반적이다.
-로그인 할 때 사용자의 아이디와 패스워드 입력창이 우선순위를 갖게 된다.
▷ 주의: 높은 우선순위를 갖는 쓰레드와 낮은 우선순위를 갖는 쓰레드가 공존하고 있을 때 낮은 우선순위를 가진 쓰레드가 계속 실행의 기회가 없는 것은 아니다. 단 처리시간이 전체적으로 적게 배정될 뿐이다. 쓰레드에 실제로 우선순위가 적용되는 구체적인 현상은 플랫폼마다 다를 수 있다.
2) 특정 메소드 호출에 의한 이동
▷ 다음과 같은 메소드를 사용하면 프로그램에서 쓰레드의 상태를 바꾸게 할 수 있다.
yield()
▷ 쓰레드를 스스로 ready 상태로 이동시키는 메소드이다.
▷ 어떤 쓰레드가 yield()를 호출한 때 다른 쓰레드들이 ready 상태에서 기다리고 있었으면 그 쓰레드가 먼저 실행될 수 있으며 기다리는 쓰레드가 없었으면 이 쓰레드가 바로 다시 runnable 상태로 갈 수 있다.
▷ yield()는 어떤 쓰레드가 CPU 시간을 독점할 우려가 있을 때 사용되는데 OS에 따라서 yield()를 반드시 필요로 하는 경우도 있고 yield()를 호출하지 않아도 OS가 자동으로 CPU 독점을 제한하는 경우도 있다. yield()는 static 메소드이므로 직접 호출할 수 있다.
- try {
- t.yield()
- } catch (InterrruptedException e) { }
스케쥴러의 동작 모드
▷ 자바에서는 스케줄러가 다음의 두가지중 하나로 구현된다.
▶ pre-emptive(선점형) 모드
▶ time-sliced 형
▷ pre-emptive(선점형) 모드에서는 어떤 쓰레드가 자신이 하던일을 마치거나 중간 대기상태로 되면 ready 상태에 있던(즉, 기다리던) 다른 쓰레드들 중 높은 우선순위의 것부터 runnable이 되는 방식이다.
▷ 어떤 경우에는 쓰레드가 실행도중에도 (즉, runnable상태에서) ready로 강제로 내려올 수도 있는데 이 경우는 다음과 같다.
- block 될 I/O를 요구한 경우
- 더 높은 우선순위의 쓰레드가 실행되려고 하는 경우
▷ time-sliced 형은 round-robin 방식이라고도 하는데 일정한 시간단위로 쓰레드를 돌아가면서 동작시키는 방식이다.
쓰레드 사용 예제
▷ 다음은 쓰레드를 두 개 만드는 예로 클래스 A의 생성자에서 쓰레드를 만든다. 각 쓰레드는 평균 500ms 간격으로 화면에 자신의 쓰레드이름을 출력한다.
- public class Test {
- public static void main(String[] args) {
- new A("FirstThread");
- new A("SecondThread");
- }
- }
- class A implements Runnable {
- Thread t;
- String threadName;
- public A(String s) {
- t = new Thread(this);
- this.threadName = s;
- t.start();
- }
- public void run() {
- while(true) {
- System.out.println(" I am " + threadName);
- try {
- t.sleep((int) (Math.random()*1000.0));
- } catch (InterruptedException e) { }
- }
- }
- }
결과)
I am FirstThread
I am FirstThread
I am FirstThread
I am FirstThread
I am SecondThread
I am SecondThread
I am SecondThread
I am SecondThread
I am FirstThread
. . .
동기화(Synchronized)
▷ 어떤 변수를 여러 쓰레드가 공동으로 엑세스하는 경우에 한 쓰레드가 이 변수를 사용하여 중요한 처리를 하는 도중에 다른 메소드에 의하여 이 변수의 값이 변하는 경우가 발생할 수 있다.
▷ 이와 같이 여러 쓰레드들이 어떤 데이터를 공유하는 경우에 발생하는 문제를 피하기 위하여 동기화와 기능을 제공한다.
▷ 멀티쓰레드 환경에서 여러 쓰레드간의 동기화를 제공하는 기법으로 다음과 같은 두가지가 있다.
1) 모니터의 사용
2) wait()와 notify()의 사용
1) 모니터의 사용
▷ 자바에서 모든 객체는 flag를 가지고 있는데 이것을 lock flag로 사용할 수 있다.
▷ 어느 코드블록을 (또는 메소드전체를) synchronized로 선언한 경우에 이 객체의 lock flag를 점유한 쓰레드만 이 동기화 부분을 실행하도록 하여 어느 순간에 한 쓰레드만 이 코드블록을 엑세스하게 한다.
▷ 모든 객체는 monitor를 가지고 있다.
▷ 어떤 쓰레드가 모니터를 잡는 때는 sychronized 로 선언된 메소드 또는 코드블록 부분을 실행하게 되는 것이다.
▷ 즉, 이 모니터의 소유권을 한 쓰레드만 잡을 수 있으며 이때 다른 쓰레드들이 이 객체의 sychronized 메소드들을 접근하는 것을 block 시키는 것이다.
▷ 다시말하면 모든 객체는 고유의 lock(즉 모니터)를 가지고 있으며 어느 한 순간에 이 lock을 엑세스하는 쓰레드는 하나뿐이다.
▷ 만일 이 lock을 다른 쓰레드가 잡고 있으면 이 쓰레드는 block 상태로 가고 그 lock이 사용가능해졌을 때 ready 상태로 가게 된다.
▷ 한편 lock을 잡고 있던 쓰레드는 synchronized 코드블록을 다 처리하고 나면 자동적으로 lock을 놓도록 되어있다.
▷ 메소드 또는 코드블록을 synchronized로 세트하는 방법은 다음과 같다. 먼저 메소드 전체를 synchronized로 세트하려면 메소드 선언문에서 다음과 같이 한다.
- public synchronized void myMethod() {
- // 보호할 코드
- }
▷ 메소드 전체가 아니라 메소드내의 코드블록을 synchronized로 세트하는 방법은 다음과 같다.
- synchronized(this) {
- // 보호할 코드
- }
▷ 이때는 synchronized()의 인자에 어떤 객체의 모니터를 잡을지를 명시해야 하는데 자신의 객체의 모니터를 잡으려면 this를 쓰고 다른 객체의 모니터를 잡으려면 그 객체의 참조를 쓴다.
- synchronized(otherObj) {
- // 보호할 코드
- }
2) wait(), notify()의 사용
▷ 앞에 설명한 동기화는 둘 이상의 쓰레드가 동시에 어떤 코드블록을 접근함으로써 발생하는 문제는 해결해 준다.
▷ 쓰레드간의 어떤 통신을 제공하지는 못한다. 예를들어 한 쓰레드가 어떤 동기화 부분의 사용을 완료한 경우 이 사실을 다를 쓰레드에게 알려주기 위하여 wait()와 notify()를 사용한다. 어떤 쓰레드가 wait()를 호출하면 waiting 상태로 가게 되는데 이 상태에서 ready로 나오려면 notify()나 notifyAll()을 받아야만 한다.
▷ 모든 객체는 두 개의 큐를 가지고 있는데 하나는 앞의 1)에서 설명한 synchronized 처리를 위한 lock 큐이고 하나는 쓰레드간 통신을 위한 waiting 큐이다. wait() 는 객체가 쓰레드의 실행을 중지하고 waiting 큐에 들어가서 어떤 상태변화(notify()의 발생)를 기다리게 하는 것이며, notify()는 이 객체를 사용하려고 기다리고 있는 쓰레드들 중 한 쓰레드에게 이 객체에 어떤 변화가 발생했음을 (즉, 모니터를 잡을 수 있음을) 알려 그 쓰레드가 waiting 큐에서 나와 계속 실행하도록 하는 것이다. 만일 모든 기다리는 쓰레드에게 이 사실을 동시에 알리려면 notifyAll()을 호출해야 한다.
▷ 여기서 주의할 것은 notify는 횟수를 축적하지 않는다는 것이다. 즉, notify는 한번 알리면 그만이고 과거의 notify 발생 횟수는 기록되지 않는다.
▷ 임의의 synchronized 코드를 가지고 있는 객체는 모두 monitor 객체가 될 수 있으며 wait()와 noify()를 사용할 수 있는 것이다.
▷ 쓰레드의 상태 변화의 관점에서 설명하면 임의의 모니터 객체는 wait() 호출로 waiting 상태로 가며 notify()의 호출로 ready 상태로 가는 것이다.
▷ 중요한 것은 wait()와 notify()는 모두 synchronized 코드부분 내에서만 호출할 수 있다는 것이다.
▷ 만일 synchronized 블록 외부에서 wait()를 호출하면 (즉, 모니터를 잡지 않고 호출하면) 자바는 IllegalMonitorStateException을 발생한다.
▷ 보통 wait()는 while 문 내에서 호출하게 되며 notify()는 블록내에서 아무곳에서나 해도 된다. wait()는 interrupt를 받으면 InterruptedException을 발생하므로 이를 처리해주어야 한다.
- try {
- wait();
- } catch(InterrruptedException e) { }
이상의 내용을 정리하면 다음과 같다.
wait()를 호출하는 순간 호출한 쓰레드에서 이루어지는 것은
- CPU의 사용을 중단 (waiting 상태로 간다.)
- 객체 lock을 놓는다.
- 객체의 waiting 큐로 들어간다.
notify()를 호출하는 순간 호출한 쓰레드에서 이루어지는 것은
- 어떤 쓰레드 하나가 waiting 큐에서 나와 ready 상태로 간다.
- notify()를 받은 쓰레드는 반드시 monitor lock을 얻어야만 계속 진행될 수 있다. (즉, notify 되었다고 바로 쓰레드가 실행되는 것은 아니다.