본문 바로가기

Software/JAVA

java Thread 관련

반응형

. 프로세스와 스레드의 생성 및 종료

 

. 프로세스 생성 및 종료

일반적으로 프로그램을 실행시키면, 하나의 프로세스로서 동작하게 됩니다. 다시 말해서, 우리가 실행시키는 하나의 프로그램은 하나의 프로세스로서 나타나게 됩니다. 자바에서의 프로세스는 자바 런타임 환경과 밀접한 관계를 갖고 있습니다. 왜냐하면, 자바 런타임 환경은 프로세스가 실행될 수 있는 기반 환경을 제공해 주기 때문입니다. 프로세스는 다른 프로세스를 생성할 수 있는데, 이 때 생성된 프로세스를 자식 프로세스라하고 기존에 있던 프로세스를 부모 프로세스라 합니다. 이러한 부모/자식 프로세스 개념은 하나의 자바 프로그램에서 다른 프로그램을 실행시키고자 할 때, 주로 사용됩니다. 다시 말해서, 플랫폼 독립적인 자바 프로그램이 플랫폼과 밀접한 관련이 있는 작업을 해야 할 경우, 해당 작업을 수행할 프로그램을 다른 언어로 해당 플랫폼에 맞도록 작성하고, 이 프로그램을 자바 프로그램에서 실행시켜 주는 것입니다.

이를 위해, 플랫폼 종속적인 시스템 함수들을 호출할 수 있도록 해 주는 Runtime 클래스와 실행하고자 하는 응용프로그램을 위한 프로세스를 관리할 수 있도록 해 주는 Process 클래스를 사용할 수 있습니다. 자바에서 프로세스를 생성하기 위하여 다음과 같이 해 줍니다.


            - “Runtime runtime = Runtime.getRuntime();”: 런타임 객체를 생성합니다.

            - “Process p = runtime.exec(“프로그램경로명”);”: exec 메소드를 이용하여 프로세스를 생성합니다.

위와 같이 프로세스를 생성할 수 있고, 프로세스의 작업을 마치거나 또는 프로세스를 강제고 종료하기 위해서는 다음 중 한 가지 방법으로 할 수 있습니다.

            - “p.waitFor();”: 자식 프로세스가 종료될 때까지 기다립니다.

            - “p.destroy();”: 부모 프로세스에서 자식 프로세스를 강제로 종료시킵니다.

            - “System.exit(0);”: 부모 프로세스만 종료되고 자식 프로세스는 계속 실행됩니다.

Runtime 클래스가 제공해 주는 주요 메소드를 살펴보면 다음과 같습니다.

            - public static Runtime getRuntime(): 현재 실행되고 있는 자바 애플리케이션과 관련된 런타임 객체를 리턴해 줍니다.

            - public void exit(int status): 현재 자바 가상머신을 종료합니다. status 매개변수는 종료시의 상태값을 나타내며, 일반적으로 0 이외의 값은 비정상적으로 종료되었음을 의미합니다.

            - public Process exec(String command) throws IOException: 주어진 명령어를 독립된 프로세스로 실행시켜 줍니다. exec(command, null)와 같이 실행시킨 것과 같습니다.

            - public Process exec(String command, String envp[]) throws IOException: 주어진 명령어를 주어진 환경을 갖는 독립된 프로세스로 실행시켜 줍니다. 이 메소드는 명령어 문자열을 토큰으로 나누어 이 토큰들을 포함하고 있는 cmdarray라는 새로운 배열을 생성합니다. 그리고 나서 exec(cmdarray, envp)을 호출합니다.

            - public Process exec(String cmdarray[]) throws IOException: 주어진 문자열 배열에 있는 명령어와 매개변수를 이용하여 독립된 프로세스로 실행시켜 줍니다. exec(cmdarray, null)을 호출합니다.

            - public Process exec(String cmdarray[], String envp[]) throws IOException: 주어진 문자열 배열에 있는 명령어와 매개변수를 이용하여 주어진 환경을 갖는 독립된 프로세스로 실행시켜 줍니다. 문자열 배열 cmdarray에는 명령어와 명령행 인자들을 나타내고 있습니다.

            - public native long freeMemory(): 시스템에 남아있는 메모리의 양을 얻습니다. 이 값은 항상 totalMemory() 메소드에 의해 얻어지는 값보다 작습니다.

            - public native long totalMemory(): 자바 가상머신의 최대 메모리 크기를 얻습니다.

Process 클래스가 제공해 주는 주요 메소드를 살펴보면 다음과 같습니다.

            - public abstract OutputStream getOutputStream(): 자식 프로세스의 출력 스트림을 얻습니다.

            - public abstract InputStream getInputStream(): 자식 프로세스의 입력 스트림을 얻습니다.

            - public abstract InputStream getErrorStream(): 자식 프로세스의 에러 스트림을 얻습니다.

            - public abstract int waitFor() throws InterruptedException: 자식 프로세스가 종료될 때까지 기다립니다.

            - public abstract int exitValue(): 자식 프로세스가 종료할 때의 상태값을 얻습니다.

            - public abstract void destroy(): 자식 프로세스를 강제로 종료시킵니다.

다음에 나오는 자바 프로그램은 위의 Runtime 클래스 및 Process 클래스를 이용하여 새로운 프로세스를 생성하고 종료하는 과정을 보여주기 위해 윈도우의 계산기를 실행시키는 간단한 예제입니다.



import java.io.*;
public class ProcessTest {
   static public void main(String args[]) {
      try {
         Process p1 = Runtime.getRuntime().exec("calc.exe");
         Process p2 = Runtime.getRuntime().exec("freecell.exe");
         Process p3 = Runtime.getRuntime().exec("Notepad.exe");
         p1.waitFor();
         p2.destroy();
         System.out.println("Exit value of p1: "+p1.exitValue());
         System.out.println("Exit value of p2: "+p2.exitValue());
      } catch(IOException e) {
         System.out.println(e.getMessage());
      } catch(InterruptedException e) {
         System.out.println(e.getMessage());
      }
      System.exit(0);
   }
}


/*
 * Results:
 D:\AIIT\JAVA\06>java ProcessTest
 Exit value of p1: 0
 Exit value of p2: 1
 D:\AIIT\JAVA\06>
*/





. 상호작용 명령어의 실행


그런데, 위와 같이 프로세스를 이용하여 방식으로 명령어를 실행하다 보면, 명령어에 따라 사용자에게 메시지를 출력하고 이에 대한 적절한 답을 사용자로부터 입력 받기를 원하는 명령어가 있습니다. 이러한 명령어를 상호작용(interactive) 명령어라고 합니다.



D:\AIIT\JAVA\06>ping 203.252.134.126

Pinging 203.252.134.126 with 32 bytes of data:

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

Reply from 203.252.134.126: bytes=32 time<10ms TTL=128

D:\AIIT\JAVA\06>

<그림 1. 상호작용 명령어의 실행>


그림에서와 같이 ping 명령어를 실행시키게 되면, ping 명령어는 그 실행 결과를 표준 출력을 이용하여 화면상에 출력해 줍니다. 이렇게 자바에서 실행시킨 프로세스가 출력하는 결과를 자바 프로그램은 알아야 하고, 또한 프로세스가 자바 프로그램으로부터 어떤 대답을 원할 경우가 있는데 이 때 사용자는 이에 대해 적절하게 답을 해 주어야 합니다. 이 때, ping 명령어는 메시지를 자신의 표준 출력에 장치에 출력하게 되는데, 이렇게 프로세스의 표준 출력을 자바 프로그램에서는 p.getInputStream 메소드를 이용하여 얻고, p.getOutputStream 메소드를 이용하여 프로세스의 표준 입력 장치에 쓰게 됩니다. 이 때, 한 가지 주의할 사항은 표준 출력 스트림에 대답을 쓴(write) , flush 또는 close 메소드를 이용하여 표준 출력 스트림을 비워(flush) 주어야 합니다.



<그림 2. 자바 프로그램과 프로세스 간의 데이터의 전달>


다음에 나오는 자바 프로그램은 도스 상에서 상호작용 명령어를 사용하는 간단한 예제를 보여줍니다.



import java.io.*;
import java.lang.*;
public class InteractiveProcessTest {
   public static void main(String[] args) {
      try {
         Process p = Runtime.getRuntime().exec("ping 203.252.134.126");
         byte[] msg = new byte[128];
         int len;
         while((len=p.getInputStream().read(msg)) > 0) {
            System.out.print(new String(msg, 0, len));
         }
         String rs = "\n";
         byte[] rb  = new byte[] { (byte)'\n' } ; //rs.getBytes();
         OutputStream os = p.getOutputStream();
         os.write(rb);
         os.close();
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

/*
 * Results:
 D:\AIIT\JAVA\06>java InteractiveProcessTest
 Pinging 203.252.134.126 with 32 bytes of data:
 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128
 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128
 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128
 Reply from 203.252.134.126: bytes=32 time<10ms TTL=128
 D:\AIIT\JAVA\06>
*/

<프로그램 2. InteractiveProcessTest.java>



. 스레드


. 스레드


하나의 프로그램을 프로세스라고 볼 때, 스레드는 하나의 프로그램 내에서의 실행 단위라고 할 수 있습니다. 자바에서는 각 작업(타스크)을 스레드로 표현하도록 하고, 이러한 스레드를 여러 개 둘 수 있도록 함으로써 멀티타스킹을 가능하게 해 줍니다. 다시 말해서, 자바에서는 멀티태스킹을 여러 개의 스레드를 동시에 수행하는 멀티스레딩을 이용하여 해결하고 있습니다. 따라서, 자바 가상머신은 하나의 애플리케이션이 동시에 수행되는 여러 개의 스레드를 가질 수 있도록 하고 있습니다. 물론, 일의 우선순위가 존재하듯이 모든 스레드는 그 우선순위를 가지게 됩니다.

자바 가상머신이 시작할 때는 데몬 스레드가 아닌 단 하나의 스레드가 존재하며, 이 스레드는 일반적으로 실행하려는 애플리케이션의 main() 메소드를 호출하도록 되어 있습니다. 자바 가상머신이 시작할 때부터 존재하는 이 스레드는 다음과 같은 경우가 발생할 때까지 계속적으로 수행됩니다.


        - Runtime 클래스의 exit() 메소드가 호출되고, 보안 관리자(security manager) exit 동작이 수행되도록 허락할 때

        - 스레드의 run() 메소드의 수행이 끝나고 리턴되거나 또는 stop() 메소드가 수행되는 등 데몬 스레드가 아닌 모든 스레드가 죽었을 때


자바에서는 이러한 스레드를 표현할 수 있도록 하기 위해 Thread 클래스를 상속하여 확장하는 방법, Runnable 인터페이스를 구현하는 방법 등 두 가지 방법을 제공해 주고 있습니다. 먼저, Thread 클래스를 상속하여 확장하는 방법에 대하여 살펴보고, 다음으로 Runnable 인터페이스를 구현하여 스레드로 실행시키는 방법에 대하여 살펴보도록 하겠습니다.


. Thread 클래스를 상속하는 스레드의 생성 및 시작


새로운 스레드를 생성하는 방법 중 Thread 클래스를 이용하는 방법에 대하여 살펴보면, 먼저 Thread 클래스를 상속하는 하위클래스를 선언하고, 이 하위클래스가 Thread 클래스의 run 메소드를 재정의하도록 하는 것입니다. 이렇게 Thread 클래스를 상속하여 run 메소드를 재정의한 하위클래스의 인스턴스를 생성하고 시작되도록 하면 됩니다. 이 때, run 메소드는 스레드가 수행할 작업을 나타내는 스레드의 몸체 역할을 합니다. 다시 말해서, 스레드를 시작시키면, run 메소드가 호출되고, run 메소드의 수행이 끝나고 리턴하게 되면 스레드는 종료하게 됩니다. 이러한 이유 때문에 run 메소드 내에는 주로 while 문을 사용하여 무한 루프로 나타냄으로써, 스레드가 반복작업을 수행하고 항상 살아 있을 수 있도록 하는 것입니다. 다음과 같이 Thread 클래스를 상속하고 run 메소드를 재정의 할 수 있습니다.



import java.lang.Thread;
class ThreadName extends Thread {
…
public void run() {
// Thread Body
// . . .
}
…
}

<그림 3. Thread 클래스의 상속 및 run 메소드의 재정의>



ThreadName t = new ThreadName();
t.start();

<그림 4. 스레드의 시작>


다음은 스레드를 생성하고, 시작시켜 주기 위해 Thread 클래스에서 기본적으로 제공해 주는 몇 가지 메소드들입니다.


            - public Thread(): 새로운 스레드 객체를 생성합니다. 이 객체생성자는 Thread(null, null, gname)와 같은 효과를 가지며

                                      스레드의 이름은 "Thread-"+n의 형태로 자동으로 생성됩니다. 이 때, 스레드의 몸체를 나타내는 run 메소드를 

                                      반드시 재정의해 주어야 합니다.

            - public Thread(Runnable target): 새로운 스레드를 생성하고, Thread(null, target, gname)와 같은 효과를 가지는 객체생성자입니다. 스레드의 이름은 "Thread-"+n의 형태로 자동으로 생성됩니다. 이 때, 스레드의 몸체인 run 메소드는 Runnable 인터페이스를 구현하는 target이 정의하고 있어야 합니다.

            - public Thread(ThreadGroup group, Runnable target): 새로운 스레드를 생성하고, Thread(group, target, gname)와 같은 효과를 가지는 객체생성자입니다. 스레드의 이름은 "Thread-"+n의 형태로 자동으로 생성됩니다. 이 때, 스레드의 몸체인 run 메소드는 Runnable 인터페이스를 구현하는 target이 정의하고 있어야 합니다.

            - public Thread(String name): 주어진 이름을 갖는 새로운 스레드를 생성하고, Thread(null, null, name)와 같은 효과를 갖는 객체생성자입니다.

            - public Thread(ThreadGroup group, String name): 주어진 이름을 갖는 새로운 스레드를 생성하고, Thread(group, null, name)와 같은 효과를 갖는 객체생성자입니다.

            - public Thread(Runnable target, String name): 주어진 이름을 갖는 새로운 스레드를 생성하고, Thread(null, target, name)와 같은 효과를 갖는 객체생성자입니다.

            - public Thread(ThreadGroup group, Runnable target, String name): 주어진 이름을 갖고 주어진 스레드 그룹에 속하는 새로운 스레드 객체를 생성합니다. 이 때, 스레드의 몸체인 run 메소드는 Runnable 인터페이스를 구현하는 target이 정의하고 있어야 합니다.

            - public native synchronized void start(): 스레드가 수행을 시작하도록 합니다. 이 때, 자바 가상머신은 이 스레드의 run 메소드를 호출하도록 되어 있습니다. 이렇게 함으로써 현재 수행중인 스레드와 새롭게 수행을 시작하도록 된 스레드 등 두 개의 스레드가 동시에 수행되게 됩니다.

            - public void run(): 만약, 이 스레드가 서로 분리된 Runnable 실행 객체로 생성되었다면, Runnable 객체의 run 메소드가 호출됩니다. 또는, Thread 클래스를 상속하여 생성된 스레드라면, Thread 클래스의 하위클래스가 갖고 있는 run 메소드가 호출됩니다. 그렇지 않을 경우, 이 메소드는 아무 것도 하지 않고 리턴됩니다.

            - public final String getName(): 스레드의 이름을 얻습니다.

            - public final void setName(String name): 스레드의 이름을 주어진 이름으로 설정합니다.

            - public String toString(): 스레드를 표현하기 위해 설정된 문자열을 얻습니다. 일반적으로, 이 문자열은 스레드의 이름, 우선순위, 스레드 그룹 등으로 이루어져 있습니다.

다음에 나오는 자바 프로그램은 Thread 클래스를 상속하여 스레드를 생성하고 실행시켜 주는 예를 보여주는 간단한 프로그램입니다.




import java.lang.*;
class PrimeThread extends Thread {
   int number;
   PrimeThread(int n) {
      super();
      number = n;
   }
   PrimeThread(String s, int n) {
      super(s);
      number = n;
   }
   public void run() {
      int n = 3;
      while(n < number) {
         if(isPrimeNumber(n))
            System.out.println(getName() + ": "
                             + n + " is prime number");
         n++;
         try {
            sleep(100);
         } catch(InterruptedException e) {
         }
      }
   }
   public boolean isPrimeNumber(int n) {
      int i;
      for(i=2;i<=(n/2);i++)
         if((n % i) == 0) return(false);
      return(true);
   }
}
class ThreadTest {
   public static void main(String args[] ) {
      Thread primeThread = new PrimeThread(30);
      System.out.println("PrimeThread: "+primeThread);
      primeThread.setName("PrimeThread");
      System.out.println("PrimeThread: "+primeThread);
      primeThread.start();
   }
}
/*
 * Results:
 D:\AIIT\JAVA\06>java ThreadTest
 PrimeThread: Thread[Thread-0,5,main]
 PrimeThread: Thread[PrimeThread,5,main]
 PrimeThread: 3 is prime number
 PrimeThread: 5 is prime number
 PrimeThread: 7 is prime number
 PrimeThread: 11 is prime number
 PrimeThread: 13 is prime number
 PrimeThread: 17 is prime number
 PrimeThread: 19 is prime number
 PrimeThread: 23 is prime number
 PrimeThread: 29 is prime number
 D:\AIIT\JAVA\06>
 */
 

<프로그램 3. ThreadTest.java>


. Runnable 인터페이스를 구현하는 스레드의 생성 및 시작


스레드를 생성하기 위한 다른 방법은 Runnable 인터페이스를 구현하는 클래스를 선언하는 것입니다. 다시 말해서, Runnable 인터페이스의 run 메소드를 구현하는 클래스를 선언하고, 그 클래스의 인스턴스를 생성한 후, 이 인스턴스를 Thread를 생성할 때 매개변수로 넘겨주는 것입니다. 다음과 같이 Runnable 인터페이스의 run 메소드를 구현하는 클래스를 선언할 수 있습니다.



import java.lang.Runnable;
class RunnableThreadName implements Runnable {
…
public void run() {
// Thread Body
// …
}
…
}

<그림 5. Runnable 인터페이스의 run 메소드를 구현>


다음과 같이 Runnable 인터페이스를 구현하는 클래스의 인스턴스를 선언하고 생성한 후, 스레드를 생성할 때 매개변수로 넘겨주어 시작시켜 주게 됩니다.




Thread t = new new Thread(new RunnableThreadName());
t.start();



<그림 6. Runnable 스레드의 시작>


Runnable 인터페이스는 클래스에 의해 구현되어져야 하고, 이 클래스의 인스턴스는 스레드에 의해 수행되도록 되어 있습니다. 자바에서는 내부적으로 Runnable 인터페이스를 Thread 클래스가 구현하고 있고, 따라서 Runnable 인터페이스는 Thread 클래스의 하위클래스를 만들지 않고서도 스레드로 수행될 클래스를 만들 수 있는 방법을 제공해 주고 있습니다. Thread 클래스의 인스턴스를 생성하면서 Runnable 인터페이스를 구현하는 클래스의 인스턴스를 매개변수로 전달해 줌으로써, Thread 클래스의 하위클래스를 생성하지 않고서도 스레드를 구현할 수 있습니다. 대부분의 경우, Runnable 인터페이스는 run 메소드만 구현하여 사용하고, Thread 클래스의 나머지 메소드는 사용할 필요가 없을 경우 주로 사용됩니다. 다음은 Runnable 인터페이스에서 선언하고 있는 run() 메소드입니다.


            - public abstract void run(): Runnable 인터페이스를 구현하는 스레드 객체가 반드시 구현해 주어야 하는 메소드입니다. 왜냐하면,

              Runnable 인터페이스를 구현하는 클래스 객체의 run 메소드가 호출되도록 함으로써 그 스레드가 분리된 스레드로 수행되도록 

              하기 때문입니다.


다음에 나오는 자바 프로그램은 Runnable 인터페이스를 구현하여 스레드를 생성하고 실행시켜 주는 예를 보여주는 간단한 프로그램입니다.




import java.lang.Thread;
import java.lang.Runnable;
class PrimeThread implements Runnable {
   int number;
   String name;
   PrimeThread(int n) {
      name = null;
      number = n;
   }
   PrimeThread(String s, int n) {
      name = s;
      number = n;
   }
   public void run() {
      int n = 3;
      while(n < number) {
         if(isPrimeNumber(n))
            System.out.println(name + ": " + n + " is prime number");
         n++;
         try {
            Thread.sleep(100);
         } catch(InterruptedException e) {
         }
      }
   }
   public boolean isPrimeNumber(int n) {
      int i;
      for(i=2;i<=(n/2);i++)
         if((n % i) == 0) return(false);
      return(true);
   }
}
class RunnableTest {
   public static void main(String args[] ) {
      Thread primeThread = new Thread(new PrimeThread(30));
      System.out.println("PrimeThread: "+primeThread);
      primeThread.setName("PrimeThread");
      System.out.println("PrimeThread: "+primeThread);
      primeThread.start();
   }
}
/*
 * Results:
 D:\AIIT\JAVA\06>java RunnableTest
 PrimeThread: Thread[Thread-0,5,main]
 PrimeThread: Thread[PrimeThread,5,main]
 null: 3 is prime number
 null: 5 is prime number
 null: 7 is prime number
 null: 11 is prime number
 null: 13 is prime number
 null: 17 is prime number
 null: 19 is prime number
 null: 23 is prime number
 null: 29 is prime number
 D:\AIIT\JAVA\06>
 */

<프로그램 4. RunnableTest.java>



. 스레드의 종료 및 대기

스레드 객체의 start 메소드를 호출하여 스레드를 시작하면, 스레드 객체의 run 메소드가 호출되고, 이 스레드는 run 메소드가 실행되는 동안 계속 살아있게 됩니다. 만약, 스레드의 run 메소드가 무한루프로 구성되어 종료되지 않고, 이 스레드를 시작시켜 준 다른 스레드가 이 스레드를 종료시켜야 할 필요성이 있습니다. 또는 복잡한 계산을 여러 개로 나누어 수행한 후, 각각의 결과값을 모두 참조하여 최종 계산 결과를 얻어 내야 할 경우, 하나의 스레드가 여러 개로 나누어진 계산을 수행할 스레드들을 수행시키고, 그 스레드들의 수행이 끝날 때까지 기다렸다가 다음 계산 작업을 수행해야 합니다.

다음은 이렇게 스레드를 강제로 종료시키거나, 종료될 때까지 기다리기 위해 Thread 클래스에서 기본적으로 제공해 주는 몇 가지 메소드들입니다.

            - public final void stop(): 수행을 강제로 정지하도록 합니다.

            - public final synchronized void stop(Throwable o): 수행을 강제로 정지하도록 합니다.

            - public final void join() throws InterruptedException: 스레드가 종료되기를 기다립니다.

            - public final synchronized void join(long millis) throws InterruptedException: millis에 주어진 밀리초 시간 동안 스레드가 종료되기를 기다립니다. 만약, 주어진 시간이 0이라면, 영원히 기다리라는 의미입니다.

            - public final synchronized void join(long millis, int nanos) throws InterruptedException: millis에 주어진 밀리초와 nanos에 주어진 나노초를 더한 시간 동안 스레드가 종료되기를 기다립니다.

stop join 메소드는 다음과 같이 사용될 수 있습니다.



ThreadName t = new ThreadName();
t.start();
…
t.stop();
// 또는 t.join([milis [,nanos]]);

<그림 7. Runnable 스레드의 시작>


다음은 두 개의 스레드를 이용하여 1부터 10000까지, 10001부터 20000까지 각각의 합을 구하고, 이 두 결과를 합하여 1부터 20000까지 합한 값을 구하는 작업에 대해 Thread 클래스를 이용하여 작성한 것입니다.




import java.lang.*;
//class SumThread extends Thread {  // Thread Class
class SumThread implements Runnable {  // Runnable Interface
   static int count=0;                 // Runnable Interface
   int id;                             // Runnable Interface
   int start, end, sum;
   SumThread(int start, int end) {
      this.start = start;
      this.end   = end;
      this.id    = count++;            // Runnable Interface
   }
   public String getName() {           // Runnable Interface
      return("Thread-"+id);            // Runnable Interface
   }                                   // Runnable Interface
   public int getSum() {
      return(sum);
   }
   public void run() {
      sum = 0;
      for(int i=start;i<=end;i++) {
         sum += i;
         System.out.println(getName() + ": " + sum);
      }
   }
}
class ThreadJoinTest {
   public static void main(String args[] ) {
//      SumThread t1 = new SumThread(1, 10000);       // Thread Class
//      SumThread t2 = new SumThread(10001, 20000);   // Thread Class
      SumThread rt1 = new SumThread(1, 10000);       // Runnable Interface
      SumThread rt2 = new SumThread(10001, 20000);   // Runnable Interface
      Thread t1 = new Thread(rt1);                    // Runnable Interface
      Thread t2 = new Thread(rt2);                    // Runnable Interface
      t1.start();
      t2.start();
      try {
         t1.join();
         t2.join();
      } catch(InterruptedException e) {
      }
//      System.out.println("Sum(10000,20000): " + (t1.getSum()+ t2.getSum()));    // Thread Class
      System.out.println("Sum(10000,20000): " + (rt1.getSum()+ rt2.getSum()));  // Runnable Interface
   }
}
/*
 * Results:
D:\AIIT\JAVA\06>java ThreadJoinTest
 ...
 Thread-0: 8778
 Thread-1: 40010
 Thread-0: 8911
 Thread-1: 50015
 ...
 Thread-1: 149985000
 Thread-1: 150005000
 Sum(10000,20000): 200010000
 D:\AIIT\JAVA\06>
 */

<프로그램 5. ThreadJoinTest.java>


위의 예제에서는 스레드를 생성하는 두 가지 방법을 각각 사용할 수 있도록 예를 보여 주고 있습니다. 각 주석 처리된 부분을 해제/설정 하는 식으로 Thread 클래스를 상속하여 스레드를 생성하거나, Runnable 인터페이스를 구현하여 스레드를 생성할 수 있습니다.

다음에 나오는 자바 프로그램은 일정 시간만큼 기다린 후 생성한 스레드를 멈추게 하는 예를 보여주는 프로그램입니다.


import java.lang.*;
class NeverStopThread extends Thread {
   int i=0;
   public void run() {
      while(true) {
         try {
            sleep(100);
            System.out.println(getName() + ": " + i++);
         } catch(InterruptedException e) {
         }
      }
   }
}
class ThreadStopTest {
   public static void main(String args[] ) {
      Thread t = new NeverStopThread();
      t.start();
      try {
         Thread.sleep(1000);
//         t.join(1000);       // 기다리는 시간은 같음
//         t.join(1000, 0);    // 기다리는 시간은 같음
      } catch(InterruptedException e) {
      }
      t.stop();
   }
}
/*
 * Results:
 D:\AIIT\JAVA\06>java ThreadStopTest
 Thread-0: 0
 Thread-0: 1
 Thread-0: 2
 Thread-0: 3
 Thread-0: 4
 Thread-0: 5
 Thread-0: 6
 Thread-0: 7
 Thread-0: 8
 D:\AIIT\JAVA\06>
 */

<프로그램 6. ThreadStopTest.java>


. 스레드의 상태

스레드가 갖는 상태를 살펴보면, 먼저 스레드가 생성되고, 스레드가 실행되고, 스레드가 실행을 멈추고 있다가 다시 실행되기를 기다리고, 마지막으로 스레드의 실행이 완전히 끝나 스레드가 죽는 등의 상태가 있습니다. 이렇게 스레드의 상태를 변화시킬 수 있도록 Thread 클래스에서는 여러 가지 메소드를 제공해 주고 있고, 이러한 메소드와 상태간의 관계도를 그려보면, 다음과 같은 스레드 상태전이도가 나타납니다.



<그림 8. 스레드의 상태전이도>


스레드각 가질 수 있는 각 상태를 살펴보면 다음과 같습니다.

            - New Thread: new 문장을 이용하여 스레드를 생성하고, 실행하기 바로 직전의 상태를 나타냅니다. 이 상태에서 스레드의 start 메소드가 호출되면 Runnable 상태가 되고, 스레드의 stop 메소드가 호출되면 바로 Dead 상태가 됩니다.

            - Runnable: 새로운 스레드가 생성되어 그 스레드의 start 메소드를 호출하면, 스레드는 실행 가능한 Runnable 상태가 되는데, 실제로 Runnable 상태는 두 가지로 나뉘게 됩니다. 먼저, CPU를 실제로 할당받아 실행되는 상태와, 실행큐에서 CPU를 할당 받아 실행되기 위해 대기하는 상태입니다. 이러한 상태전환은 자바 런타임 스케줄러에 의해 크게 좌우되며, CPU를 사용하던 스레드가 다른 스레드가 실행될 수 있도록 CPU를 양보하기 위해서는 yield 메소드를 사용할 수 있습니다.

            - Not Runnable: 현재, 실행중인 스레드의 suspend, wait, sleep 메소드를 호출하거나, 또는 스레드가 I/O 작업을 수행하게 되면, 스레드는 Runnable 상태에서 Not Runnable 상태로 전환하게 됩니다. 다시 말해서, CPU를 전혀 사용하지 않으므로 실행큐에서 대기하지도 않고, 따라서, 전혀 스케줄되지 않겠지요. 이렇게 네 가지 경우에 의해 스레드는 Runnable 상태에서 Not Runnable 상태로 전환하게 되는데, 반대로 Not Runnable 상태에서 Runnable 상태로 전환해 주기 위해서는 suspend 메소드가 호출되었을 경우에는 resume메소드를 호출하고, wait 메소드가 호출된 경우에는 notify 메소드를 호출하고, sleep 메소드가 호출된 경우에는 주어진 시간이 경과하면, I/O 작업을 수행했을 경우에는 해당 I/O 작업을 마치면 됩니다.

            - Dead: 스레드는 하나의 실행단위입니다. 따라서, 스레드가 할 일을 모두 마치면 스레드는 Dead 상태가 됩니다. 다시 말해서, 스레드의 run 메소드가 끝나거나 리턴된 경우, 스레드의 stop 메소드가 호출된 경우에 스레드는Dead 상태가 됩니다. 이 때, 스레드가 살아있는지 죽었는지를 확인하기 위해서는 isAlive 메소드를 이용할 수 있습니다.

스레드의 상태와 관련하여 Thread 클래스에서 제공해 주고 있는 메소드를 살펴보면, 다음과 같습니다.

            - static Thread currentThread(): 현재 수행중인 스레드를 얻습니다.

            - public native synchronized void start(): 스레드가 수행을 시작하도록 합니다. 이 때, 자바 가상머신은 이 스레드의 run 메소드를 호출하도록 되어 있습니다. 이렇게 함으로써 현재 수행중인 스레드와 새롭게 수행을 시작하도록 된 스레드 등 두 개의 스레드가 동시에 수행되게 됩니다.

            - public final void suspend(): 스레드를 잠시 쉬게(suspend) 합니다.

            - public final void resume(): 잠시 쉬고 있는(suspended) 스레드를 다시 시작(resume)하도록 합니다.

            - public final void stop(): 수행을 강제로 정지하도록 합니다.

            - public final synchronized void stop(Throwable o): 수행을 강제로 정지하도록 합니다.

            - void destroy(): 스레드를 파괴합니다.

            - public static native void yield(): 현재 실행중인 스레드 객체가 잠시 멈추고 다른 스레드가 수행되도록 프로세서(CPU)를 양보합니다.

            - public static native void sleep(long millis) throws InterruptedException: 현재 실행중인 스레드가 주어진 시간(millis)만큼 sleep하도록 합니다. 스레드는 모니터에 대한 소유를 잃지 않고, 주어진 시간이 경과하면 다시 깨어나서 실행됩니다.

            - public static void sleep(long millis, int nanos) throws InterruptedException: 현재 실행중인 스레드가 주어진 시간(millis+nanos)만큼 sleep하도록 합니다. 스레드는 모니터에 대한 소유를 잃지 않고, 주어진 시간이 경과하면 다시 깨어나서 실행됩니다. millis millisecond 단위이고, nanos 0-999999 사이에 있는 nanosecond 단위의 추가적인 시간입니다.

            - public final native boolean isAlive(): 스레드가 살아 있는 지 검사하는데, 스레드가 실행된 후 아직 죽지 않았다면, 이 스레드는 살아있는 스레드 입니다.


. 스레드의 우선순위와 자바 스케줄링


일반적으로 우리가 일을 수행할 때, 일들 사이에 운선순위가 존재하듯이, 자바 프로그램에서 수행하는 작업에도 우선순위를 부여할 수 있고, 자바에서 이러한 작업들은 스레드로 표현할 수 있으므로 스레드 역시 우선순위를 갖게 됩니다. 또한, 이러한 우선순위는 자바 프로그램 개발자가 스레드의 실행을 위한 스케줄링을 할 수 있는 방법을 제공해 주는 것입니다.

스레드의 우선순위를 제어하기 위해 Thread 클래스에서 제공해주는 파이널 변수와 메소드는 다음과 같습니다.

            - public static final int MIN_PRIORITY(=1): 스레드가 가질 수 있는 가장 낮은 우선순위 값입니다.

            - public static final int NORM_PRIORITY(=5): 스레드가 가질 수 있는 디폴트 우선순위 값입니다.

            - public static final int MAX_PRIORITY(=10): 스레드가 가질 수 있는 가장 높은 우선순위 값입니다.

            - static Thread currentThread(): 현재 수행중인 스레드를 얻습니다.

            - public final int getPriority(): 스레드의 우선순위를 얻습니다.

            - public final void setPriority(int newPriority): 스레드의 우선순위를 주어진 값으로 설정합니다.

자바 런타임 스케줄러는 현재 실행 가능한 스레드들 중에서 우선순위가 가장 높은 스레드를 실행시켜 주는데, 현재 실행중인 스레드보다 더 높은 우선순위를 갖는 스레드가 실행가능 상태가 되면 자바 런타임 스케줄러는 우선순위가 더 높은 스레드를 실행시켜 줍니다. 이러한 스케줄링 방식을 선점형 스케줄링이라 합니다. 또한, 같은 우선순위를 갖는 여러 개의 스레드가 동시에 실행되기를 원할 때는 주어진 타임 슬라이스 만큼 스레드를 순서대로 돌아가면서 실행시켜 주는데, 이러한 방식을 타임 슬라이싱(time-slicing)이라 하고, 이 때 순서대로 돌아가면서 실행시켜 주므로 라운드-로빈(round-robin)이라 합니다. 마지막으로, 스레드가 스레드를 생성할 경우 새로 생성되는 스레드의 우선순위는 자신을 생성시킨 스레드의 우선순위를 그대로 상속하도록 되어 있습니다.

다음에 나오는 자바 프로그램은 스레드들이 갖는 우선순위 값에 대한 예를 보여주는 프로그램입니다.


import java.lang.*;
class ThreadWithPriority extends Thread {
   ThreadWithPriority() {
   }
   ThreadWithPriority(int priority) {
      setPriority(priority);
   }
   public void run() {
      int i=0;
      while(i++ < 500) {
         System.out.println(this/*this.toString()*/);
      }
   }
}
class ThreadPriorityTest {
   public static void main(String args[] ) {
      Thread.currentThread().setPriority(Thread.NORM_PRIORITY+3);
      Thread t1 = new ThreadWithPriority();
      Thread t2 = new ThreadWithPriority(6);
      Thread t3 = new ThreadWithPriority(4);
      System.out.println(Thread.currentThread());
      t1.start();
      t2.start();
      t3.start();
   }
}
/*
 * Results:
 D:\AIIT\JAVA\06>java ThreadPriorityTest > ThreadPriorityTest.txt
 D:\AIIT\JAVA\06>type ThreadPriorityTest.txt
 Thread[main,8,main]
 Thread[Thread-0,8,main]
 ...
 Thread[Thread-1,6,main]
 ...
 Thread[Thread-2,4,main]
 ...
 D:\AIIT\JAVA\06>
 */

<프로그램 7. ThreadPriorityTest.java>


위의 자바 프로그램에서는 총 네 개의 스레드가 존재합니다. 다시 말해서, 현재 main 메소드도 하나의 스레드로 실행이 된다는 것입니다. 그리고, 스레드 t1은 현재 스레드(main을 포함하는)가 갖는 우선순위를 그대로 상속한다는 것을 알 수 있고, 스레드 t2와 스레드 t3는 각각 6, 4의 우선순위 값을 갖습니다. 그러므로, 우선순위가 높은 t2 스레드가 가장 많이 실행되고, t1 스레드가 실행되다가도 t2스레드에 의해 CPU를 바로 빼앗기게 됩니다. 스레드 t1, t2, t3는 자신의 타임 슬라이스 만큼 라운드-로빈 방식에 의해 순서대로 실행됩니다.


. 스레드 그룹(ThreadGroup)


스레드 그룹은 스레드 또는 스레드 그룹을 멤버로 포함할 수 있으며, 모든 스레드는 스레드 그룹에 속하게 됩니다. 이 때, 자바 애플리케이션에 있는 최상위 스레드 그룹은 시스템 스레드 그룹입니다. 자바 애플리케이션이 시작되면 자바 런타임 시스템은 시스템 스레드 그룹의 멤버로서 main 스레드 그룹 인트턴스를 만들고, main 스레드 그룹은 main 스레드를 생성하여 멤버로 포함하게 됩니다. 그리고, main 스레드는 main 메소드를 실행하도록 되어 있습니다. 기본적으로 사용자가 작성한 새로운 스레드와 스레드 그룹은 이 main 스레드 그룹의 멤버가 됩니다. 그런데, 사용자가 새로운 스레드 그룹을 만들고 스레드들을 관리할 수 있는데, 새로운 스레드 그룹을 만들기 위해서는 다음과 같이 해 주면 됩니다.


ThreadGroup tg = new ThreadGroup(“NewThreadGroup”);
Thread t = new Thread(tg, “NewThread”);

<그림 9. 스레드에 대한 스레드 그룹의 설정>


이렇게 스레드 및 스레드 그룹을 관리하기 위해, Thread 클래스에서 제공해 주고 있는 메소드를 살펴보면, 다음과 같습니다.

            - public Thread(ThreadGroup group, String name): 주어진 이름을 갖는 새로운 스레드를 생성하고, Thread(group, null, name)와 같은 효과를 갖는 객체생성자입니다.

            - public Thread(ThreadGroup group, Runnable target): 새로운 스레드를 생성하고, Thread(group, target, gname)와 같은 효과를 가지는 객체생성자입니다. 스레드의 이름은 "Thread-"+n의 형태로 자동으로 생성됩니다. 이 때, 스레드의 몸체인 run 메소드는 Runnable 인터페이스를 구현하는 target이 정의하고 있어야 합니다.

            - public Thread(ThreadGroup group, Runnable target, String name): 주어진 이름을 갖고 주어진 스레드 그룹에 속하는 새로운 스레드 객체를 생성합니다. 이 때, 스레드의 몸체인 run 메소드는 Runnable 인터페이스를 구현하는 target이 정의하고 있어야 합니다.

            - public final ThreadGroup getThreadGroup(): 스레드가 속한 스레드 그룹을 리턴해 줍니다.

위에서 살펴본 바와 같이, 스레드에 대한 스레드 그룹의 설정은 스레드 생성시에만 가능합니다. 다음으로, 스레드 및 스레드 그룹을 관리하기 위해 ThreadGroup 클래스에서 제공해 주고 있는 메소드를 살펴보면, 다음과 같습니다.

            - public ThreadGroup(String name): 주어진 이름의 새로운 스레드 그룹을 생성합니다. 이 스레드 그룹의 상위 스레드 그룹을 이 스레드 그룹을 생성하는 스레드가 속한 스레드 그룹이 됩니다.

            - public ThreadGroup(ThreadGroup parent, String name): 주어진 스레드 그룹을 상위 스레드 그룹으로 하는 주어진 이름의 스레드 그룹을 생성합니다.

            - public final String getName(): 이 스레드 그룹의 이름을 얻습니다.

            - public final ThreadGroup getParent(): 이 스레드 그룹의 상위 스레드 그룹을 얻습니다.

            - public final int getMaxPriority(): 이 스레드 그룹의 최대 우선순위 값을 얻습니다. 이 스레드 그룹에 속한 스레드들은 이 최대 우선순위 값보다 큰 우선순위 값을 가질 수 없습니다.

            - public final void setMaxPriority(int pri): 이 그룹의 최대 우선순위 값을 설정합니다. 이 스레드 그룹에 속한 스레드들은 이 최대 우선순위 값보다 큰 우선순위 값을 가질 수 없습니다.

            - public int activeCount(): 이 스레드 그룹에 속한 active 스레드의 개수를 구합니다.

            - public int activeGroupCount():이 스레드 그룹에 속한 active 스레드 그룹의 개수를 구합니다.

            - public final void stop(): 이 스레드 그룹에 속한 모든 프로세스들을 완전히 멈추게 합니다.

            - public final void suspend(): 이 스레드 그룹에 속한 모든 프로세스들을 잠깐 멈추게 합니다.

            - public final void resume(): 이 스레드 그룹에 속한 모든 프로세스들을 다시 시작하게 합니다.

            - public final void destroy(): 이 스레드 그룹과 하위 스레드 그룹들을 모두 없앱니다.

            - public synchronized boolean isDestroyed(): 스레드 그룹이 없어질(destroy) 수 있는 가를 검사합니다.

            - public String toString(): 이 스레드 그룹을 표현하고 있는 문자열을 얻습니다.

다음에 나오는 자바 프로그램은 스레드 그룹에 대한 간단한 예를 보여주는 프로그램입니다.


import java.lang.*;
class ThreadGroupTest {
   public static void main(String args[] ) {
      ThreadGroup tg1 = Thread.currentThread().getThreadGroup();
      ThreadGroup tg2 = new ThreadGroup(tg1, "ThreadGroup2");
      ThreadGroup tg3 = new ThreadGroup("ThreadGroup3");
      Thread t1 = new Thread();
      Thread t2 = new Thread(tg1, "Thread-tg1");
      Thread t3 = new Thread(tg2, "Thread-tg2");
      Thread t4 = new Thread(tg3, "Thread-tg3");
      System.out.println("this: "+Thread.currentThread());
      System.out.println("  t1: "+t1);
      System.out.println("  t2: "+t2);
      System.out.println("  t3: "+t3);
      System.out.println("  t4: "+t4);
      System.out.println("this: "+Thread.currentThread().getThreadGroup()+", "
        +Thread.currentThread().getThreadGroup().activeCount()+", "
        +Thread.currentThread().getThreadGroup().activeGroupCount());
  System.out.println(" t1: "+t1.getThreadGroup());
  System.out.println(" t2: "+t2.getThreadGroup());
  System.out.println(" t3: "+t3.getThreadGroup());
      System.out.println("t4:"+t4.getThreadGroup()+","+t4.getThreadGroup().getName());
      Thread.currentThread().getThreadGroup().list();
   }
}
/*
 * Results:
 D:\AIIT\JAVA\06>java ThreadGroupTest
 this: Thread[main,5,main]
   t1: Thread[Thread-0,5,main]
   t2: Thread[Thread-tg1,5,main]
   t3: Thread[Thread-tg2,5,ThreadGroup2]
   t4: Thread[Thread-tg3,5,ThreadGroup3]
 this: java.lang.ThreadGroup[name=main,maxpri=10], 7, 2
   t1: java.lang.ThreadGroup[name=main,maxpri=10]
   t2: java.lang.ThreadGroup[name=main,maxpri=10]
   t3: java.lang.ThreadGroup[name=ThreadGroup2,maxpri=10]
   t4: java.lang.ThreadGroup[name=ThreadGroup3,maxpri=10], ThreadGroup3
 java.lang.ThreadGroup[name=main,maxpri=10]
     Thread[main,5,main]
     Thread[SymcJIT-LazyCompilation-0,1,main]
     Thread[SymcJIT-LazyCompilation-PA,10,main]
     Thread[Thread-0,5,main]
     Thread[Thread-tg1,5,main]
     java.lang.ThreadGroup[name=ThreadGroup2,maxpri=10]
         Thread[Thread-tg2,5,ThreadGroup2]
     java.lang.ThreadGroup[name=ThreadGroup3,maxpri=10]
         Thread[Thread-tg3,5,ThreadGroup3]
 D:\AIIT\JAVA\06>
 */

<프로그램 8. ThreadGroupTest.java>


위의 자바 프로그램에서는 마지막 라인의 “Thread.currentThread().getThreadGroup().list();” 문을 이용하여 현재 main 스레드 그룹이 가지고 있는 스레드와 스레드 그룹에 대한 정보를 보여주고 있습니다. 이 때, main 스레드 그룹이 총 7개의 스레드(activeCount)와 두 개의 스레드 그룹을 포함하고 있다는 것을 알 수 있습니다.


. 데몬 스레드와 데몬 스레드 그룹


일반적으로, 스레드는 자신의 작업을 수행하도록 되어 있는 반면, 데몬 스레드란 다른 스레드로부터 요청을 받아 특정 서비스를 수행하는 작업을 합니다. 따라서, 데몬 스레드 자신이 맡고 있는 서비스에 대한 요청이 언제 발생하더라고 모두 수행해 주어야 합니다. 이를 위해, 데몬 스레드의 몸체는 보통 무한루프를 돌도록 되어 있고, 시스템이 살아있는 동안 계속 그 시스템과 생명주기를 같이 하도록 되어 있습니다. 그리고, 자바에서는 다른 일반 스레드가 모두 종료되고 데몬 스레드만 남아 있다면, 해당 프로그램을 자동으로 종료하도록 하고 있습니다. 왜냐하면, 데몬 스레드는 다른 스레드를 돕는 역할을 하는데 다른 스레드가 하나도 남아있지 않다면, 데몬 스레드의 존재 가치가 더 이상 필요 없기 때문입니다. 마지막으로, 데몬 스레드가 생성한 스레드는 디폴트로 데몬 스레드가 됩니다. 이러한, 데몬 스레드를 위해 Thread 클래스에서 제공해 주고 있는 메소드를 살펴보면, 다음과 같습니다.

            - public final void setDaemon(boolean on): 이 스레드를 데몬 스레드 또는 사용자 스레드로 설정합니다. 현재, 수행되고 있는 모든 스레드가 데몬 스레드이면 자바 가상머신은 수행을 마치게 되며, 메소드는 반드시 스레드가 시작(start)하기 전에 호출되어야 합니다.

            - public final boolean isDaemon(): 스레드가 데몬 스레드인지 얻습니다.

데몬 스레드 그룹은 자신이 포함하고 있는 멤버가 다 종료되면 자동으로 종료됩니다. 데몬 스레드의 경우와 마찬가지로, 데몬 스레드 그룹의 자식 스레드 그룹은 디폴트로 데몬 스레드 그룹이 됩니다. ThreadGroup 클래스에서 데몬 스레드 그룹을 위해 제공해주는 메소드를 살펴보면, 다음과 같습니다.

            - public final void setDaemon(boolean daemon): 이 스레드 그룹을 데몬 스레드 그룹으로 설정합니다.

            - public final boolean isDaemon(): 이 스레드 그룹이 데몬 스레드 그룹인지를 얻습니다.

다음에 나오는 자바 프로그램은 데몬 스레드와 데몬 스레드 그룹에 대한 간단한 예를 보여주는 프로그램입니다.


import java.lang.*;
class DaemonThread extends Thread {
   public void run() {
      while(true) {
         System.out.println(getName() + ": Polling...");
         try {
            sleep(100);
         } catch(InterruptedException e) {
         }
      }
   }
}
class DaemonThreadTest {
   public static void main(String args[] ) {
      Thread t1 = new DaemonThread();
      Thread t2 = new DaemonThread();
      t1.setDaemon(true);   // (a)
      t2.setDaemon(true);   // (a)
      t1.start();
      t2.start();
//      t1.setDaemon(true);   // (b)
//      t2.setDaemon(true);   // (b)
      try {
         Thread.sleep(1000);
      } catch(InterruptedException e) {
      }
   }
}
/*
 * Results:
 (a) 부분을 주석처리한 후, 실행시킨 경우
 D:\AIIT\JAVA\06>java DaemonThreadTest
 Thread-0: Polling...
 Thread-1: Polling...
 ...
 ...^C
 무한루프
 D:\AIIT\JAVA\06>
 (b) 부분을 주석처리한 후, 실행시킨 경우
 D:\AIIT\JAVA\06>java DaemonThreadTest
 Thread-0: Polling...
 Thread-1: Polling...
 ...
 D:\AIIT\JAVA\06>
 */

<프로그램 9. DaemonThreadTest.java>


setDaemon 메소드는 스레드가 시작하기 전에 호출되어야 데몬 스레드 또는 데몬 스레드 그룹으로 설정할 수 있습니다. 위의 (a) 부분만 주석처리하고 실행시키거나, (b) 부분만 주석처리하고 실행시켜 보면, 그 차이를 알 수 있습니다.


. 멀티스레딩(Multi-threading)


. 멀티스레드 프로그래밍


자바에서는 여러 개의 스레드를 작성하여 사용하는 멀티스레드 프로그래밍을 제공해 주고 있습니다. 이러한 여러 개의 스레드는 하나의 자원을 공유하기도 합니다. 예를 들어, 하나의 파일을 공유하면서, 한 스레드는 그 파일에 자신이 생성한 데이터를 쓰고, 이와 동시에 다른 스레드는 거기에 있는 데이터를 읽는다고 가정합니다. 한 스레드가 쓰기도 전에 다른 스레드가 읽는다거나, 한 스레드가 다음 데이터를 쓰기 전에 다른 스레드가 읽는다면 이는 원하는 데이터가 아닙니다. 우리는 한 스레드가 방금 쓴 그 데이터를 읽길 원하는 것이고, 또한 항상 새로운 데이터를 읽기를 원하는 것입니다. 또한, 이렇게 여러 개의 스레드가 하나의 자원을 공유하는 멀티스레드 프로그램을 작성할 경우 다음과 같은 경우를 고려해 주어야 합니다.

            - 공정(fairness): 여러 개의 스레드가 하나의 컴퓨팅 자원을 사용하기 위해 동시에 접근하는 프로그램을 작성할 경우, 모든 스레드가 공정하게 그 자원을 사용할 수 있도록 해 주어야 합니다. 이러한 시스템을 공정(fair)하다고 말할 수 있고, 그렇지 못할 경우 기아(starvation) 또는 교착상태(deadlock)를 야기할 수 있습니다.

            - 기아(starvation): 기아란 하나 또는 그 이상의 스레드가 원하는 자원을 얻기 위해 블록되는데, 그 자원을 얻을 수 없으므로 다른 작업을 못하는 상태를 말합니다. 다시 말해서, 하나의 시스템 자원을 얻지 못하고 계속 블록되어 있는 상태를 말하는 것입니다.

            - 교착상태(deadlock): 교착상태란 기아의 근본적인 문제인데, 이는 두 개 이상의 스레드가 만족하지 못하는 상태로 계속 기다릴 때 발생할 수 있습니다. 다시 말해서, 교착상태는 두 개 이상의 스레드가 서로에게 어떤 일을 해 주기를 기다리는 상태를 말하는데, 서로가 서로에게 어떤 일을 하기를 기다리기 때문에 한 스레드가 먼저 포기하지 않는 이상 영원히 기다릴 수 밖에 없는 것입니다.

이런 식으로 여러 개의 스레드가 하나의 자원 또는 데이터를 공유할 때, 다음과 같은 문제가 발생할 수 있습니다. 다시 말해서, 동기화를 하지 않을 경우, 그림에서와 같이 한 스레드가 값을 읽어간 후 변경 하고 있는 도중에 다른 스레드가 기존의 값을 읽어갑니다. 그리고 먼저 읽어간 스레드가 값을 변경하고 그 값을 저장했는데, 마찬가지로 두 번째 읽어간 스레드도 나름대로 값을 변경한 후 그 값을 다시 저장합니다. 이렇게 하면, 첫 번째 스레드가 변경한 작업은 결과적으로 무시됩니다.



<그림 10. 두 스레드간에 동기화가 이루어지지 않은 경우>


따라서, 위와 같은 경우 원하는 결과를 얻기 위해서는 다음과 같이 두 개의 스레드간에 서로 동기화가 필요합니다.



<그림 11. 두 스레드간에 동기화가 이루어진 경우>


. 생성자/요청자 문제(Generator/Requester Problem)


전산학 공부를 전공했던 사용자들이라면, 많이 들어봤던 문제 중의 하나가 생성자/요청자 문제일 것입니다. 운영체제 공부한다고 할 때, 많이 나오던 문제 중의 하나입니다. 생성자/요청자 문제에서 생성자는 항상 어떤 것을 생성하고, 요청자는 생성자가 생성한 것을 요청하여 가져다 사용하는 형태를 취하지요. 다른 말로 생산자(producer)/소비자(consumer) 문제라 할 수도 있습니다. 이번에 살펴 볼 생성자/요청자 문제에서는 위의 그림에서와 같이 생성자와 소비자가 정수값을 공유하도록 합니다. 이 때, 생성자는 정수값을 생성하여 공유 저장 공간에 저장합니다. 또한, 동기화 문제를 테스트 하기 위해, 생성자는 무작위로 시간 동안 sleep하게 되고 이러한 과정을 계속적으로 반복합니다. 반대로, 요청자는 생성자에 의해 공유 저장 공간에 저장되어 있는 정수값을 가능한 빨리 가져다 사용합니다. 생성자와 마찬가지로 이러한 과정을 계속적으로 반복합니다. 이 때, 생성자와 요청자는 SharedData 객체를 이용하여 정수 데이터를 공유하도록 되어 있기 때문에 다음과 같은 문제가 발생할 수 있습니다. 하나는 생성자가 요청자보다 더 빠를 경우: 요청자가 가져가기도 전에 다른 정수를 생성하므로 전에 생성된 정수는 전혀 요청자에 의해 사용될 수 없습니다. 그러므로, 요청자는 수행 속도 차에 의해 하나 또는 그 이상의 수들을 다음과 같이 놓치게 됩니다.

Requester #1 got: 3

Generator #1 put: 4

Generator #1 put: 5

Requester #1 got: 5

생성자가 요청자보다 빠를 경우: 요청자는 같은 값을 두 번 이상 가져가 사용할 수 있습니다. 다시 말해서, 가져다 쓴 값을 또 가져다 쓸 수 있다는 것입니다. 따라서, 요청자는 다음과 같이 같은 값을 두 번 이상 출력하게 됩니다.

Generator #1 put: 4

Requester #1 got: 4

Requester #1 got: 4

Generator #1 put: 5

이 두 경우 모두 의도하는 결과를 얻을 수가 없습니다. 이러한 경우를 경주 상태(race condition)라 하고, 이러한 문제는 다중스레드 프로그램에서 비동기적으로 실행되는 여러 스레드가 동시에 같은 객체에 접근하려 할 때 자주 발생하게 됩니다. 이러한 경주 상태를 해결하기 위해 생성자가 SharedData 자원에 정수값을 쓰는 것과 요청자가 SharedData 자원에 있는 값을 참조하는 것이 반드시 동기화 될 수 있도록 해 주어야 합니다. 이렇게 함으로써 요청자는 생성자가 생성한 정수를 정확히 한 번 가져다 사용할 수 있게 됩니다. 이러한 생성자/요청자 문제에서 생성자 스레드와 요청자 스레드를 동기화하기 위해 자바에서는 다음과 같은 두 가지 기법을 제공해 주고 있습니다.

            - 모니터(Monitors)

            - notifyAll and wait 메소드

 

. 스레드의 동기화

각 스레드가 자신이 실행될 때 필요로 하는 모든 데이터와 메소드를 포함하고, 다른 외부 자원이나 메소드를 전혀 필요로 하지 않을 때, 이를 비동기(asynchronous) 스레드라고 합니다. 게다가, 이러한 스레드는 동시에 수행되고 있는 다른 스레드가 어떤 상태가 되고 실행되는지 상관없이 오직 자신의 길을 가는 스레드입니다. 그러나, 이러한 스레드만이 존재하는 것이 아니고, 서로 독립적으로 실행되더라도 서로 데이터를 공유한다거나 다른 스레드의 상태 또는 행동들을 고려해서 수행되는 스레드도 있습니다. 이러한 경우를 가장 잘 보여주는 예가 위에서 언급한 바와 같이, 데이터를 계속 생성하는 생성자와 이 데이터를 계속 요청하여 가져가는 요청자의 관계를 나타내는 생성자/요청자 문제(Generator/Requester problem)입니다.

예를 들어, 자바 애플리케이션을 작성하였는데, 거기에 포함된 두 개의 스레드 중 하나는 파일에 데이터를 쓰고, 이와 동시에 다른 하나의 스레드는 같은 파일에서 그 데이터를 읽는 작업을 수행한다거나, 또는 여러분이 키보드에 키를 계속 입력할 때 생성자 스레드는 이벤트 큐에 키 이벤트를 추가시키고, 요청자 스레드는 같은 큐로부터 그 이벤트를 읽어 들일 수 있습니다. 위의 예에 나타난 두 가지 경우 모두 해당 스레드들은 파일 또는 이벤트 큐를 공유하는 것과 같이 같은 자원을 공유하고 있습니다. 따라서, 이러한 경우에 해당하는 스레드들은 반드시 하나의 같은 자원을 공유하여 원하는 결과를 얻기 위해서 반드시 동기화 될 필요가 있습니다.

            - 모니터(Monitors): 두 개의 스레드에 의해 공유되고 그 값이 참조될 때 반드시 동기화되어야 하는 SharedData와 같은 객체를 상태 변수(condition variables)라고 합니다. 자바 언어에서는 상태 변수에 대한 모니터를 사용할 수 있도록 함으로써 스레드를 동기화 할 수 있도록 해 주고 있습니다. 모니터는 두 개의 스레드가 동시에 같은 변수에 접근하는 것을 방지해 줍니다.

            - notifyAll 메소드와 wait 메소드: 모니터보다 더 고수준 기법으로서 Object 객체의 notifyAll 메소드와 wait 메소드를 사용하여 생성자 스레드와 요청자 스레드의 행동을 중재해 줍니다. SharedData 객체는 생성자 스레드에 의해 자신 내에 위치한 각 값이 요청자 스레드에 의해 단지 한 번만 쓰여질 수 있도록 하기 위해 notifyAll 메소드와 wait 메소드를 사용합니다.

 

. 모니터

자바 언어와 자바 런타임 시스템은 모니터의 사용을 통해 스레드를 동기화 할 수 있도록 해 주고 있습니다. 모니터란 상태 변수(condition variable)라는 특별한 데이터 아이템과 데이터에 대해 lock을 걸도록 작동되는 기능(function)과 관련되어 있습니다. 스레드가 어떤 데이터 아이템에 대한 모니터를 갖게 되었을 때, 다른 스레드는 lock 되어 그 데이터를 참조하거나 변경할 수 없도록 되어 있습니다. 이렇게 서로 별개이면서 동시에 실행되고 있는 스레드들에 의해 같은 데이터가 참조될 수 있는 코드 세그먼트를 임계영역(critical section)이라 합니다. 자바 언어에서는 프로그램 내에 이러한 임계영역을 만들기 위하여 synchronized 키워드를 사용할 수 있습니다. synchronized 키워드를 사용할 수 있는 방법은 다음과 같이 세 가지가 있습니다.


            - synchronized 클래스 메소드

synchronized static int getID() {

}

            - synchronized 인스턴스 메소드

synchronized int get() {

}

            - synchronized 메소드 내의 블록문

synchronized int get() {

synchronized(this) {

// synchronized code

}

}


자바에서는 일반적으로 메소드를 임계영역으로 설정하기를 권장하고 있습니다. 그리고, 메소드 레벨에서만 synchronized 키워드를 사용하도록 권장하고 있습니다. 이렇게 함으로써, 아주 작은 코드 세그먼트까지 동기화 되도록 할 수 있습니다. 자바 언어에서는 synchronized 메소드를 갖는 모든 객체는 하나의 고유한 모니터를 갖도록 하고 있습니다. 앞에서 살펴본 생성자/요청자 문제에서 사용하던 SharedData 클래스는 다음과 같은 두 개의 synchronized 메소드를 갖습니다.

            - put(): SharedData 내에 있는 값을 변경하기 위해 사용

            - get():SharedData 내에 있는 값을 참조하기 위해 사용

그러므로 시스템은 SharedData 클래스의 모든 인스턴스에 대하여 하나의 고유한 모니터를 연관시키게 됩니다. 이 때, 자바 모니터는 재진입이 가능합니다. 같은 스레드는 이미 잡고있는 모니터를 synchronized 메소드를 호출할 수 있으므로 모니터를 재획득이 가능합니다. 생성자 스레드가 SharedData put 메소드를 호출할 때마다, 생성자 스레드는 그 SharedData을 위한 모니터를 획득할 수 있고, 이 때 요청자 스레드는 get 메소드를 호출할 수 없게 됩니다. 다음에 나오는 그림은 SharedData 클래스의 put 메소드를 보여주고 있습니다.


public synchronized void put(int value) {
// Generator 스레드는 모니터를 할당 받습니다.
  while (available == true) {
    try {
        wait();
         }
    catch (InterruptedException e) {
    }
  }
  contents = value;
  available = true;
  notifyAll();
  // Generator 스레드는 모니터를 해제합니다.
}

<그림 12. SharedData 객체의 put 메소드 내에서의 모니터의 할당과 해제>


put 메소드가 리턴할 때, 생성자 스레드는 SharedData 객체를 unlocking 하기 때문에 모니터를 해제하게 됩니다. 반대로, 요청자 스레드가 SharedData 객체의 get 메소드를 호출할 때마다, 요청자 스레드는 SharedData 객체를 위한 모니터를 획득하게 되고, 이 때 생성자 스레드는 put 메소드를 호출할 수 없도록 됩니다. 다음에 나오는 그림은 SharedData 클래스의 put 메소드를 보여주고 있습니다.




public synchronized int get() {
  // Requester 스레드는 모니터를 할당 받습니다.
  while (available == false) {
    try {
        wait();
    } catch (InterruptedException e) {
    }
  }
  available = false;
  notifyAll();
  return contents;
  // Requester 스레드는 모니터를 해제합니다.
}

<그림 13. SharedData 객체의 get 메소드 내에서의 모니터의 할당과 해제>


모니터의 획득과 해제는 자바 런타임 시스템에 의해 자동으로 하나의 명령으로 이루어 집니다. 이렇게 함으로써 스레드 구현을 보다 효율적으로 할 수 있고 경주 상태가 발생하지 않도록 할 수 있습니다. 자바 모니터는 재진입(reentrant)이 가능하기 때문에, 자바 런타임 시스템은 이미 다른 스레드에 의해 잡혀있는 모니터라도 다른 스레드가 재획득 할 수 있도록 해 주고 있습니다. 이렇게 재진입 가능한 모니터는 하나의 단일 스레드가 이미 잡고 있는 모니터에 대해 요청함으로써 자신에게 deadlock 될 가능성을 제거해 줍니다.

 

. 효율적인 동기화 기법 – wait notify 메소드

SharedData 객체 내에 있는 get 메소드와 put 메소드는 둘 다 SharedData 객체 내에 값을 위치시키고 얻는 것을 중재하기 위하여 notifyAll 메소드와 wait 메소드를 사용하고 있습니다. 이 때, notifyAll 메소드와 wait 메소드는 모두 java.lang.Object 클래스의 메소드입니다. 그리고 한 가지 주의할 점은 notifyAll 메소드와 wait 메소드는 모두 lock을 잡고 있는 스레드에 의해서만 호출될 수 있다는 것입니다.

get 메소드는 리턴하기 전에 마지막 작업으로 notifyAll 메소드를 호출하고 있습니다. notifyAll 메소드는 현재 스레드에 잡혀있는 모니터 상에서 기다리고 있는 모든 스레드에게 통보하여 깨워주는(wake-up) 역할을 합니다. 일반적으로, 기다리고 있는 스레드 중 하나가 모니터를 잡고 자신의 일을 처리할 수 있습니다.

생성자/요청자 예에서는 요청자 스레드가 get 메소드를 호출하고, 그러므로 요청자 스레드는 get 메소드를 실행하는 동안 SharedData 객체를 위한 모니터를 잡고 있게 됩니다. get 메소드의 끝에서 notifyAll  메소드를 호출함으로써 SharedData 객체의 모니터를 얻기 위해 기다리고 있는 생성자 스레드를 깨우게 됩니다. 이제, 생성자 스레드는 SharedData 모니터를 얻을 수 있고 자신의 작업을 계속할 수 있게 되는 것이지요.


public synchronized int get() {
  while (available == false) {
  try {
       wait();
       } catch (InterruptedException e) {
       }
  }
  available = false;
  notifyAll();                      // notifies Generator
  return contents;
}

<그림 14. SharedData 객체의 get 메소드 내에서의 notifyAll>


여러 개의 스레드들이 하나의 모니터를 기다리고 있다면, 자바 런타임 머신은 실행하기 위해 기다리고 있는 스레드 중 하나를 선택해 줍니다. 이 때 어떤 스레드가 선택될 지는 보장할 수 없습니다.

Object 클래스는 notify 메소드를 갖고 있는데, notify 메소드는 해당 모니터를 기다리고 있는 스레드들 중 하나를 마음대로 깨우게 됩니다. 이러한 상황에서, 기다리고 있는 스레드들 중 선택되지 않고 남아있는 나머지 스레드들은 모니터를 획득할 때까지 계속 기다리게 됩니다. notify 메소드를 사용하는 것은 다소 위험할 수 있으므로, 보다 안정적인 프로그램을 작성하기 위해서는 notify 메소드 보다는 notifyAll  메소드를 사용하는 것이 바람직합니다.

wait 메소드는 현재 스레드가 다른 스레드가 상태 변화에 대해 통보(notify)할 때까지 기다리도록 합니다. 따라서, 같은 자원을 여러 개의 스레드가 사용할 때, 이의 중재를 위해 notify 또는 notifyAll 메소드와 결합하여 wait 메소드를 사용할 수 있습니다.

get 메소드는 available이 참이 될 때까지 반복하는 while 문을 포함하고 있습니다. 만약 available이 거짓이면 생성자 스레드가 새로운 값을 아직 생성하지 않았으므로 요청자 스레드는 기다려야 한다는 것을 나타내는 것이므로 get 메소드는 wait 메소드를 호출하게 됩니다. 따라서, wait 메소드는 생성자 스레드가 통보할 때까지 무한정 기다리게 될 것입니다. put 메소드에서 notifyAll 메소드를 호출하게 되면, 요청자 스레드는 wait 상태에서 깨어나 while 루프를 계속 반복하게 됩니다. 아마도, 생성자 스레드가 새로운 값을 생성하고 available을 참으로 변경하였을 것이기 때문에 get 메소드는 while 루프를 빠져나가 계속 수행되겠지요. 만약 생성자 스레드가 새로운 값을 생성하지 못했다면, available이 거짓이 되기 때문에 while 루프를 다시 반복하게 되고 생성자 스레드가 새로운 값을 생산하고 notifyAll 메소드를 호출할 때까지 계속 기다리게 되겠지요.

 public synchronized int get() {
		while (available == false) {
			try {
				wait();
				// waits for notifyAll() call from Generator
			} catch (InterruptedException e) {
			}
		}
		available = false;
		notifyAll();
		return contents;
	}

<그림 15. SharedData 객체의 get 메소드 내에서의 notifyAll>


put 메소드 역시 get 메소드와 같은 방식으로 동작하게 됩니다. 요청자 스레드가 현재 값을 소비할 때까지 기다리고, 요청자 스레드가 현재 값을 소비하게 되면, 생성자 스레드는 다시 새로운 값을 생성하게 되는 것이지요.

생성자/요청자 문제에서는 무한정 기다리는 wait 메소드를 사용하였지만, 실제로 Object 클래스는 다음과 같은 세 가지의 wait 메소드를 제공해 주고 있습니다.

            - wait() throws InterruptedException,

            - wait(long timeout) throws InterruptedException,

            - wait(long timeout, int nanos) throws InterruptedException: 객체 내에서의 변화를 다른 스레드에 의해 알려지기(notify)를 기다립니다.

timeout - 최대 대기 시간(milliseconds)

nanos - 추가 시간(nanoseconds, 범위 0-999999)

다음에 나오는 자바 프로그램은 위에서 살펴본, 생성자/요청자 문제를 위한 프로그램입니다.



class SharedData {
	private int data;
	private boolean available = false;

	public synchronized int get() {
		while (available == false) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}
		available = false;
		notifyAll();
		return data;
	}

	public synchronized void put(int value) {
		while (available == true) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}
		data = value;
		available = true;
		notifyAll();
	}
}

class Generator extends Thread {
	private SharedData sharedData;

	public Generator(SharedData data, int id) {
		sharedData = data;
		setName("Generator-" + id);
	}

	public void run() {
		int i;
		while (true) {
			i = (int) (Math.random() * 100);
			System.out.println(getName() + " put: " + i);
			sharedData.put(i);
			try {
				sleep((int) (Math.random() * 200));
			} catch (InterruptedException e) {
			}
		}
	}
}

class Requester extends Thread {
	private SharedData sharedData;

	public Requester(SharedData c, int id) {
		sharedData = c;
		setName("Requester-" + id);
	}

	public void run() {
		while (true) {
			System.out.println(getName() + " get: " + sharedData.get());
		}
	}
}

class GeneratorRequesterTest {
	public static void main(String[] args) {
		SharedData c = new SharedData();
		Generator gen = new Generator(c, 1);
		Requester req = new Requester(c, 1);
		gen.start();
		req.start();
		try {
			Thread.sleep(3000);
		} catch (InterruptedException e) {
		}
		gen.stop();
		req.stop();
	}
}
/*
 * Results: D:\AIIT\JAVA\06> ... Generator-1 put: 47 Requester-1 get: 47
 * Generator-1 put: 25 Requester-1 get: 25 Generator-1 put: 99 Requester-1 get:
 * 99 Generator-1 put: 5 Requester-1 get: 5 D:\AIIT\JAVA\06>
 */

<프로그램 10. GeneratorRequesterTest.java>


반응형