카테고리 | html | 잡담 | 영어공부 | 수학 | 한글공부 | 컴퓨터공부 | 자격증 | javascript | php | mysql | c | cpp | api | mfc | java | zero | freeboard | game | stl | 오픈소스 | direct | xml | 정치 | 경제 | 생활/문화 | 세계 | IT/과학 | 지역 | 연예 | 스포츠 | 라이프 | 재미 | 주식 | 기타 | 사회
이전 페이지로

Thread

Chapter

 14

   Thread

 

현재 실행되고 있는 프로그램을 프로세스(Process)라고 한다. 요즘의 PC에 설치된 대부분의 운영체제는 여러 개의 프로세스를 실행시킬 수 있다. 예를 들면, 하나의 PC로 인터넷 서핑을 하면서 음악을 듣거나 TV를 보는 것이다. CPU가 하나인 컴퓨터에서는 여러 프로그램이 동시에 실행될 수 없다. CPU가 여러 프로그램을 교대로 번갈아 가며 실행시켜주기 때문에 마치 여러 프로그램이 동시에 실행되는 것처럼 보이는 것이다. 그러나 한 시점에서 실행되는 프로세스는 하나이다.

 

[그림 14-1] Multi-Process

 

하나의 프로세스 내에서도 여러 작업이 동시에 실행될 수 있는데, 프로세스 안에서 실행되는 여러 작업을 스레드(Thread)라고 한다. 여러 스레드가 존재하더라도 한 시점에서 실행되는 스레드는 하나이다.

 

[그림 14-2] Multi-Thread

 

스레드는 게임이나 네트워크 프로그래밍에서 유용하게 활용할 수 있다.

 

 

분야

게임 프로그래밍

게임에 등장하는 캐릭터를 움직이게 하는 스레드

여러 캐릭터들이 동시에 움직인다.

네트워크 프로그래밍

프로그램의 흐름을 담당하는 스레드

네트워크로부터 데이터를 기다리는 스레드

[표 14-1] 스레드의 활용

 

 

알아두기

 

Multi-Programming

여러 개의 프로그램들이 단일 CPU 상에서 동시에 실행되는 것을 말한다. 그러나, 그 컴퓨터에는 한 개의 CPU만이 존재하기 때문에, 진정한 의미로는 여러 개의 프로그램이 동시에 실행된다고 볼 수는 없다. 다만, 한 프로그램이 일부 실행되고 나서, 또 다른 프로그램이 일부 실행되는 식이다. 따라서 모든 프로그램이 마치 동시에 수행되는 것처럼 보이게 된다.

 

 

 

Runtime 클래스와 Process 클래스

자바에는 runtime object라는 객체가 존재한다. 이 객체는 JVM이 작동하는 시스템과의 인터페이스를 제공하는 객체로 자바 클래스가 아닌 운영체제 기반의 프로그램을 실행시키거나 운영체제에 대한 정보를 제공한다.

 

Runtime 클래스의 static 메소드, getRuntime을 호출하면 runtime object를 얻어올 수 있다. 즉 다음과 같이 하면 된다.

 

Runtime rt=Runtime.getRuntime();

 

runtime object는 자바 프로그램이 실행되면 생기는 객체이므로 new 키워드로 만들 수 없고 얻어와서(getRuntime) 사용해야 한다.

 

Runtime 클래스의 유용한 메소드를 살펴보자.

 

Runtime 클래스의 유용한 메소드

 

public static Runtime getRuntime()

runtime object의 레퍼런스를 반환한다.

public Process exec(String command) throws IOException

명령(command)을 실행시키고, 실행시킨 프로세스의 레퍼런스를 반환한다.

이 외에도 많으므로 API를 참고하자.

public void exit(int status)

JVM을 종료시킨다.

public native void gc();

garbage collector를 실행시킨다. 사용 가능한 메모리가 증가한다.

public native long freeMemory();

JVM이 사용 가능한 메모리 양(bytes)을 반환한다.

public native long maxMemory();

JVM이 사용할 수 있는 최대 메모리 양을 반환한다.

public native long totalMemory();

JVM이 사용하고 있는 전체 메모리를 반환한다.

 

 

다음 예제는 runtime object를 사용하여 Windows의 메모장 프로그램을 실행시키고 메모리 양을 출력하는 예제이다. Runtime 클래스와 Process 클래스는 java.lang 패키지에 있으므로 따로 import하지 않아도 된다.

 

RunAndPro1.java

 

public class RunAndPro1{

  public static void main(String[] args) throws Exception{

    Runtime rt=Runtime.getRuntime();                         // x1

    Process pr=rt.exec("c:\\windows\\notepad.exe");     // x2

   

    System.out.println("최대 메모리: "+rt.maxMemory()+"bytes");

    System.out.println("토탈 메모리: "+rt.totalMemory()+"bytes");

    System.out.println("자유 메모리: "+rt.freeMemory()+"bytes");

  }

}


 

 

x1행에서 runtime object를 얻어온다. 즉 rt가 runtime object의 레퍼런스이다. x2행에서는 Windows의 메모장 프로그램을 실행시킨다. pr은 실행되고 있는 메모장 프로그램(프로세스)을 참조한다. 그리고 메모리의 상태를 출력하고 있다.

 

Process 클래스는 현재 실행되고 있는 프로세스와 관련된 클래스이다. Process 클래스의 유용한 메소드를 살펴보자.

 

Process 클래스의 유용한 메소드

 

abstract public void destroy();

프로세스를 강제로 종료시킨다.

abstract public InputStream getInputStream();

프로세스의 InputStream을 반환한다.

abstract public OutputStream getOutputStream();

프로세스의 OutputStream을 반환한다.

abstract public int waitFor() throws InterruptedException;

프로세스가 종료되기를 기다린다. 프로세스가 종료되면 종료 값을 반환한다.

종료 값이 0이면 보통 정상적으로 종료되었음을 의미한다.

 

 

다음 예제는 메모장을 실행시키고 끝나기를 기다린 다음 종료 값을 출력하는 예제이다.

 

RunAndPro2.java

 

public class RunAndPro2{

  public static void main(String[] args) throws Exception{

    Runtime rt=Runtime.getRuntime();

    System.out.println("메모장을 실행합니다.");

    Process pr=rt.exec("c:\\windows\\notepad.exe");

 

    // 프로세스가 종료되기를 기다린다.

    int exitValue=pr.waitFor();

 

    System.out.println("메모장이 끝났습니다.");  // 메모장을 닫으면 실행된다.

    System.out.println("종료 값: "+exitValue);

  }

}


 

InteractiveProcessTest.java

 

import java.io.*;

import java.lang.*;

class InteractiveProcessTest {

   public static void main(String[] args) {

      try {

         Process p = Runtime.getRuntime().exec("ping 211.221.44.35");

         byte[] msg = new byte[128];

         int len;

         while((len=p.getInputStream().read(msg)) > 0) {

            System.out.print(new String(msg, 0, len));

         }

      } catch (Exception e) {}

   }

}


 

thread 만들기

스레드를 만드는 방법은 조금 복잡해 보이나 원리만 이해한다면 쉽고 단순하다(?). 스레드도 객체로 구현된다. 그렇다고 해서 아무 객체나 스레드가 될 수 있는 것은 아닐 것이다. Runnable이라는 인터페이스를 implement하는 클래스로부터 만들어지는 객체만이 스레드가 될 수 있다. Runnable을 implement하기만 하면 스레드가 될 수 있다는 말이기도 하다.

 

public interface Runnable{

    public abstract void run();

}

 

// runnable의 사전적으로 "실행할 수 있는"의 의미이다.

 

보다시피 Runnable은 멤버로 run()을 가지는 아주 단순한 인터페이스이다. 하지만 그 능력은 무시할 수 없다. 앞에서 언급했듯이 Runnable을 implement하는 클래스는 스레드가 될 수 있다.

 

스레드를 만드는 방법은 다음과 같다.

 

Thread t=new Thread(runnable 객체);

 

runnable 객체란 Runnable 인터페이스를 implement하는 클래스의 객체를 말하는 것으로 스레드가 실행되면 runnable 객체의 run()이 호출되어 실행된다.

 

위에서 만든 스레드 t를 실행시키려면 Thread의 멤버 메소드 start()를 다음과 같이 호출한다.

 

t.start();

 

다음 예제는 스레드를 만들고 실행하는 방법을 보여 준다.

 

Thread1.java

 

class MyRunnable implements Runnable{      // x1

  String name;

  MyRunnable(String name){                    // 생성자

    this.name=name;

  }

  public void run(){                           // x2, Runnable을 구현

    for(int i=1;i<=10;i++)

      System.out.println(name+": "+i);

  }

}

 

public class Thread1{

  public static void main(String[] args){

    MyRunnable myr=new MyRunnable("myrunable");    // x3, runnable object

    Thread t=new Thread(myr);                        // x4, 스레드 객체 만듦

    t.start();                                           // x5,  스레드 실행

  }

}


 

출력 결과

 

myrunable: 1

myrunable: 2

...

myrunable: 9

myrunable: 10


 

x1행에서 Runnable을 implement하는 클래스 MyRunnable을 정의한다. x2행에서 run()을 오버라이드하고 있다. x3행에서 runnable 객체 myr을 만든다. x4행에서 스레드 객체 t를 만들고 x5행에서 스레드를 실행시킨다. 스레드가 실행되면 runnable 객체, myr의 run()이 호출되어 실행된다. 출력 결과를 보면 run() 메소드가 실행되었음을 알 수 있다. x2행의 run()의 실행이 끝나면 스레드가 끝나는데 스레드가 죽었다(die)라고 말한다. 반대인 경우는 스레드가 살아있다(alive)라고 말한다. 현재 스레드의 상태를 알고싶다면 isAlive()를 호출하면 된다.

 

public final native boolean isAlive();

 

살아있다면 true를 반환하고 죽었다면 false를 반환한다.

 

t.isAlive()      // 죽었니? 살았니?

 

 

 

스레드를 만드는 또 다른 방법

사실 Thread 클래스도 Runnable을 implement한다. 따라서 Thread 클래스를 상속하는 클래스를 만들고 run()을 오버라이드 하면, 클래스로부터 만들어지는 객체는 스레드가 된다.

 

다음 예제를 해보자.

 

Thread2.java

 

class MyThread extends Thread{                     // 스레드 클래스

  public void run(){                                  // 오버라이드

    for(int i=1;i<=10;i++)

      System.out.println(super.getName()+": "+i);    // x1

  }

}

public class Thread2{

  public static void main(String[] args){

    MyThread mt=new MyThread();

    mt.start();

  }

}


 

출력 결과

 

Thread-1: 1

Thread-1: 2

...

Thread-1: 9

Thread-1: 10


 

 

x1행의 super.getName()은 스레드의 이름을 반환한다. 기본적으로 'Thread-번호' 형태의 이름을 가진다. 위 예제에는 스레드가 하나이므로 'Thread-1'이다. 만든 순서대로 번호가 증가한다.

 

위 예제와 같은 방법은 runnable 객체를 이용하는 방법보다 간편하지만 Thread 클래스를 상속하면 다른 클래스를 상속할 수 없다는 것에 유의해야 한다.

 

한가지 짚어 둘 것이 있다. 다음 예제의 SumThread의 객체는 from~to까지 정수의 합을 구하는 역할을 한다.

 

Thread3.java

 

class SumThread extends Thread{

  int from, to;                           // from~to까지

  long sum;                             // from~to까지의 합

  SumThread(int from, int to){           // 생성자

    this.from=from;

    this.to=to;

  }

  long getSum(){                      // 합을 반환한다.

    return sum;

  }

  public void run(){                    // from~~to까지 합을 구한다.

    for(int i=from;i<=to;i++)

      sum+=i;

  }

}

public class Thread3{

  public static void main(String[] args){

    SumThread st=new SumThread(1, 1000);    // 1부터 1000까지의 합을 구한다.

    st.start();                                   // x1, 스레드를 실행시킨다.

    System.out.println(st.getSum());            // x2, st가 구한 합을 출력한다.

    System.out.println(st.isAlive());             // x3, st가 살아있는 지 알아본다.

  }

}


 

출력 결과

 

0

true


 

 

출력 결과부터 보자. 위와 다르게 출력될 수도 있으나 분명히 원하는 값보다 작은 값이 출력될 것이다. x1행에서 st를 실행시키는 것은 main 스레드이다. main 스레드는 st를 실행시켜놓고 바로 x2행을 실행한다. 따라서 st의 실행이 끝나기도 전에 합을 출력하기 때문에 정확한 값이 출력되지 않는다. 증거로 x3행에서 true가 출력된다. 그렇다면 어떻게 하면 정확한 값을 구할 수 있을까?

 

st의 실행이 끝난 후에 합을 출력하면 될 것이다. 어이없는 말 같지만 맞는 다중 스레드를 다룰 때 이와 비슷한 문제와 많이 부딪힌다. 하여튼 join이라는 Thread의 멤버 메소드를 이용하면 스레드가 끝날 때까지 기다릴 수 있다.

 

 

public final void join() throws InterruptedException

스레드가 끝날 때까지 무작정 기다린다.

public final synchronized void join(long millis)

스레드가 끝날 때까지 기다린다. 하지만 millis(1/1000초)까지만 기다린다.

※ synchronized 키워드는 동기화 부분에서 자세히 살펴보도록 하자.

public final synchronized void join(long millis, int nanos)

스레드가 끝날 때까지 기다린다. 하지만 millis(밀리초)+nanos(나노초)까지 기다린다.

 

join 메소드를 이용하여 문제점을 개선해보자.

 

Thread3_1.java

 

public class Thread3_1{

  public static void main(String[] args){

    SumThread st=new SumThread(1, 1000);

    st.start();

    try{

      st.join();  // x1, st가 끝나도록 기다린다.

    }catch(InterruptedException ie){}

  

    System.out.println(st.getSum());     // x2, st가 구한 합을 출력한다.

  }

}


 

출력 결과

 

500500


 

x1행에서 st가 끝날 때까지 기다린다. 그런데 누가 기다린다는 말인가? main 스레드가 st가 끝날 때까지 기다린다. 즉, main 스레드는 대기 상태가 된다. st가 죽으면 main 스레드가 살아나서 x3행을 실행한다. 따라서 원하던 값을 얻는다.

 

join 메소드는 InterruptedException을 던지는데 조금 있다가 자세히 알아보자.

 

 

 

다중 thread

여러 스레드가 한꺼번에 실행(start)되면 스레드들은 실행 순서대로 주어진 시간만큼씩 번갈아 가며 실행한다.

 

다음 예제를 해보자.

 

Thread4.java

 

class MyThread extends Thread{

  public void run(){

    for(int i=1;i<=10;i++)

      System.out.println(super.getName()+": "+i);

  }

}

public class Thread4{

  public static void main(String[] args){

    MyThread mt1=new MyThread();

    MyThread mt2=new MyThread();

    mt1.start();                        // x1, Thread-1

    mt2.start();                        // x2, Thread-2

  }

}


 

출력 결과

 

Thread-2: 1

Thread-1: 1

Thread-2: 2

Thread-1: 2

...

Thread-2: 10

Thread-1: 10


 

x1행과 x2행에서 두 개의 스레드를 실행시키고 있다. 따라서 mt1과 mt2의 run 메소드가 번갈아 가며 실행된다. 위 예제를 여러 번 실행하면 그때마다 출력 결과가 다르게 나오는데 스레드의 실행 시간과 순서를 스레드 스케줄러(Thread Scheduler)란 놈이 관리하기 때문이다.

 

이와 같은 다중 스레드를 이용하면 복잡한 작업을 여러 작업으로 쪼개서 처리할 수 있다.

 

다음 예제는 1~500까지 합을 구하는 스레드와 501~1000까지 합을 구하는 스레드로 나누어서 1~1000까지의 합을 구하는 프로그램이다. SumThread는 앞에서 작성한 클래스이다.

 

Thread5.java

 

public class Thread5{

  public static void main(String[] args){

    SumThread st1=new SumThread(1,500);            // 1~500까지 합

    SumThread st2=new SumThread(501, 1000);       // 501~1000까지 합

    st1.start();                                        // st1 스레드 실행

    st2.start();                                        // st2 스레드 실행

    try{

      st1.join();                           // x1, st1 스레드가 끝나기를 기다린다.

      st2.join();                           // x2,  st2 스레드가 끝나기를 기다린다.

    }catch(InterruptedException ie){}

 

    long sum=st1.getSum()+st2.getSum();             // x3

    System.out.println("1~1000까지 합: "+sum);

  }

}


 

출력 결과

 

1~1000까지 합: 500500


 

st1은 1부터 500까지의 합을 구하는 스레드이고 st2는 501~1000까지의 합을 구하는 스레드이다. x1행과 x2행에서 이들 스레드가 끝나기를 기다리고 x3행에서 두 스레드가 구한 합을 더하여 1~1000까지의 합을 구한다.

 

스레드를 이용하는 작업을 분할했다고 1~1000까지 합을 바로 구하는 것보다 속도 면에서 빠른 것은 아니다.

 

 

혼자 해보기

 Alone14_1.java

10개의 스레드를 이용하여 1~1000까지의 합을 구하여보자. 스레드 배열을 이용하는 것이 효율적이다.

 

출력 결과 예시

 

1~100까지 합: 5050

101~200까지 합: 15050

201~300까지 합: 25050

...

901~1000까지 합: 95050

총 합: 500500


 

알아두기

 

<스레드의 상태>

Startable : start()를 호출하면 언제든지 실행 가능한 상태

Runnable: 스레드 스케줄러에 의해 실행 순서를 할당받아 순서가 돌아온다면 언제든지 실행될 수 있는 상태

Not Runnable: 잠시 대기 상태(실행 순서를 할당받지 못한 상태)

Dead: run 메소드의 실행이 끝났거나 스레드의 실행을 강제로 정지시킨 상태

 

 

 

thread의 우선 순위

우선 순위(Priority)가 높은 스레드는 우선 순위가 낮은 스레드에 비해 실행시간을 많이 할당받는다. 다른 스레드에 비하여 어떤 스레드가 많은 실행시간을 필요로 한다면 우선 순위를 높게 두면 된다. 스레드의 우선 순위를 지정하는 메소드로 setPriority가 있다.

 

public final void setPriority(int newPriority)

 

newPrority값으로 1~10까지의 정수가 사용하는데 클수록 우선 순위가 높다.

 

Thread 클래스에 우선 순위와 관련된 상수가 다음과 같이 선언되어 있다.

 

public final static int MIN_PRIORITY = 1;     // 가장 낮은 우선 순위

public final static int NORM_PRIORITY = 5;   // 기본 우선 순위

public final static int MAX_PRIORITY = 10;    // 가장 높은 우선 순위

 

A 스레드가 1의 우선 순위를 가졌고, B 스레드가 10의 우선 순위를 가졌다면 B스레드가 할당받은 실행 시간에 비해 A 스레드는 지극히 적은 시간을 할당받는다.

 

다음 예제를 해 보자.

 

Thread6.java

 

public class Thread6 extends Thread{

  public void run(){

    for(int i=1;i<=10;i++)

      System.out.println(getName()+": "+i);

  }

  public static void main(String[] args){

    Thread6 t1=new Thread6();

    Thread6 t2=new Thread6();

    t1.setPriority(Thread.MIN_PRIORITY);       // 우선 순위를 가장 낮게

    t2.setPriority(Thread.MAX_PRIORITY);      // 우선 순위를 가장 높게

    t1.start();

    t2.start();

  }

}


 

출력 결과

 

Thread-2: 1

Thread-2: 2

...

Thread-1: 9

Thread-1: 10


 

 

출력 결과에서 보이듯이 우선 순위가 높은 스레드 t2가 실행시간을 많이 할당받는다. t1이 먼저 start()하더라도 t2에 비해 우선 순위가 낮아서 t2가 끝나기 전까지 거의 실행시간을 할당받지 못했음을 알 수 있다.

 

우선 순위를 지정하지 않으면 기본 우선 순위(NORM_PRIORITY)를 가진다.

 

 

 

thread 재우기와 깨우기

스레드를 재운다는 말은 스레드를 잠시 대기 상태로 만든다는 것이다. sleep 메소드는 주어진 시간 동안 스레드를 대기 상태로 만든다. 주어진 시간이 지나면 대기 상태를 벗어나 하던 일을 계속 실행한다.

 

public static native void sleep(long millis) throws InterruptedException;

millis(밀리초)까지 스레드를 재운다.

public static void sleep(long millis, int nanos) throws InterruptedException

millis(밀리초)+nanos(나노초)까지 스레드를 재운다.

 

다음 예제를 실행시키면 약 1초에 한번씩 x1행이 실행된다.

 

Thread7.java

 

public class Thread7 extends Thread{

  public void run(){

    for(int i=1;i<=10;i++){

      System.out.println(getName()+": "+i);          // x1

      try{

        sleep(1000);                              // 1초간 대기하라...!!

      }catch(InterruptedException ie){}

    }

  }

  public static void main(String[] args){

    Thread7 t1=new Thread7();

    t1.start();

  }

}


 

 

다음 예제를 해보자.

 

Thread8.java

 

 

// 개행 문자를 출력하고 약 1초 동안 대기하는 쓰레드

 

class NewLine extends Thread{

  public void run(){

    for(int i=1;i<=10;i++){

      System.out.println();              // 개행 문자 출력

      try{

        sleep(1000);                    // 1초 동안 대기

      }catch(InterruptedException ie){}

    }

  }

}

 

// 정수를 출력하고 약 0.1초 동안 대기하는 쓰레드

 

public class Thread8 extends Thread{

  public void run(){

    for(int i=1;i<=100;i++){

      System.out.print(i);        // print 메소드는 개행 문자를 출력하지 않는다.

      try{

        sleep(100);              // 0.1초 동안 대기

      }catch(InterruptedException ie){}

    }

  }

  public static void main(String[] args){

    Thread8 t=new Thread8();

    NewLine n=new NewLine();

   

    // 두 스레드를 동시에 실행시킨다.

    t.start();

    n.start();

  }

}


 

출력 결과

 

1

2345678910

...

81828384858687888990

919293949596979899100


 

t스레드는 약 0.1초에 한번씩 정수를 출력하고 n스레드는 약 1초에 한번씩 개행 문자를 출력하는 것을 볼 수 있을 것이다.

 

 

혼자 해보기

 Alone14_2.java

"타자기 효과의 문자 찍기" 문자열의 처음 문자부터 1초당 한 글자씩 출력하는 프로그램을 만들어보자.

 

 

출력 결과 예시

 

처음: 타

1초 후: 타자

2초 후: 타자기

....

13초 후: 타자기 효과의 문자 찍기

 


 

interrupt 메소드를 이용하면 자고 있는 스레드를 깨울 수 있다. 깨운다는 것은 대기 상태에 있는 스레드를 실행 가능 상태로 만든다는 말이다. interrupt는 '방해하다'의 뜻이다. 말 그대로 자고 있는 스레드를 방해한다.

 

public void interrupt()   // 자고 있는 스레드를 실행 가능 상태로 만든다.

 

sleep 메소드나 join 메소드로 인해 대기 상태가 된 스레드의 interrupt 메소드를 호출하면 sleep 메소드나 join 메소드에서 InterruptedException이 발생한다.

 

다음 예제를 해보자.

 

Thread9.java

 

public class Thread9 extends Thread{

  public void run(){

    System.out.println("잘테니 깨우지마.");

    try{

      sleep(1000000);                                              // x1

    }catch(InterruptedException ie){                      // x2

      System.out.println("헐~ 누가 깨웠어?");          // x3

    }

  }

  public static void main(String[] args){

    Thread9 t=new Thread9();

    t.start();                                      // x4

    t.interrupt();                                 // x5

  }

}


 

출력 결과

 

잘테니 깨우지마.

헐~ 누가 깨웠어?


 

x4행에서 스레드를 start시키면 x1행에서 곧바로 많은 시간동안 스레드는 잠이 든다. 그러나 x5행에서 main 스레드가 t를 깨우게 되는데, 자다가 방해를 받은 t는 x1행의 sleep 메소드에서 InterruptedException를 던진다. 그래서 x3행이 실행된다.

 

 

 

thread 양보하기

yield 메소드를 이용하여 다른 스레드에게 실행을 양보할 수 있다.

 

public static native void yield();

다른 스레드에게 실행을 양보한다.

 

yield 메소드를 사용해서 특정 스레드에게 실행을 양보할 수는 없다. 살아있는 다른 임의의 스레드가 실행될 것이다.

 

다음 예제를 해보자.

 

Thread10.java

 

public class Thread10 extends Thread{

  public void run(){

    for(int i=1;i<=10;i++){

      System.out.println(getName()+": "+i);  // x1

      yield();                             // x2, 다른 스레드에게 실행을 양보한다.

      try{

          sleep(100);

        }catch(InterruptedException ie){}

    }

  }

  public static void main(String[] args){

    Thread10 t1=new Thread10();

    Thread10 t2=new Thread10();

    t1.start();

    t2.start();

  }

}


 

출력 결과

 

Thread-2: 1

Thread-1: 1

...

Thread-1: 5

Thread-2: 5

...

Thread-1: 10

Thread-2: 10


 

x2행의 yield()가 있을 때와 없을 때의 차이를 비교해 보면 없을 때 보다 있을 때 두 스레드가 번갈아 가며 x1행을 실행할 확률이 높다는 것을 확인할 수 있다. t1과 t2는 서로 실행을 양보(남을 위하여 자신의 이익을 희생하는 것)하기 때문이다.

 

 

짚어두기

 

스레드로부터 양보의 미덕을 배우자.

요즘 지하철이나 버스를 타보면 노약자에게 자리를 양보하는 사람은 몇몇에 불과하다. 자리를 양보하더라도 억지로 하는 사람이 대부분이다. 필자는 노약자가 다른 노약자에게 양보하는 것을 많이 봐왔다. 많이 가져서 베푸는 양보보다는 부족할 때 베푸는 양보가 더 아름다운 것 같다. 자신이 좌석에 앉아야할 만큼 피곤할 때 양보를 해보자. 그만큼 얻는 것도 클 것이다.

 

 

 

동기화(syncronization)

다중 스레드를 실행하면 여러 가지 유추할 수 있는 문제가 발생할 수 있다. 예를 들어, 어떤 두 스레드 A와 B가 같은 프린터를 사용하여 자신들의 데이터를 인쇄한다고 생각하자. 먼저 A가 주어진 시간 동안 인쇄하다가, 다른 스레드에게 실행 순서가 넘어가서 대기상태가 된다. 그러면 실행 권한을 얻은 B가 자신의 데이터를 인쇄하다가 실행 순서가 다른 스레드에게로 넘어간다. 계속해서 A스레드가 실행 권한을 얻어서 하던 작업을 연이어 할 것이다. 이와 같이 두 스레드가 공유하고 있는 프린터로 동시에 인쇄하면 A와 B의 실행이 모두 끝났을 때, 인쇄 결과는 불 보듯 뻔하다. 엉망진창.

 

반면에 A스레드가 자신의 데이터를 완전히 인쇄한 후에, B스레드가 인쇄를 시작하면 문제가 없을 같다. 프린터에 대한 사용 권한을 하나의 스레드만이 가질 수 있다고 생각하자. 먼저 A스레드가 프린터에 대한 사용 권한을 가지고 인쇄 작업을 시작할 것이다. 실행 권한이 B로 넘어가더라도 B는 프린터에 대한 사용 권한이 없어서 대기 상태가 된다. A스레드가 모든 인쇄 작업을 마치고 프린터에 대한 사용 권한을 포기하면 대기하고 있던 B스레드가 프린터에 대한 사용 권한을 얻어서 인쇄를 시작한다.

 

이와 같이 스레드들이 공유하고 있는 자원에 대한 권한을 얻은 스레드만이 어떤 작업을 수행할 수 있도록 하는 것을 동기화(syncronization)라고 한다.

 

다음 예제를 실행해 보자.

 

Synchronization1.java

 

class PrintThread extends Thread{

  char ch;

  public PrintThread(char ch){         // 생성자, ch는 화면에 출력될 문자이다.

    this.ch=ch;

  }

  void printCh10(){                   // 화면에 해당 문자를 10회 출력한다.

    for(int i=1;i<=10;i++)

      System.out.print(ch);           // 개행 문자를 출력하지 않음에 유의하자.

  }

  public void run(){

    for(int i=1;i<=10;i++){

      printCh10();                   // 문자를 10회 출력하고

      System.out.println();          // 개행 문자를 출력한다.

    }

  }

}

public class Synchronization1{

  public static void main(String[] args){

    PrintThread pt=new PrintThread('A');

    pt.start();

  }

}


 

출력 결과

 

AAAAAAAAAA

AAAAAAAAAA

...

AAAAAAAAAA

AAAAAAAAAA


 

어려운 예제가 아니므로 쉽게 이해할 수 있을 것이다. 하지만 다음 예제와 같이 두 개 이상의 스레드가 실행되면 어떻게 출력될까?

 

Synchronization2.java

 

public class Synchronization2{

  public static void main(String[] args){

    PrintThread pt1=new PrintThread('A');

    PrintThread pt2=new PrintThread('B');

    pt1.start();

    pt2.start();

  }

}


 

출력 결과

 

AAAAAAAAAA

...

BABABABABABABABABABA

...

BABABABABABABABABABA

...

BBBBBBBBBB


 

매번 실행될 때마다 위 출력 결과와 다르게 출력될 수 있다. pt1과 pt2는 서로 경쟁하면서 화면에 출력하기 때문에 출력 결과가 뒤죽박죽인 것은 당연하다. 만약 3개 이상의 스레드가 한꺼번에 start된다면 더더욱 뒤죽박죽 된다.

 

이런 문제에 대한 해결 방안은 동기화에 있다. 동기화를 위한 synchronized block과 synchronized method에 대하여 알아보자.

 

 

 

synchronized block

동기화 블록은 어떤 객체의 모니터(monitor)를 가지고 있는 스레드가 실행 권한을 획득하게 하는 방법이다. 모니터는 자바의 모든 객체가 가지고 있는 동기화 보호 객체로 객체에 대한 권한쯤으로 생각하면 되는데, 예를 들어 어떤 당구장에 테이블이 4개 있다고 가정하자. 큐가 4개 이상이면 모든 테이블에서 동시에 당구를 칠 수 있을 것이다. 하지만 이 당구장에 큐가 하나밖에 없다면 번갈아 가며 큐를 가지고 치는 수밖에 없을 것이다. 3번 테이블에서 큐를 가지고 있다면 다른 테이블에서는 큐가 자신의 테이블로 넘어올 때까지 대기해야 한다. 다른 말로 큐를 가지고 있는 테이블이 당구를 칠 수 있는 권한을 가진다고 말할 수 있다.

 

여기서 큐를 어떤 객체의 모니터라고 생각하면 된다. 여러 스레드(테이블)가 있지만 해당 객체의 모니터(큐)를 가지고 있는 스레드(테이블)만이 코드(당구)를 실행할 수 있다. 다른 스레드(테이블)는 객체의 모니터(큐)를 가지기 전까지 대기하게 된다.

 

이제 본론으로 들어가서, 동기화 블록(synchronized block)은 다음과 같은 형태이다.

 

synchronized(someObject){

  [코드]

}

 

동기화 블록 내의 [코드]를 실행 할 수 있는 스레드는 someObject의 모니터를 가지는 스레드이다. 예를 들어, A스레드와 B스레드가 동시에 동기화 블록 안의 [코드]를 실행하려 한다고 생각하자. 그러나 someObject의 모니터를 가진 스레드만이 [코드]를 실행할 수 있다. 만일 A스레드가 [코드]를 실행하고 있다면 someObject에 대한 모니터를 가졌다는 의미이므로 B스레드는 [코드]를 실행할 수 없고 대기 상태가 된다. 또 B스레드가 [코드]를 실행하고 있다면 A스레드는 someObject의 모니터를 가지고 있지 않으므로 대기 상태가 된다. 다른 말로 [코드]를 실행할 수 있는 권한을 가진 스레드가 동기화 블록 내의 [코드]를 실행할 수 있다는 것이다. 그리고 [코드]를 다 실행하고 스레드가 블록을 빠져 나오면 자동적으로 객체의 모니터를 반납하게 되는데, 이때 대기하고 있는 스레드 중, 하나가 someObject의 모니터를 가지게 되어 [코드]를 실행하게 된다.

 

이제 'Synchronization1.java' 예제의 PrintThread 클래스를 수정하여 동기화시켜보자.

 

Sybchronization3.java

 

class PrintThread2 extends Thread{

  char ch;

  static Object printer=new Object();        // x1

  public PrintThread2(char ch){

    this.ch=ch;

  }

  void printCh10(){

    for(int i=1;i<=10;i++)

      System.out.print(ch);

  }

  public void run(){                            // x2

    synchronized(printer){                    // x3, 동기화 블록

      for(int i=1;i<=5;i++){

        printCh10();

        System.out.println();

      }

    }                                          // x4, 동기화 블록 끝

  }

}

public class Synchronization3{

  public static void main(String[] args){

    PrintThread2 pt1=new PrintThread2('A');

    PrintThread2 pt2=new PrintThread2('B');

    PrintThread2 pt3=new PrintThread2('C');

    pt1.start();                                  // x5

    pt2.start();                                  // x6

    pt3.start();                                  // x7

  }

}


 

출력 결과

 

AAAAAAAAAA

AAAAAAAAAA

...

CCCCCCCCCC

...

BBBBBBBBBB

BBBBBBBBBB


 

x1행의 printer 객체는 필자가 임의로 만든 객체로 프린터(하드웨어)를 대표하는 객체라고 생각하자. printer 객체는 static 변수이므로 모든 쓰레드가 공유하고 있다. x2행의 run 메소드에 맨 먼저 접근한 스레드가 x3행의 동기화 블록을 만나서 printer객체의 모니터를 획득하고 for문을 돌면서 화면에 출력할 것이다. x3행에 도달한 나머지 다른 스레드들은 printer의 모니터를 얻기 위해 대기한다. printer의 모니터를 가진 스레드가 x4행의 동기화 블록을 완전히 빠져 나오면 printer의 모니터를 반납하게 된다. 대기하고 있던 스레드 중, 한 스레드가 printer의 모니터를 획득하고 동기화 블록에 진입하여 실행(출력)한다.

 

x5행 이후로 pt1, pt2 그리고 pt3이 차례대로 start한다. 이들 세 스레드는 스레드 스케줄러에 의해 실행 순서가 정해지므로 프로그래머는 어느 스레드가 맨 먼저 실행될지는 알 수 없다. 하지만 출력 결과를 보면 pt1이 맨 먼저 printer의 모니터를 획득했음을 알 수 있다. 그리고 pt1이 동기화 블록을 빠져 나오면 pt3가 printer의 모니터를 획득하고 출력을 시작하여 동기화 블록을 빠져 나온다. 마지막으로 pt2가 printer의 모니터를 획득하고 출력을 시작한다.

 

 

동기화 블록이 다른 위치에 있을 경우를 살펴보자.

 

Synchronization4.java

 

class PrintThread3 extends Thread{

  char ch;

  static Object printer=new Object();

  public PrintThread3(char ch){

    this.ch=ch;

  }

  void printCh10(){

    synchronized(printer){          // x1, 동기화 블록 시작

      for(int i=1;i<=10;i++)           // x2

        System.out.print(ch);         // x3

    }                               // 동기화 블록 끝

  }

  public void run(){

    for(int i=1;i<=5;i++){

      printCh10();

      System.out.println();

    }

  }

}

public class Synchronization4{

  public static void main(String[] args){

    PrintThread3 pt1=new PrintThread3('A');

    PrintThread3 pt2=new PrintThread3('B');

    PrintThread3 pt3=new PrintThread3('C');

    pt1.start();

    pt2.start();

    pt3.start();

  }

}


 

출력 결과

 

AAAAAAAAAA

CCCCCCCCCC

AAAAAAAAAA

BBBBBBBBBB

...


 

x2행과 x3행이 동기화 블록으로 묶어져 있으므로 printer의 모니터를 가진 스레드가 동기화 블록 안의 명령을 실행하고 있다면 다른 스레드가 동기화 블록을 만났을 때 x1행에서 대기하게 된다. 위와 같이 출력되는 이유는 모니터를 가진 스레드가 문자를 10회 출력한 후에 다른 스레드에게 모니터를 넘겨주기 때문이다.

 

 

혼자 해보기

 Alone14_3.java

다음 소스를 수정하여 출력 결과와 같이 실행되도록 동기화 코드를 삽입해보자.

 

class Alone14_3 extends Thread{

  int[] array;                                       // 정수형 배열

  static StringBuffer sb=new StringBuffer();          // 공유 객체

  public Alone14_3(int[] array){                     // 생성자

    this.array = array;

  }

  public void addLeft(){                            // 배열의 왼쪽 반을 sb에 추가

    for(int i=0; i<array.length/2; i++){

      sb.append(array[i]);

      try{ sleep(100); }catch(Exception e){}

    }

  }

  public void addRight(){                         // 배열의 오른쪽 반을 sb에 추가

    for(int i=array.length/2; i<array.length; i++){

      sb.append(array[i]);

      try{ sleep(100); }catch(Exception e){}

    }

  }

  public void run(){

    addLeft(); addRight();

  }

  public static void main(String[] args){

    Alone14_3 a=new Alone14_3(new int[]{1,1,1,1,1,2,2,2,2,2});

    Alone14_3 b=new Alone14_3(new int[]{3,3,3,3,3,4,4,4,4,4});

    a.start(); b.start();

    try{ a.join(); b.join(); }catch(InterruptedException ie){}

    System.out.println(sb);

  }

}

 

출력 결과 예시

 

경우1: "33333111114444422222"

경우2: "11111333332222244444"


 

 

 

동기화 메소드(synchronized method)

동기화 블록은 블록 안의 코드만을 동기화하는 반면에 동기화 메소드는 메소드 전체를 동기화한다. 동기화 메소드의 형태는 다음과 같다.

 

synchronized void f(){

  [코드]

}

 

동기화 메소드도 동기화 블록처럼 모니터를 가진 스레드에게 실행권한을 주는 방식으로 동기화 블록과 크게 다르지 않다. 그렇다면 여기서 말하는 모니터는 어떤 객체의 모니터일까? 바로 자기 자신의 모니터, 즉, this의 모니터를 말하는 것이다.

 

어떤 스레드가 this의 f()를 실행하고 있다면 다른 스레드는 f()를 만났을 때 대기하게 된다. 다른 말로 this의 모니터를 얻은 객체가 f()를 실행할 수 있다.

 

따라서 동기화 메소드를 동기화 블록으로 변환한다면 다음과 같을 것이다.

 

void f(){

  synchronized(this){

    [코드];

  }

}

 

 

다음은 예제는 쓰레드가 공유하고 있는 객체에 대하여 동기화 메소드를 사용한 것이다.

 

Synchronization5.java

 

class Printer{

  synchronized void printChar(char ch){   // x1, 동기화 메소드, 문자를 10회 출력

    for(int i=1;i<=10;i++)

      System.out.print(ch);

  }

}

class PrinterThread extends Thread{

  Printer ptr;                                              // 공유할 객체

  char ch;                                                 // 출력할 문자

  PrinterThread(Printer ptr, char ch){           // 생성자

    this.ptr=ptr;

    this.ch=ch;

  }

  public void run(){

    for(int i=1;i<=10;i++){

      ptr.printChar(ch);                                    // x2

      System.out.println();

    }

  }

}

public class Synchronization5{

  public static void main(String[] args){

    Printer ptr=new Printer();                                     // 공유 객체

    PrinterThread pt1=new PrinterThread(ptr,'A');        // x3

    PrinterThread pt2=new PrinterThread(ptr,'B');        // x4

    pt1.start();

    pt2.start();

  }

}


 

출력 결과

 

AAAAAAAAAA

...

BBBBBBBBBB

AAAAAAAAAA

BBBBBBBBBB

...

BBBBBBBBBB

 


 

x1행의 printChar()는 동기화 메소드이다. 따라서 이 메소드를 실행할 수 있는 스레드는 printChar()을 멤버로 가지는 객체의 모니터를 가지는 스레드이다. 그런데 주의할 점은 스레드가 Printer 클래스로부터 생성되는 객체를 공유하고 있어야 동기화가 적용된다는 것이다. x3행과 x4행에서 ptr이 공유되고 있음을 확인하자. x2행을 실행할 수 있는 스레드는 ptr의 모니터를 가진 스레드이다. ptr의 모니터를 가진 스레드가 메소드를 실행하고 빠져 나오면 x2행에서 대기하고 있던 나머지 스레드 중의 한 놈이 ptr의 모니터를 얻고 printChar 메소드를 실행한다.

 

x1행의 synchronized 키워드를 삭제하고 실행해보면 동기화와 비동기화의 차이를 알 수 있다.

 

 

혼자 해보기

 Alone14_4.java

출력 결과와 같이 실행되도록 아래 소스를 수정해 보자.

 

public class Alone14_4 extends Thread{

  IntArrayBuffer iab;

  int num;

  public Alone14_4(IntArrayBuffer iab , int num){

    this.iab = iab;

    this.num = num;

  }

  public void run(){

    iab.add(new int[]{num, num, num, num, num});

  }

  public static void main(String[] args){

    IntArrayBuffer iab=new IntArrayBuffer();

    Alone14_4 ob1 = new Alone14_4(iab, 1);

    Alone14_4 ob2 = new Alone14_4(iab, 2);

    ob1.start(); ob2.start();

    try{ ob1.join(); ob2.join(); }catch(InterruptedException ie){}

    System.out.println(iab.sb);

  }

  static class IntArrayBuffer{

    StringBuffer sb=new StringBuffer();

 

    public void add(int[] array){

      for(int i=0; i<array.length; i++){

        sb.append(array[i]);

        try{Thread.sleep(100);}catch(Exception e){}

      }

 

    }

  }

}

 

출력 결과 예시

 

경우1: "1111122222"

경우2: "2222211111"


 

 

 

생산자와 소비자

어떤 햄버거 가게가 있다. 이 햄버거 가게에는 햄버거생산을 담당하는 한 명의 주방장과 햄버거판매를 담당하는 한 명의 종업원이 있다. 그리고 주방장이 만든 햄버거를 담아두는 상자가 하나 있다.

 

주방장은 계속해서 햄버거를 만드는데 하나의 햄버거가 만들어지면 햄버거 상자에 담아 둔다. 그리고 종업원은 손님이 올 때마다 햄버거 상자에서 햄버거를 하나씩 꺼내어 손님에게 판다. 주방장은 하나의 햄버거를 만드는데 일정한 시간을 소비한다. 햄버거를 사기 위해 끊임없이 손님이 가게로 들어온다고 가정했을 때, 햄버거 상자가 비는 경우가 생길 것이다. 햄버거 상자가 비었을 때 손님을 돌려보낼 것인가? 일반적으로 다음과 같이 할 것이다(?).

 

종업원이 햄버거 상자를 열었을 때 햄버거가 하나도 없다면 종업원은 잠시 기다린다. 주방장이 햄버거를 만들어 상자에 넣은 후에 종업원에게 알리면, 대기하고있던 종업원이 햄버거를 꺼내어 손님에게 판다.

 

주방장과 종업원을 쓰레드라고 가정하자. 위에서 진한 글씨의 '기다리다'와 '알리다'가 보일 것이다. 쓰레드를 기다리게 하는 메소드로 wait()이 있고, 기다리고 있는 스레드에게 알리는 메소드로는 notify()가 있다. 그런데 특이한 점은 wait()과 notify()는 Thread 클래스의 멤버가 아니고 Object 클래스의 멤버라는 것이다. 사실 기다리거나 알리는 것도 객체의 모니터를 이용하는 것이기 때문이다.

 

wait()은 스레드로 하여금 해당 객체의 모니터를 반납하고 대기하게 하는 메소드이다. 이 메소드는 스레드를 대기상태로 만들기 때문에 sleep()과 같이 InterruptedException을 던진다.

 

다음은 wait()을 호출하는 코드 조각이다.

 

synchronized(key){                   // x1

  ...

  try{

    if(조건)key.wait();                 // x2

  }catch(InterruptedException ie){}

  ...

}

 

위와 같이 동기화 블록 또는 동기화 메소드 안에서만 wait()을 호출할 수 있다. 객체의 모니터를 반납하기 위해서는 먼저 객체의 모니터를 가지고 있는 상태이어야 하기 때문이다.

x1행에서 key에 대한 모니터를 가지고 동기화 블록을 실행하다가 x2행의 조건을 만족하면 key의 모니터를 반납한다. 모니터를 반납하였기 때문에 스레드는 x2행에서 대기 상태가 된다.

 

key를 햄버거 상자로 생각해 보자. 종업원이 햄버거 상자에 대한 모니터를 얻고 햄버거 상자를 열었을 때 햄버거가 하나도 없다면 햄버거 상자에 대한 모니터를 반납하고 대기하게 하면 된다.

 

notify()는 대기 중인 스레드에게 알려서 깨우는 기능을 한다. 다음의 notify()를 호출하는 코드 조각이다.

 

synchronized(key){      // x1

  ...

  key.notify();           // x2

  ...

}

 

notify()도 wait()과 같이 동기화 블록이나 동기화 메소드 안에서만 사용된다. x1행에서 key의 모니터를 가지고 실행하다가 x1행을 만나면 wait()메소드에 의해 대기 중이던 스레드에게 알려 스레드를 깨운다.

 

마찬가지로 key를 햄버거 상자로 생각해 보자. 주방장이 햄버거를 만들어서 햄버거 상자에 담고 '햄버거 상자.wait()'에 의해 대기 중인 종업원에게 알린다. 종업원은 깨어나서 하던 작업을 계속해서 할 것이다.

 

이제 햄버거 가게를 최대한 단순화하여 코드로 옮겨보자. 우선 햄버거 상자와 몇 가지 변수를 포함하는 클래스를 정의하자.

 

class Ham{

  static Object box=new Object();    // x1, 햄버거 상자

 

  static int 총재료=6;       // 햄버거를 만들 수 있는 재료의 량(6개 만들 수 있다.)

  static int 판매량=0;       // 햄버거 판매 수량(맨 처음에는 0이다.)

  static int 생산량=3;       // 손님이 오기 전에 3개 먼저 만듦

}

 

x1행의 box 객체는 단순히 햄버거 상자임을 나타내는 것이다. 실제로는 저렇게 단순하지 않겠지만... Ham 클래스의 모든 변수를 static으로 정한 이유는 주방장과 종업원이 box와 각각의 변수를 공유하도록 하기 위한 것이다.

 

 

이제 주방장 클래스를 정의하여 보자.

 

HMaker.java

 

class HMaker extends Thread{      // 주방장 클래스

 

  void make(){               // 햄버거 하나를 만들고 종업원에게 알리는 메소드

 

    // 햄버거 상자에 대한 모니터를 가졌을 때 만듦

    synchronized(Ham.box){

      Ham.생산량++;             // 1개의 햄버거를 만들어 상자에 담는다.

      System.out.println("주방장: 햄버거 여기 있어!! (총 "+Ham.생산량+"개 생산)");

 

      Ham.box.notify();        // x1, 종업원에게 알린다.

    }

  }

  public void run(){

    while(Ham.생산량<Ham.총재료){     // 있는 재료만큼 햄버거를 만든다.

      try{

        sleep(3000);          // 하나의 햄버거를 만들고 3초간 대기(주방장의 능력)

      }catch(InterruptedException ie){}

      make();  // 햄버거 하나 만듦

    }

  }

}


 

x1행의 'Ham.box.notify()'은 'Ham.box.wait()'에 의해 대기 중인 스레드를 깨우는 것인데, 나중에 종업원 클래스에서 'Ham.box.wait()'을 호출한다.

 

이제 종업원 클래스를 정의하여 보자.

 

HAssistant.java

 

class HAssistant extends Thread{ // 종업원 클래스

 

  void sell(){                     // 하나의 햄버거를 파는 메소드

 

    // 햄버거 상자의 모니터를 가지고 있을 때 판다.

    synchronized(Ham.box){

 

      if(Ham.생산량==Ham.판매량){     // x1, 햄버거 상자에 햄버거가 없을 때

        System.out.println("종업원: 잠시만 기다리세요.");

        try{

          Ham.box.wait();       // x2, 햄버거 상자의 모니터를 반납하고 기다린다.

        }catch(InterruptedException ie){}  // 주방장이 종업원을 깨우면 예외 발생

    }

    

    Ham.판매량++;  // 햄버거 상자에서 햄버거를 꺼내온다. 판매량이 증가한다.

 

    System.out.println(

             "종업원: 손님, 햄버거 나왔어요.(총 "+Ham.판매량+"개 판매)");

    } 

  }

  public void run(){

    while(Ham.판매량<Ham.총재료){

      System.out.println("<손님이 햄버거를 주문한다.>");

      sell();                  // 햄버거를 판다.

      try{

        sleep(1000);       // 햄버거를 팔면 1초간 쉰다(1초 후에 손님이 찾아온다).

      }catch(InterruptedException ie){}

    }

  }

}


 

x1행에서 판매량과 생산량이 같다면 햄버거 상자에 햄버거가 없다는 말이 된다. 이 때는 x2행에서 햄버거 상자의 모니터를 반납하고 대기한다. 나중에 주방장 스레드가 깨워줄 것이다.

 

이제 위에서 만들어진 세 클래스를 바탕으로 주방장 스레드와 종업원 스레드를 start시켜 보자.

 

Synchronization6.java

 

public class Synchronization6{

  public static void main(String[] args){

    HMaker maker=new HMaker();

    HAssistant assistant=new HAssistant();

    maker.start();

    assistant.start();

  }

}


 

출력 결과

 

<손님이 햄버거를 주문한다.>

종업원: 손님, 햄버거 나왔어요.(총 1개 판매)

<손님이 햄버거를 주문한다.>

...

주방장: 햄버거 여기 있어!! (총 6개 생산)

종업원: 손님, 햄버거 나왔어요.(총 6개 판매)


 

실행 결과를 검토해 보자. 이미 햄버거 상자에 햄버거가 3개 있으므로 종업원은 햄버거를 팔기 시작할 것이다. 그러다가 햄버거 상자가 비어서 종업원은 주방장이 햄버거를 만들기 전까지 대기한다. 주방장이 햄버거를 만들어 햄버거 상자에 넣어두고 종업원에게 알리면 종업원은 깨어나서 햄버거 상자에 있는 햄버거를 꺼내어 손님에게 판다.

 

 

 

다중 소비자

앞에서 다룬 생산자·소비자 문제는 그럴 듯 하다. 하지만 많은 문제점을 앉고 있는데 종업원이 두 명 이상이 되면 이상한 결과를 초래할 것이다. wait()에 의해 대기 중인 스레드가 여럿이라면 notify()를 호출했을 때 깨어나는 스레드는 어떤 것일까? 사실 알 수 없고 대기하고 있던 스레드 중에서 임의의 스레드가 깨어난다. 따라서 나머지 중 어떤 것은 한번도 깨어나지 않고 영원히 잠들 수도 있을 것이다. 이와 같은 왕자 없는 백설공주 현상을 없애기 위해 여러 가지 방법을 제시할 수 있다. 하나의 방법으로 모든 스레드를 깨우고 보는 것이다. 모든 스레드를 깨우고자 할 때는 notifyAll()을 호출하면 되는데 유의해야 할 것이 있다. 예를 들어 주방장이 하나의 햄버거를 만들고 종업원을 모두 깨우면, 모든 종업원이 하나씩 꺼내 가는 결과를 초래할 수 있다는 것이다. 만든 것은 하나인데 말이다. 만들지 않은 것을 꺼내는 것은 있을 수 없으므로 프로그램의 오류로 작동하게 될 것이다. 하지만 코드로써 처리할 수 있다.

 

다음은 다중 점원을 고려해서 앞의 예제를 수정한 것이다.

 

Synchronization7.java

 

class HMaker2 extends Thread{

  void make(){

    synchronized(Ham.box){

      Ham.생산량++;

      System.out.println("주방장: 햄버거 여기 있어!! (총 "+Ham.생산량+"개 생산)");

      Ham.box.notifyAll();       // 모든 종업원을 깨운다.

    }

  }

  public void run(){

    while(Ham.생산량<Ham.총재료){

        try{

          sleep(3000);

        }catch(InterruptedException ie){}

        make();

      }

  }

}

 

class HAssistant2 extends Thread{

  void sell(){

    synchronized(Ham.box){

      if(Ham.생산량==Ham.판매량){

        System.out.println(getName()+": 잠시만 기다리세요.");   

        try{

          Ham.box.wait();

        }catch(InterruptedException ie){}

      }

      if(Ham.생산량 >Ham.판매량){   // x1, 깨어났을 때 한번 더 검사한다.

        Ham.판매량++;

        System.out.println(

          getName()+": 손님, 햄버거 나왔어요. (총 "+Ham.판매량+"개 판매)");

      }

    }

  }

  public void run(){

    while(Ham.판매량<Ham.총재료){

      System.out.println("<손님이 "+getName()+"에게 햄버거를 주문한다.>");

      sell();

      try{

        sleep(1000);

      }catch(InterruptedException ie){}

    }

  }

}

public class Synchronization7{

  public static void main(String[] args){

    HMaker2 maker=new HMaker2();

    HAssistant2 assistant1=new HAssistant2();

    HAssistant2 assistant2=new HAssistant2();

    assistant1.setName("방실이");

    assistant2.setName("은실이");

    maker.start();

    assistant1.start();

    assistant2.start();

  }

}


 

출력 결과

 

<손님이 방실이에게 햄버거를 주문한다.>

방실이: 손님, 햄버거 나왔어요.(총 1개 판매)

...

은실이: 잠시만 기다리세요.

주방장: 햄버거 여기 있어!! (총 6개 생산)

은실이: 손님, 햄버거 나왔어요.(총 6개 판매)


 

종업원이 깨어났을 때 x1행에서 햄버거 상자를 한번 더 검사하여 버그를 잡을 수 있다.

 

스레드를 잘 컨트롤하는 것은 상당히 힘든 일이다. 많은 프로그램을 해봄으로써 실력을 향상하는 것이 최선의 길일 것이다.

 

 

 

stop(), suspend(), resume()

stop(), suspend() 그리고 rusume()은 jdk1.4.x 이후로 비난받는 메소드로 이런 메소드를 deprecated API라고 한다. jdk의 버전이 업그레이드되면서 추가되는 클래스나 메소드가 있는 반면에 기능상의 하자로 인해 없어지거나 수정되는 메소드가 생겼다. 하지만 낮은 버전과의 호환 때문에 지원하고 있다. 그러나 이런 메소드는 사용을 자제해야 한다. 아니 사용하지 말아야한다. 이렇게 사용의 자제를 요구하는 API를 deprecated API라고 한다.

 

stop()은 스레드를 강제로 멈추게 할 때 사용되었는데 안전상의 이유로 deprecated되었다. java.sum.com을 방문하면 보다 자세한 내용을 알 수 있다.

 

다음 코드를 컴파일 하면 deprecated 되었다는 메시지를 볼 수 있을 것이다.

 

Deprecation1.java

 

public class Deprecation1 extends Thread{

  public void run(){

    while(true)

      System.out.println("Hi~~~");

  }

  public static void main(String[] args){

    Deprecation1 dt=new Deprecation1();

    dt.start();

    dt.stop();           // deprecated API

  }

}


 

deprecate되었다고 해서 실행되지 않는 것은 아니다. 실행해 보면 스레드가 멈추는 것을 볼 수 있다. 하지만 사용을 자제해야한다. 그렇다면 stop()을 사용하지 않고 스레드를 멈출 수 있을까? 다음 예제를 통해 확인해 보자.

 

Deprecation2.java

 

public class Deprecation2 extends Thread{

  boolean runnable=true;

  void stopThread(){

    runnable=false;                     // 이 부분이 스레드를 멈추게 한다.

  }

  public void run(){

    while(runnable)                      // runnable이 false이면 스레드는 멈춘다.

      System.out.println("Hi~~~");

  }

  public static void main(String[] args){

    Deprecation2 dt=new Deprecation2();

    dt.start();

    try{

      Thread.sleep(1000);               // dt 스레드가 실행할 수 있는 시간을 준다.

    }catch(InterruptedException ie){}

 

    dt.stopThread();                   // 스레드를 멈춘다.

 

    System.out.println("스레드 종료");

  }

}


 

출력 결과

 

Hi~~~

...

Hi~~~

Hi~~~

스레드 종료


 

 

쉬운 예제이므로 쉽게 이해했을 것이다.

 

 

 

 

연습 문제

 

 

1. 게임 프로그래밍에서 스레드가 어떻게 사용될 수 있는지 특정 게임을 예로 들어 구체적으로 설명해보자.

 

 

2. 다음의 스레드들의 우선 순위를 결정해보자.

 

① 파일 관리 프로그램: 파일을 복사하는 스레드, 복사 진행상황을 보여주는 스레드

② 웹브라우저: 웹 페이지를 다운로드 하는 스레드, 화면을 업데이트하는 스레드

③ 게임 프로그램: 캐릭터를 움직이는 스레드, 채팅 메시지를 보여주는 스레드

④ 바둑 프로그램: 둘 곳을 생각하는 스레드(컴퓨터), 컴퓨터의 생각 시간을 보여주는 스레드

 

 

3. 은행의 '계좌 관리 프로그램'을 예로 들어서 동기화의 중요성을 설명해보자.

 

   계좌 관리 프로그램의 주요 내용: 입금, 출금, 예금 조회, 계좌 이체

 

 

4. 어떤 햄버거 가게에 두 명의 주방장과 한 명의 종업원이 있고, 두 종류의 햄버거, A와 B가 있다. 한 명의 주방장은 A햄버거만 만들고, 다른 한 명의 주방장은 B햄버거만 만든다고 가정했을 때, 햄버거 가게를 프로그래밍 해보자. 주방장가 종업원은 스레드로 구현하자.

 

   A햄버거 초기 생산량: 20개

   B햄버거 초기 생산량: 30개

   A햄버거 제조시간: 3초

   B햄버거 제조시간: 4초

   A햄버거를 구하는 고객의 빈도: 1초당 한 명

   A햄버거를 구하는 고객의 빈도: 2초당 한 명

 

 



이전 페이지로


전체 페이지수 : 79, 게시물 수 : 790
랜덤게시물 : 문자열과 특수문자 php 코딩 규약 (PEAR 표준) 가상 세계에서는 나도 다른 성별?? 사이버 성전환 경험은.. api2강 마우스 오른쪽 버튼 누르면 화면에 점 찍히기 DirectX SDK 의 사용법 그림이 아래로 떨어지게 하는 효과2 B U T T O N 태 그 참고1 1-1-가.윈도우즈의 역사 입출력함수1 파일 처리 함수(2) api 강좌 덧셈 뺄셈 계산기 코딩 [구현중] 사이트 방문경로와 검색한 단어 알기 3-1-나. 문자열 출력 [mfc강좌1] 비쥬얼베이직 1.4 STL의 구조 Flash Player 설치 오류 나시는분들에게.....(최신버전포함) c++ 함수에 대해서 include(), require(), require_once() 차이점 실패자가 되기 위한 10가지 충고


링크 : 중독성게임 | 디펜스게임 | 심리테스트 | 웹게임 | 종이접기 | | 자동차갤러리 | 고전게임 | 성경바이블 | 개발 | 다운 | 자유 | 웃긴 | RSS | UCC | 유니티